Fix NaN logits from deterministic Triton extend on the unified memory pool (#33517)
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
parent
0661eb1c50
commit
8a7c8a72d6
@@ -1678,7 +1678,16 @@ class TritonAttnBackend(AttentionBackend):
|
||||
and isinstance(pool, SWAKVPool)
|
||||
and pool.layers_mapping[layer.layer_id][1]
|
||||
):
|
||||
# Consumes VIRTUAL ids, so it must see out_cache_loc untranslated.
|
||||
extend_kv_indices = pool.translate_loc_from_full_to_swa(extend_kv_indices)
|
||||
elif self.forward_metadata.out_cache_loc_full_physical is not None:
|
||||
# Unified pool: this kernel reads the extend half OUT OF THE POOL (the
|
||||
# 2-stage path takes it from the k/v arguments), so it needs the same
|
||||
# translated loc the KV write uses -- otherwise the prefix is read at
|
||||
# physical ids and the extend tokens at virtual ones. Reuse the
|
||||
# per-forward translation rather than re-translating: this runs once
|
||||
# per layer.
|
||||
extend_kv_indices = self.forward_metadata.out_cache_loc_full_physical
|
||||
|
||||
# Handle cases where extend_seq_lens or extend_start_loc might not be set
|
||||
# In speculative decoding, we can infer these from spec_info or compute them
|
||||
|
||||
@@ -0,0 +1,118 @@
|
||||
"""Deterministic inference on the unified memory pool (virtual-id KV).
|
||||
|
||||
`--enable-deterministic-inference` routes Triton extend through
|
||||
`TritonAttnBackend._forward_extend_unified`, a 1-stage kernel that reads BOTH
|
||||
the prefix and the extend half of the KV out of the pool -- unlike the default
|
||||
2-stage path, which takes the extend half from its `k`/`v` arguments. On the
|
||||
unified memory pool `forward_batch.out_cache_loc` holds VIRTUAL ids, so feeding
|
||||
it to that kernel untranslated read the prefix at physical ids and the extend
|
||||
tokens at virtual ones. The mismatch produced garbage logits, surfacing as
|
||||
`NaN detected! sampler: next_token_logits` once CI arms
|
||||
`SGLANG_ENABLE_ASYNC_ASSERT` and as silent corruption without it.
|
||||
|
||||
Needs all three of unified memory + Triton attention + deterministic inference;
|
||||
any two are clean, which is why no existing test covered it.
|
||||
"""
|
||||
|
||||
import unittest
|
||||
|
||||
import requests
|
||||
|
||||
from sglang.srt.utils import kill_process_tree
|
||||
from sglang.test.ci.ci_register import register_cuda_ci
|
||||
from sglang.test.test_utils import (
|
||||
DEFAULT_TIMEOUT_FOR_SERVER_LAUNCH,
|
||||
DEFAULT_URL_FOR_TEST,
|
||||
CustomTestCase,
|
||||
popen_launch_server,
|
||||
)
|
||||
|
||||
register_cuda_ci(est_time=300, stage="base-b", runner_config="1-gpu-large")
|
||||
|
||||
KIMI_LINEAR_MODEL = "yujiepan/kimi-linear-tiny-random"
|
||||
SERVER_ENV = {"SGLANG_BATCH_INVARIANT_OPS_ENABLE_MM_DEEPGEMM": "0"}
|
||||
# Triton is pinned on purpose here: the defect lives in the Triton deterministic
|
||||
# extend kernel, so resolving to another backend would not exercise it.
|
||||
BASE_ARGS = [
|
||||
"--trust-remote-code",
|
||||
"--skip-tokenizer-init",
|
||||
"--random-seed",
|
||||
"1",
|
||||
"--enable-deterministic-inference",
|
||||
"--attention-backend",
|
||||
"triton",
|
||||
"--linear-attn-backend",
|
||||
"triton",
|
||||
"--mamba-backend",
|
||||
"triton",
|
||||
"--max-mamba-cache-size",
|
||||
"32",
|
||||
"--max-total-tokens",
|
||||
"4096",
|
||||
"--cuda-graph-backend-decode",
|
||||
"disabled",
|
||||
"--cuda-graph-backend-prefill",
|
||||
"disabled",
|
||||
]
|
||||
|
||||
|
||||
class TestUnifiedMemoryDeterministicParity(CustomTestCase):
|
||||
"""Unified pool must match the static pool token-for-token. A multi-token
|
||||
prompt is required: the defect only shows once an extend batch writes tokens
|
||||
that the same forward then reads back through `out_cache_loc`."""
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
cls.model = KIMI_LINEAR_MODEL
|
||||
cls.base_url = DEFAULT_URL_FOR_TEST
|
||||
|
||||
@classmethod
|
||||
def _generate(cls):
|
||||
response = requests.post(
|
||||
cls.base_url + "/generate",
|
||||
json={
|
||||
"input_ids": [1] + [100 + i % 1000 for i in range(256)],
|
||||
"sampling_params": {
|
||||
"temperature": 0,
|
||||
"max_new_tokens": 4,
|
||||
"ignore_eos": True,
|
||||
},
|
||||
"return_logprob": True,
|
||||
},
|
||||
timeout=120,
|
||||
)
|
||||
response.raise_for_status()
|
||||
return response.json()["meta_info"]["output_token_logprobs"]
|
||||
|
||||
def _run(self, extra_args):
|
||||
process = popen_launch_server(
|
||||
self.model,
|
||||
self.base_url,
|
||||
timeout=DEFAULT_TIMEOUT_FOR_SERVER_LAUNCH,
|
||||
other_args=BASE_ARGS + extra_args,
|
||||
env=SERVER_ENV,
|
||||
)
|
||||
try:
|
||||
return self._generate()
|
||||
finally:
|
||||
kill_process_tree(process.pid, wait_timeout=60)
|
||||
|
||||
def test_matches_static_pool(self):
|
||||
unified = self._run(["--enable-unified-memory"])
|
||||
static = self._run([])
|
||||
|
||||
self.assertEqual(len(unified), 4)
|
||||
for logprob, _token_id, _text in unified:
|
||||
self.assertEqual(logprob, logprob, "logprob is NaN")
|
||||
|
||||
self.assertEqual(
|
||||
[token_id for _lp, token_id, _t in unified],
|
||||
[token_id for _lp, token_id, _t in static],
|
||||
"unified pool diverged from the static pool",
|
||||
)
|
||||
for (unified_lp, _uid, _ut), (static_lp, _sid, _st) in zip(unified, static):
|
||||
self.assertAlmostEqual(unified_lp, static_lp, delta=0.05)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Reference in New Issue
Block a user