[Core][V1] Support trace_decode_token_ids for deterministic decode replay (#46701)
Signed-off-by: ianzxliu <ianzxliu@tencent.com> Signed-off-by: aoshen02 <aoshen@inferact.ai> Signed-off-by: Zhixia Liu <liuzhixia.nju@gmail.com> Co-authored-by: ianzxliu <ianzxliu@tencent.com> Co-authored-by: Claude <noreply@anthropic.com> Co-authored-by: aoshen02 <aoshen@inferact.ai> Co-authored-by: Codex <noreply@openai.com> Co-authored-by: Nick Hill <nickhill123@gmail.com>
Zhixia Liu committed
Aug 20, 2026 at 15:12 UTC
a1c5b1fd9f6ef06a4fa236b7d48350115e5688b9
13 files changed
+931
-9
docs/serving/online_serving/trace_replay.md
new
+60
@@ -0,0 +1,60 @@
1
+# Trace Replay
2
+
3
+Trace replay forces the engine to emit a predetermined token sequence during decoding while computing real logprobs from the model's unmodified logit distribution. The primary use case is comparing logprob distributions across different configurations:
4
+
5
+- **Inference config diff**: compare logprobs between different quantization schemes, tensor parallelism layouts, or attention backends for the same model.
6
+- **Train vs inference diff**: replay a training-time token sequence through the inference engine to detect logprob divergence caused by numerical differences between training and serving frameworks.
7
+
8
+## Requirements
9
+
10
+Trace replay reserves a per-request trace buffer, so it is off by default. Enable it with `--enable-trace-replay` (or `LLM(..., enable_trace_replay=True)`). It is only supported by model runner V2; requests are rejected when either condition is not met.
11
+
12
+## Usage
13
+
14
+```python
15
+from vllm import LLM, SamplingParams
16
+
17
+llm = LLM(model="Qwen/Qwen3-0.6B", enable_trace_replay=True)
18
+
19
+# Token sequence captured from a previous run or training log
20
+trace_tokens = [15, 284, 1026, 374]
21
+
22
+params = SamplingParams(
23
+ trace_decode_token_ids=trace_tokens,
24
+ logprobs=5,
25
+)
26
+outputs = llm.generate(["Once upon a time"], sampling_params=params)
27
+
28
+for token, logprob in zip(
29
+ outputs[0].outputs[0].token_ids,
30
+ outputs[0].outputs[0].logprobs,
31
+):
32
+ print(f"token={token} logprob={logprob[token].logprob:.4f}")
33
+```
34
+
35
+The output tokens will always be `[15, 284, 1026, 374]`. The logprobs reflect the model's true probability for each forced token under the current inference configuration.
36
+
37
+To compare configurations, run the same trace against two engine setups and diff the per-token logprobs.
38
+
39
+## Behavior
40
+
41
+When `trace_decode_token_ids` is set:
42
+
43
+- `max_tokens` is automatically set to the trace length.
44
+- The trace is truncated if it does not fit within `max_model_len` given the prompt length.
45
+- All stop conditions (EOS, stop strings, stop token IDs) are disabled.
46
+- Generation produces exactly the trace tokens, then stops.
47
+
48
+## Limitations
49
+
50
+`trace_decode_token_ids` is incompatible with the following features (raises `ValueError`):
51
+
52
+| Feature | Reason |
53
+| --- | --- |
54
+| `n > 1` | Trace replay produces a single deterministic sequence |
55
+| `prompt_logprobs` | Expanded logit layout conflicts with trace kernel indexing |
56
+| Speculative decoding | Multi-token speculation conflicts with single-token trace stepping |
57
+| Structured outputs | Grammar constraints conflict with forced token injection |
58
+| `repetition_detection` | Would terminate the request on repeated patterns in the trace |
59
+| `thinking_token_budget` | Logit masking corrupts logprobs for trace tokens |
60
+| `bad_words` | Logit masking corrupts logprobs for trace tokens |
examples/generate/trace_replay_offline.py
new
+195
@@ -0,0 +1,195 @@
1
+# SPDX-License-Identifier: Apache-2.0
2
+# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3
+"""
4
+Trace-replay with vLLM offline inference.
5
+
6
+Trace-replay lets you supply a known sequence of decode token IDs alongside
7
+the prompt. Instead of sampling from the model distribution, the engine
8
+injects each decode token deterministically, step by step. All other
9
+outputs — logprobs, token ranks, text decoding — are computed faithfully
10
+from the real logit distribution for that token.
11
+
12
+How it works:
13
+ 1. Start the engine with ``--enable-trace-replay`` (or
14
+ ``LLM(..., enable_trace_replay=True)``).
15
+ 2. Set ``SamplingParams.trace_decode_token_ids`` to the list of decode
16
+ token IDs you want to force.
17
+ 3. The engine will output exactly those tokens and stop. ``max_tokens``
18
+ is overwritten with the trace length, and the trace is truncated if it
19
+ does not fit within ``max_model_len``. EOS tokens inside the trace
20
+ sequence do **not** halt generation early.
21
+
22
+Requires model runner V2. Requests are rejected with ``ValueError`` when
23
+combined with any of:
24
+ * n > 1 * Speculative decoding
25
+ * prompt_logprobs * Structured outputs
26
+ * repetition_detection * thinking_token_budget
27
+ * bad_words
28
+
29
+Typical use-cases:
30
+ * Reproduce exact outputs from a previous run for benchmarking.
31
+ * Compute logprobs for an already-known output (e.g. reference answers).
32
+ * Dataset annotation: given (prompt, response) pairs, obtain per-token
33
+ logprob scores without altering the response.
34
+
35
+Usage:
36
+ python examples/generate/trace_replay_offline.py
37
+ python examples/generate/trace_replay_offline.py --model facebook/opt-125m
38
+"""
39
+
40
+import argparse
41
+
42
+from vllm import LLM, SamplingParams
43
+
44
+DEFAULT_PROMPT = "Hello, my name is"
45
+
46
+
47
+def build_llm(args: argparse.Namespace) -> LLM:
48
+ """Construct an LLM from common CLI args."""
49
+ llm_kwargs: dict = {
50
+ "model": args.model,
51
+ "trust_remote_code": args.trust_remote_code,
52
+ "tensor_parallel_size": args.tensor_parallel_size,
53
+ "enforce_eager": args.enforce_eager,
54
+ "gpu_memory_utilization": args.gpu_memory_utilization,
55
+ "max_num_seqs": args.max_num_seqs,
56
+ "enable_trace_replay": True,
57
+ }
58
+ if args.max_model_len is not None:
59
+ llm_kwargs["max_model_len"] = args.max_model_len
60
+ return LLM(**llm_kwargs)
61
+
62
+
63
+def run_normal_generation(llm: LLM, prompt: str, max_tokens: int) -> list[int]:
64
+ """Run a standard greedy generation and return the output token IDs."""
65
+ sampling_params = SamplingParams(
66
+ temperature=0.0,
67
+ max_tokens=max_tokens,
68
+ # Request logprobs for the greedy token at each step so we can
69
+ # compare them against the trace-replay logprobs below.
70
+ logprobs=1,
71
+ )
72
+ outputs = llm.generate([prompt], sampling_params=sampling_params)
73
+ result = outputs[0].outputs[0]
74
+ output_token_ids = list(result.token_ids)
75
+ print("[Normal generation]")
76
+ print(f" Prompt : {prompt!r}")
77
+ print(f" Output token IDs : {output_token_ids}")
78
+ print(f" Output text : {result.text!r}")
79
+ return output_token_ids
80
+
81
+
82
+def run_trace_replay(llm: LLM, prompt: str, decode_token_ids: list[int]) -> None:
83
+ """Replay a known decode sequence and print per-token logprobs."""
84
+ sampling_params = SamplingParams(
85
+ # Provide the decode tokens to replay.
86
+ trace_decode_token_ids=decode_token_ids,
87
+ # Request top-5 logprobs so we can inspect the distribution.
88
+ logprobs=5,
89
+ )
90
+ outputs = llm.generate([prompt], sampling_params=sampling_params)
91
+ result = outputs[0].outputs[0]
92
+ replayed_ids = list(result.token_ids)
93
+
94
+ print("\n[Trace-replay]")
95
+ print(f" Requested decode token IDs : {decode_token_ids}")
96
+ print(f" Replayed output token IDs : {replayed_ids}")
97
+ print(f" Replayed output text : {result.text!r}")
98
+
99
+ # Verify the replayed tokens match exactly.
100
+ assert replayed_ids == decode_token_ids, (
101
+ f"Mismatch!\n expected: {decode_token_ids}\n got: {replayed_ids}"
102
+ )
103
+ print(" Replayed tokens match the requested trace exactly.")
104
+
105
+ # Show per-token logprobs (computed from the real distribution).
106
+ if result.logprobs:
107
+ print("\n Per-token logprobs (trace token):")
108
+ for step, (token_id, logprob_dict) in enumerate(
109
+ zip(replayed_ids, result.logprobs)
110
+ ):
111
+ sampled_lp = logprob_dict.get(token_id)
112
+ lp_value = f"{sampled_lp.logprob:.4f}" if sampled_lp is not None else "n/a"
113
+ rank = sampled_lp.rank if sampled_lp is not None else "n/a"
114
+ print(
115
+ f" step {step:2d}: token_id={token_id:6d} "
116
+ f"logprob={lp_value} rank={rank}"
117
+ )
118
+
119
+
120
+def run_demo(args: argparse.Namespace) -> None:
121
+ print(f"Loading model: {args.model}\n")
122
+ llm = build_llm(args)
123
+ prompt = args.prompt
124
+
125
+ print("=" * 60)
126
+ print("Step 1 — Normal greedy generation (captures decode tokens)")
127
+ print("=" * 60)
128
+ decode_token_ids = run_normal_generation(llm, prompt, max_tokens=8)
129
+
130
+ print("\n" + "=" * 60)
131
+ print("Step 2 — Trace-replay with the captured tokens")
132
+ print("=" * 60)
133
+ run_trace_replay(llm, prompt, decode_token_ids)
134
+
135
+
136
+def parse_args() -> argparse.Namespace:
137
+ parser = argparse.ArgumentParser(
138
+ description="Trace-replay with vLLM offline inference"
139
+ )
140
+ parser.add_argument(
141
+ "--prompt",
142
+ type=str,
143
+ default=DEFAULT_PROMPT,
144
+ help="Text prompt to use for generation (default: %(default)r)",
145
+ )
146
+ parser.add_argument(
147
+ "--model",
148
+ type=str,
149
+ default="facebook/opt-125m",
150
+ help="Name or path of the HuggingFace model to use",
151
+ )
152
+ parser.add_argument(
153
+ "--trust-remote-code",
154
+ action="store_true",
155
+ help="Trust remote code from HuggingFace",
156
+ )
157
+ parser.add_argument(
158
+ "--tensor-parallel-size",
159
+ type=int,
160
+ default=1,
161
+ help="Number of tensor parallel replicas",
162
+ )
163
+ parser.add_argument(
164
+ "--enforce-eager",
165
+ action="store_true",
166
+ help="Always use eager-mode PyTorch (disable CUDA graph)",
167
+ )
168
+ parser.add_argument(
169
+ "--gpu-memory-utilization",
170
+ type=float,
171
+ default=0.9,
172
+ help="Fraction of GPU memory to use",
173
+ )
174
+ parser.add_argument(
175
+ "--max-model-len",
176
+ type=int,
177
+ default=None,
178
+ help="Model context length",
179
+ )
180
+ parser.add_argument(
181
+ "--max-num-seqs",
182
+ type=int,
183
+ default=256,
184
+ help="Maximum number of sequences per iteration",
185
+ )
186
+ return parser.parse_args()
187
+
188
+
189
+def main() -> None:
190
+ args = parse_args()
191
+ run_demo(args)
192
+
193
+
194
+if __name__ == "__main__":
195
+ main()
tests/v1/engine/test_input_processor_trace_replay.py
new
+110
@@ -0,0 +1,110 @@
1
+# SPDX-License-Identifier: Apache-2.0
2
+# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3
+
4
+import time
5
+from types import SimpleNamespace
6
+from unittest.mock import patch
7
+
8
+import pytest
9
+
10
+from vllm import SamplingParams
11
+from vllm.config import VllmConfig
12
+from vllm.exceptions import VLLMValidationError
13
+from vllm.v1.core.sched.utils import check_stop
14
+from vllm.v1.engine.input_processor import InputProcessor
15
+from vllm.v1.request import Request, RequestStatus
16
+
17
+pytestmark = pytest.mark.skip_global_cleanup
18
+
19
+
20
+def _make_request(params: SamplingParams) -> Request:
21
+ return Request(
22
+ request_id="req-0",
23
+ client_index=0,
24
+ prompt_token_ids=[1, 2, 3],
25
+ sampling_params=params,
26
+ pooling_params=None,
27
+ arrival_time=time.time(),
28
+ )
29
+
30
+
31
+def _normalize(params: SamplingParams, prompt_len: int, max_model_len: int) -> None:
32
+ processor = SimpleNamespace(
33
+ model_config=SimpleNamespace(max_model_len=max_model_len)
34
+ )
35
+ InputProcessor._normalize_trace_replay_params(processor, params, prompt_len)
36
+
37
+
38
+def test_normalize_trace_replay_params():
39
+ params = SamplingParams(
40
+ max_tokens=16,
41
+ min_tokens=10,
42
+ stop_token_ids=[20],
43
+ trace_decode_token_ids=[10, 20, 30],
44
+ )
45
+ params.update_from_generation_config({}, eos_token_id=20)
46
+
47
+ _normalize(params, prompt_len=3, max_model_len=128)
48
+
49
+ assert params.max_tokens == 3
50
+ assert params.min_tokens == 0
51
+ assert params.ignore_eos is True
52
+ assert params.eos_token_id is None
53
+ assert params.stop_token_ids == []
54
+ assert params.all_stop_token_ids == set()
55
+
56
+ request = _make_request(params)
57
+ assert request.max_tokens == 3
58
+ request.append_output_token_ids([10, 20, 30])
59
+ assert check_stop(request, max_model_len=128)
60
+ assert request.status == RequestStatus.FINISHED_LENGTH_CAPPED
61
+
62
+
63
+def test_trace_longer_than_remaining_context_is_truncated():
64
+ """The trace is staged into a max_model_len-wide row, so it must be cut.
65
+
66
+ An explicitly set max_tokens is never clamped to max_model_len, so it cannot
67
+ bound the staged write.
68
+ """
69
+ params = SamplingParams(max_tokens=100, trace_decode_token_ids=list(range(20)))
70
+
71
+ _normalize(params, prompt_len=6, max_model_len=10)
72
+
73
+ assert params.trace_decode_token_ids == [0, 1, 2, 3]
74
+ assert params.max_tokens == 4
75
+
76
+
77
+def _validate(enable_trace_replay: bool) -> None:
78
+ """Run _validate_params' trace gating with the rest of verify() stubbed."""
79
+ processor = SimpleNamespace(
80
+ model_config=SimpleNamespace(
81
+ return_sampling_mask=False,
82
+ enable_trace_replay=enable_trace_replay,
83
+ ),
84
+ vllm_config=SimpleNamespace(reasoning_config=None),
85
+ speculative_config=None,
86
+ structured_outputs_config=None,
87
+ tokenizer=None,
88
+ )
89
+ params = SamplingParams(trace_decode_token_ids=[1, 2, 3])
90
+ with patch.object(SamplingParams, "verify"):
91
+ InputProcessor._validate_params(processor, params, ("generate",))
92
+
93
+
94
+def test_trace_request_rejected_when_feature_disabled():
95
+ with pytest.raises(VLLMValidationError, match="--enable-trace-replay"):
96
+ _validate(enable_trace_replay=False)
97
+
98
+
99
+def test_trace_request_accepted_when_feature_enabled():
100
+ _validate(enable_trace_replay=True)
101
+
102
+
103
+def test_trace_replay_requires_v2_model_runner():
104
+ config = SimpleNamespace(
105
+ model_config=SimpleNamespace(enable_trace_replay=True),
106
+ use_v2_model_runner=False,
107
+ )
108
+
109
+ with pytest.raises(ValueError, match="trace replay requires Model Runner V2"):
110
+ VllmConfig._verify_trace_replay_config(config)
tests/v1/sample/test_trace_replay_params.py
new
+111
@@ -0,0 +1,111 @@
1
+# SPDX-License-Identifier: Apache-2.0
2
+# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3
+
4
+import pytest
5
+
6
+from vllm import SamplingParams
7
+from vllm.exceptions import VLLMValidationError
8
+from vllm.sampling_params import StructuredOutputsParams
9
+
10
+pytestmark = pytest.mark.skip_global_cleanup
11
+
12
+# ---------------------------------------------------------------------------
13
+# SamplingParams field
14
+# ---------------------------------------------------------------------------
15
+
16
+
17
+def test_sampling_params_trace_field_defaults_to_none():
18
+ params = SamplingParams(max_tokens=10)
19
+ assert params.trace_decode_token_ids is None
20
+
21
+
22
+def test_sampling_params_trace_field_accepts_list():
23
+ ids = [100, 200, 300]
24
+ params = SamplingParams(trace_decode_token_ids=ids)
25
+ assert params.trace_decode_token_ids == ids
26
+
27
+
28
+def test_sampling_params_trace_field_preserved_by_clone():
29
+ ids = [1, 2, 3]
30
+ params = SamplingParams(trace_decode_token_ids=ids)
31
+ cloned = params.clone()
32
+ assert cloned.trace_decode_token_ids == ids
33
+ assert cloned.trace_decode_token_ids is not params.trace_decode_token_ids
34
+
35
+
36
+def test_sampling_params_trace_field_rejects_empty_list():
37
+ params = SamplingParams(trace_decode_token_ids=[])
38
+ with pytest.raises(ValueError, match="non-empty"):
39
+ params._validate_trace_replay(
40
+ _make_model_config(vocab_size=100), speculative_config=None
41
+ )
42
+
43
+
44
+def test_sampling_params_trace_field_requires_single_output():
45
+ params = SamplingParams(n=2, trace_decode_token_ids=[1])
46
+ with pytest.raises(ValueError, match="requires n=1"):
47
+ params._validate_trace_replay(
48
+ _make_model_config(vocab_size=100), speculative_config=None
49
+ )
50
+
51
+
52
+@pytest.mark.parametrize("invalid_ids", [[-1, 5], [1, "2"]])
53
+def test_sampling_params_trace_field_rejects_invalid_token_ids(invalid_ids):
54
+ params = SamplingParams(trace_decode_token_ids=invalid_ids)
55
+ with pytest.raises(ValueError, match="non-negative integers"):
56
+ params._validate_trace_replay(
57
+ _make_model_config(vocab_size=100), speculative_config=None
58
+ )
59
+
60
+
61
+def _make_model_config(vocab_size: int):
62
+ from unittest.mock import Mock
63
+
64
+ model_config = Mock()
65
+ model_config.get_vocab_size = lambda: vocab_size
66
+ return model_config
67
+
68
+
69
+def test_validate_trace_replay_accepts_in_vocab():
70
+ params = SamplingParams(trace_decode_token_ids=[0, 50, 99])
71
+ # Should not raise.
72
+ params._validate_trace_replay(
73
+ _make_model_config(vocab_size=100), speculative_config=None
74
+ )
75
+
76
+
77
+def test_validate_trace_replay_rejects_out_of_vocab():
78
+ # The non-negative check passes at construction, but the token id exceeds
79
+ # the vocabulary; verify() must reject it before it reaches the sampler.
80
+ params = SamplingParams(trace_decode_token_ids=[0, 100])
81
+ with pytest.raises(VLLMValidationError, match="out-of-vocab"):
82
+ params._validate_trace_replay(
83
+ _make_model_config(vocab_size=100), speculative_config=None
84
+ )
85
+
86
+
87
+def test_validate_trace_replay_noop_when_unset():
88
+ params = SamplingParams(max_tokens=4)
89
+ # Should not raise when the field is unset.
90
+ params._validate_trace_replay(
91
+ _make_model_config(vocab_size=100), speculative_config=None
92
+ )
93
+
94
+
95
+def test_trace_decode_token_ids_rejects_speculative_decoding():
96
+ params = SamplingParams(trace_decode_token_ids=[1])
97
+ with pytest.raises(ValueError, match="not supported with speculative decoding"):
98
+ params._validate_trace_replay(
99
+ _make_model_config(vocab_size=100), speculative_config=object()
100
+ )
101
+
102
+
103
+def test_trace_decode_token_ids_rejects_structured_outputs():
104
+ params = SamplingParams(
105
+ trace_decode_token_ids=[1],
106
+ structured_outputs=StructuredOutputsParams(json_object=True),
107
+ )
108
+ with pytest.raises(ValueError, match="not supported with structured outputs"):
109
+ params._validate_trace_replay(
110
+ _make_model_config(vocab_size=100), speculative_config=None
111
+ )
tests/v1/worker/test_gpu_trace_replay.py
new
+195
@@ -0,0 +1,195 @@
1
+# SPDX-License-Identifier: Apache-2.0
2
+# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3
+"""Tests for Model Runner V2 inference trace-replay.
4
+
5
+Trace-replay forces the sampler to emit a predetermined sequence of decode
6
+token IDs. The replay step for a request is derived from GPU state as
7
+``total_len - prompt_len`` (output tokens produced so far), and the trace token
8
+overwrites the sampled token in place before logprobs are computed.
9
+"""
10
+
11
+from types import SimpleNamespace
12
+from typing import cast
13
+
14
+import pytest
15
+import torch
16
+
17
+pytest.importorskip("triton")
18
+if not torch.cuda.is_available():
19
+ pytest.skip("CUDA required for trace-replay tests", allow_module_level=True)
20
+
21
+from vllm.sampling_params import SamplingParams
22
+from vllm.v1.worker.gpu.sample.trace_replay import (
23
+ TraceReplayState,
24
+ apply_trace_tokens,
25
+)
26
+from vllm.v1.worker.gpu.states import RequestState
27
+
28
+DEVICE = "cuda"
29
+TEST_MAX_MODEL_LEN = 2048
30
+
31
+
32
+def _i32(x) -> torch.Tensor:
33
+ return torch.tensor(x, dtype=torch.int32, device=DEVICE)
34
+
35
+
36
+def _i64(x) -> torch.Tensor:
37
+ return torch.tensor(x, dtype=torch.int64, device=DEVICE)
38
+
39
+
40
+def _trace_state(max_num_reqs: int) -> TraceReplayState:
41
+ """Build a state whose req_states exposes the buffers apply_trace reads.
42
+
43
+ total_len/prompt_len are written by the caller to position each request at
44
+ the desired replay step.
45
+ """
46
+ req_states = cast(
47
+ RequestState,
48
+ SimpleNamespace(
49
+ max_num_reqs=max_num_reqs,
50
+ max_model_len=TEST_MAX_MODEL_LEN,
51
+ device=torch.device(DEVICE),
52
+ total_len=SimpleNamespace(
53
+ gpu=torch.zeros(max_num_reqs, dtype=torch.int32, device=DEVICE)
54
+ ),
55
+ prompt_len=SimpleNamespace(
56
+ gpu=torch.zeros(max_num_reqs, dtype=torch.int32, device=DEVICE)
57
+ ),
58
+ ),
59
+ )
60
+ return TraceReplayState(req_states)
61
+
62
+
63
+def _set_lens(state: TraceReplayState, total_len, prompt_len) -> None:
64
+ """Position each request at a replay step via total_len - prompt_len."""
65
+ state.req_states.total_len.gpu[: len(total_len)] = _i32(total_len)
66
+ state.req_states.prompt_len.gpu[: len(prompt_len)] = _i32(prompt_len)
67
+
68
+
69
+# ------------------------------ Kernel ------------------------------------
70
+
71
+
72
+def test_replay_overwrites_sampled_at_each_step():
73
+ """The trace token for the current step replaces the sampled token."""
74
+ trace = [[100, 101, 102], [200, 201, 202]]
75
+ trace_len = _i32([3, 3])
76
+ trace_token_ids = torch.zeros(
77
+ 2, TEST_MAX_MODEL_LEN, dtype=torch.int32, device=DEVICE
78
+ )
79
+ for i, t in enumerate(trace):
80
+ trace_token_ids[i, : len(t)] = _i32(t)
81
+ prompt_len = _i32([5, 8])
82
+ idx_mapping = _i32([0, 1])
83
+
84
+ for step in range(3):
85
+ sampled = _i64([-7, -7]) # sentinel that must be overwritten
86
+ total_len = _i32([5 + step, 8 + step])
87
+ apply_trace_tokens(
88
+ sampled, idx_mapping, trace_token_ids, trace_len, total_len, prompt_len
89
+ )
90
+ assert sampled.tolist() == [trace[0][step], trace[1][step]]
91
+
92
+
93
+def test_past_end_of_trace_leaves_sampled_untouched():
94
+ """Once step >= trace_len, the sampler's own token is kept."""
95
+ trace_token_ids = torch.zeros(
96
+ 1, TEST_MAX_MODEL_LEN, dtype=torch.int32, device=DEVICE
97
+ )
98
+ trace_token_ids[0, :2] = _i32([100, 101])
99
+ trace_len = _i32([2])
100
+ prompt_len = _i32([4])
101
+ idx_mapping = _i32([0])
102
+
103
+ sampled = _i64([999])
104
+ total_len = _i32([4 + 2]) # step == 2 == trace_len -> out of range
105
+ apply_trace_tokens(
106
+ sampled, idx_mapping, trace_token_ids, trace_len, total_len, prompt_len
107
+ )
108
+ assert sampled.tolist() == [999]
109
+
110
+
111
+def test_non_trace_request_untouched():
112
+ """trace_len == 0 means the request never uses replay."""
113
+ trace_token_ids = torch.zeros(
114
+ 1, TEST_MAX_MODEL_LEN, dtype=torch.int32, device=DEVICE
115
+ )
116
+ trace_len = _i32([0])
117
+ prompt_len = _i32([3])
118
+ idx_mapping = _i32([0])
119
+
120
+ sampled = _i64([42])
121
+ total_len = _i32([3])
122
+ apply_trace_tokens(
123
+ sampled, idx_mapping, trace_token_ids, trace_len, total_len, prompt_len
124
+ )
125
+ assert sampled.tolist() == [42]
126
+
127
+
128
+def test_idx_mapping_indirection_and_negative_skip():
129
+ """batch_idx -> req_state_idx indirection, and negative entries are skipped."""
130
+ # req_state 0: no trace; req_state 1: trace [500, 501].
131
+ trace_token_ids = torch.zeros(
132
+ 2, TEST_MAX_MODEL_LEN, dtype=torch.int32, device=DEVICE
133
+ )
134
+ trace_token_ids[1, :2] = _i32([500, 501])
135
+ trace_len = _i32([0, 2])
136
+ prompt_len = _i32([10, 6])
137
+
138
+ # batch: [maps to state 1, masked (-1), maps to state 0].
139
+ idx_mapping = _i32([1, -1, 0])
140
+ sampled = _i64([1, 2, 3])
141
+ # Indexed by req_state_idx: state 0 at total_len=10, state 1 at step 1.
142
+ total_len = _i32([10, 6 + 1])
143
+ apply_trace_tokens(
144
+ sampled, idx_mapping, trace_token_ids, trace_len, total_len, prompt_len
145
+ )
146
+ # batch 0 -> state 1 step 1 -> 501; batch 1 masked; batch 2 -> state 0 no trace.
147
+ assert sampled.tolist() == [501, 2, 3]
148
+
149
+
150
+# --------------------------- TraceReplayState -----------------------------
151
+
152
+
153
+def test_state_end_to_end():
154
+ """add_request -> apply_staged_writes -> apply_trace overwrites correctly."""
155
+ state = _trace_state(4)
156
+ state.add_request(0, SamplingParams(trace_decode_token_ids=[11, 22, 33]))
157
+ state.add_request(1, SamplingParams()) # no trace
158
+ state.apply_staged_writes()
159
+
160
+ idx_mapping = _i32([0, 1])
161
+ sampled = _i64([-1, -1])
162
+ _set_lens(state, [7 + 1, 7, 0, 0], [7, 7, 0, 0]) # req 0 at step 1
163
+ state.apply_trace(sampled, idx_mapping)
164
+ assert sampled.tolist() == [22, -1]
165
+
166
+
167
+def test_state_leaves_non_trace_batch_unchanged():
168
+ """Requests with trace_len == 0 remain unchanged after a trace was seen."""
169
+ state = _trace_state(4)
170
+ state.add_request(0, SamplingParams(trace_decode_token_ids=[11, 22]))
171
+ state.add_request(1, SamplingParams())
172
+ state.apply_staged_writes()
173
+
174
+ # Only the non-trace request (state 1) is in this batch.
175
+ idx_mapping = _i32([1])
176
+ sampled = _i64([555])
177
+ _set_lens(state, [0, 7, 0, 0], [7, 7, 0, 0])
178
+ state.apply_trace(sampled, idx_mapping)
179
+ assert sampled.tolist() == [555]
180
+
181
+
182
+def test_slot_reuse_clears_trace():
183
+ """Reusing a slot for a non-trace request must not replay stale tokens."""
184
+ state = _trace_state(2)
185
+ state.add_request(0, SamplingParams(trace_decode_token_ids=[11, 22]))
186
+ state.apply_staged_writes()
187
+ # Slot 0 reused by a request without a trace.
188
+ state.add_request(0, SamplingParams())
189
+ state.apply_staged_writes()
190
+
191
+ idx_mapping = _i32([0])
192
+ sampled = _i64([888])
193
+ _set_lens(state, [3, 0], [3, 0])
194
+ state.apply_trace(sampled, idx_mapping)
195
+ assert sampled.tolist() == [888]
vllm/config/model.py
+7
@@ -262,6 +262,12 @@ class ModelConfig:
262
equivalent exponential-race sampling. FP64 preserves lower-tail sampling
263
events that fp32 uniform/exponential draws can truncate, at the cost of
264
significantly lower throughput on most GPUs."""
265
+ enable_trace_replay: bool = False
266
+ """Whether to allow requests to set
267
+ `SamplingParams.trace_decode_token_ids`, which forces decoding to follow a
268
+ predetermined token sequence while still computing real logprobs. Reserved
269
+ for debugging and RL workflows: enabling it reserves a per-request trace
270
+ buffer, so it is off by default."""
271
disable_sliding_window: bool = False
272
"""Whether to disable sliding window. If True, we will disable the sliding
273
window functionality of the model, capping to sliding window size. If the
@@ -424,6 +430,7 @@ class ModelConfig:
430
"return_sampling_mask",
431
"logprobs_mode",
432
"use_fp64_gumbel",
433
+ "enable_trace_replay",
434
"disable_cascade_attn",
435
"skip_tokenizer_init",
436
"served_model_name",
vllm/config/vllm.py
+8
@@ -1091,6 +1091,13 @@ class VllmConfig:
1091
"are normalized over the same nucleus as the sampling mask"
1092
)
1093
1094
+ def _verify_trace_replay_config(self) -> None:
1095
+ model_config = self.model_config
1096
+ if model_config is None or not model_config.enable_trace_replay:
1097
+ return
1098
+ if not self.use_v2_model_runner:
1099
+ raise ValueError("trace replay requires Model Runner V2")
1100
+
1101
def __post_init__(self):
1102
"""Verify configs are valid & consistent with each other."""
1103
@@ -1141,6 +1148,7 @@ class VllmConfig:
1148
)
1149
1150
self._verify_sampling_replay_config()
1151
+ self._verify_trace_replay_config()
1152
1153
if self.lora_config is not None:
1154
self.lora_config.verify_with_model_config(self.model_config)
vllm/engine/arg_utils.py
+5
@@ -543,6 +543,7 @@ class EngineArgs:
543
max_logprobs: int = ModelConfig.max_logprobs
544
logprobs_mode: LogprobsMode = ModelConfig.logprobs_mode
545
use_fp64_gumbel: bool = ModelConfig.use_fp64_gumbel
546
+ enable_trace_replay: bool = ModelConfig.enable_trace_replay
547
disable_log_stats: bool = False
548
aggregate_engine_logging: bool = False
549
revision: str | None = ModelConfig.revision
@@ -884,6 +885,9 @@ class EngineArgs:
885
model_group.add_argument("--max-logprobs", **model_kwargs["max_logprobs"])
886
model_group.add_argument("--logprobs-mode", **model_kwargs["logprobs_mode"])
887
model_group.add_argument("--use-fp64-gumbel", **model_kwargs["use_fp64_gumbel"])
888
+ model_group.add_argument(
889
+ "--enable-trace-replay", **model_kwargs["enable_trace_replay"]
890
+ )
891
model_group.add_argument(
892
"--disable-sliding-window", **model_kwargs["disable_sliding_window"]
893
)
@@ -1760,6 +1764,7 @@ class EngineArgs:
1764
max_logprobs=self.max_logprobs,
1765
logprobs_mode=self.logprobs_mode,
1766
use_fp64_gumbel=self.use_fp64_gumbel,
1767
+ enable_trace_replay=self.enable_trace_replay,
1768
disable_sliding_window=self.disable_sliding_window,
1769
disable_cascade_attn=self.disable_cascade_attn,
1770
skip_tokenizer_init=self.skip_tokenizer_init,
vllm/sampling_params.py
+72
-8
@@ -346,14 +346,6 @@ class SamplingParams(
346
"""Arbitrary additional args, that can be used by custom sampling
347
implementations, plugins, etc. Not used by any in-tree sampling
348
implementations."""
349
- routed_experts_prompt_start: int = 0
350
- """When enable_return_routed_experts is active, skip the first
351
- routed_experts_prompt_start prompt tokens from the returned routing
352
- data. In multi-turn agent scenarios, set this to the length of the
353
- already-returned prefix to avoid duplicating routing for prompt tokens
354
- covered by earlier turns. Default 0 returns routing for all prompt
355
- tokens."""
356
-
349
# Fields used for bad words
350
bad_words: list[str] | None = None
351
"""Words that are not allowed to be generated. More precisely, only the
@@ -373,6 +365,19 @@ class SamplingParams(
365
'\\emoji \\emoji \\emoji ...'). This feature can detect such behavior
366
and terminate early, saving time and tokens."""
367
368
+ # Debugging / RL-specific parameters. Not intended for production serving.
369
+ routed_experts_prompt_start: int = 0
370
+ """When enable_return_routed_experts is active, skip the first
371
+ routed_experts_prompt_start prompt tokens from the returned routing
372
+ data. In multi-turn agent scenarios, set this to the length of the
373
+ already-returned prefix to avoid duplicating routing for prompt tokens
374
+ covered by earlier turns. Default 0 returns routing for all prompt
375
+ tokens."""
376
+ trace_decode_token_ids: list[int] | None = None
377
+ """If provided, forces the engine to emit this predetermined sequence of
378
+ token IDs during decoding instead of sampling randomly. Real logprobs are
379
+ still computed. Conflict checking is performed at the engine level."""
380
+
381
@staticmethod
382
def from_optional(
383
n: int | None = 1,
@@ -407,6 +412,8 @@ class SamplingParams(
412
repetition_detection: RepetitionDetectionParams | None = None,
413
logprob_token_ids: list[int] | None = None,
414
routed_experts_prompt_start: int = 0,
415
+ # Debugging / RL-specific parameters.
416
+ trace_decode_token_ids: list[int] | None = None,
417
) -> "SamplingParams":
418
if logit_bias is not None:
419
# Fast path uses a dict comprehension; on failure we iterate once
@@ -470,6 +477,7 @@ class SamplingParams(
477
skip_clone=skip_clone,
478
repetition_detection=repetition_detection,
479
routed_experts_prompt_start=routed_experts_prompt_start,
480
+ trace_decode_token_ids=trace_decode_token_ids,
481
)
482
483
def __post_init__(self) -> None:
@@ -780,6 +788,7 @@ class SamplingParams(
788
) -> None:
789
self._validate_logprobs(model_config)
790
self._validate_logit_bias(model_config)
791
+ self._validate_trace_replay(model_config, speculative_config)
792
self._validate_logits_processors(model_config)
793
self._validate_allowed_token_ids(tokenizer)
794
self._validate_spec_decode(speculative_config)
@@ -870,6 +879,61 @@ class SamplingParams(
879
value=invalid_token_ids,
880
)
881
882
+ def _validate_trace_replay(
883
+ self,
884
+ model_config: ModelConfig,
885
+ speculative_config: SpeculativeConfig | None,
886
+ ) -> None:
887
+ """Validate trace replay request compatibility."""
888
+ if self.trace_decode_token_ids is None:
889
+ return
890
+
891
+ if len(self.trace_decode_token_ids) == 0:
892
+ raise ValueError("trace_decode_token_ids must be a non-empty list.")
893
+ if self.n != 1:
894
+ raise ValueError("trace_decode_token_ids requires n=1.")
895
+ if not all(isinstance(t, int) and t >= 0 for t in self.trace_decode_token_ids):
896
+ raise ValueError(
897
+ "trace_decode_token_ids must contain non-negative integers."
898
+ )
899
+
900
+ if self.prompt_logprobs is not None:
901
+ raise ValueError(
902
+ "trace_decode_token_ids is not supported with prompt_logprobs."
903
+ )
904
+ if speculative_config is not None:
905
+ raise ValueError(
906
+ "trace_decode_token_ids is not supported with speculative decoding."
907
+ )
908
+ if self.structured_outputs is not None:
909
+ raise ValueError(
910
+ "trace_decode_token_ids is not supported with structured outputs."
911
+ )
912
+ if self.repetition_detection is not None:
913
+ raise ValueError(
914
+ "trace_decode_token_ids is not supported with repetition_detection."
915
+ )
916
+ if self.thinking_token_budget is not None:
917
+ raise ValueError(
918
+ "trace_decode_token_ids is not supported with thinking_token_budget."
919
+ )
920
+ if self.bad_words:
921
+ raise ValueError("trace_decode_token_ids is not supported with bad_words.")
922
+
923
+ vocab_size = model_config.get_vocab_size()
924
+ invalid_token_ids = [
925
+ token_id
926
+ for token_id in self.trace_decode_token_ids
927
+ if token_id < 0 or token_id >= vocab_size
928
+ ]
929
+ if invalid_token_ids:
930
+ raise VLLMValidationError(
931
+ f"token_id(s) {invalid_token_ids} in trace_decode_token_ids "
932
+ f"contain out-of-vocab token ids. Vocabulary size: {vocab_size}",
933
+ parameter="trace_decode_token_ids",
934
+ value=invalid_token_ids,
935
+ )
936
+
937
def _validate_logits_processors(self, model_config: ModelConfig) -> None:
938
from vllm.v1.sample.logits_processor import (
939
validate_logits_processors_parameters,
vllm/v1/engine/input_processor.py
+40
-1
@@ -51,7 +51,6 @@ class InputProcessor:
51
self.speculative_config = vllm_config.speculative_config
52
self.structured_outputs_config = vllm_config.structured_outputs_config
53
self.observability_config = vllm_config.observability_config
54
- self.use_v2_model_runner = vllm_config.use_v2_model_runner
54
55
self.generation_config_fields = model_config.try_get_generation_config()
56
@@ -128,6 +127,15 @@ class InputProcessor:
127
"not configured. Please set --reasoning-parser "
128
"and/or --reasoning-config to use thinking_token_budget."
129
)
130
+ if (
131
+ params.trace_decode_token_ids
132
+ and not self.model_config.enable_trace_replay
133
+ ):
134
+ raise VLLMValidationError(
135
+ "trace_decode_token_ids is set but trace replay is not "
136
+ "enabled. Start the engine with --enable-trace-replay "
137
+ "to use it."
138
+ )
139
elif isinstance(params, PoolingParams):
140
supported_pooling_tasks = [
141
task for task in supported_tasks if task in POOLING_TASKS
@@ -156,6 +164,30 @@ class InputProcessor:
164
f"but got {type(params).__name__}"
165
)
166
167
+ def _normalize_trace_replay_params(
168
+ self, sampling_params: SamplingParams, prompt_len: int
169
+ ) -> None:
170
+ """Apply trace replay's generation semantics to request-local params."""
171
+ trace_token_ids = sampling_params.trace_decode_token_ids
172
+ assert trace_token_ids
173
+ assert sampling_params.max_tokens is not None
174
+
175
+ max_trace_len = max(self.model_config.max_model_len - prompt_len, 1)
176
+ trace_token_ids = trace_token_ids[:max_trace_len]
177
+ sampling_params.trace_decode_token_ids = trace_token_ids
178
+
179
+ # Apply this after the generation config so its EOS token cannot stop
180
+ # replay before the trace is exhausted.
181
+ sampling_params.max_tokens = min(
182
+ len(trace_token_ids), sampling_params.max_tokens
183
+ )
184
+ sampling_params.min_tokens = 0
185
+ sampling_params.ignore_eos = True
186
+ sampling_params._eos_token_id = None
187
+ sampling_params.stop = []
188
+ sampling_params.stop_token_ids = []
189
+ sampling_params._all_stop_token_ids = set()
190
+
191
def _validate_lora(self, lora_request: LoRARequest | None) -> None:
192
if lora_request is None:
193
return
@@ -340,6 +372,13 @@ class InputProcessor:
372
)
373
if self.tokenizer is not None:
374
sampling_params.update_from_tokenizer(self.tokenizer)
375
+ if sampling_params.trace_decode_token_ids:
376
+ self._normalize_trace_replay_params(
377
+ sampling_params,
378
+ length_from_prompt_token_ids_or_embeds(
379
+ prompt_token_ids, prompt_embeds
380
+ ),
381
+ )
382
else:
383
pooling_params = params.clone()
384
vllm/v1/worker/gpu/model_runner.py
+1
@@ -408,6 +408,7 @@ class GPUModelRunner(LoRAModelRunnerMixin):
408
logprobs_mode=self.model_config.logprobs_mode,
409
num_speculative_tokens=self.decode_query_len,
410
use_fp64_gumbel=self.model_config.use_fp64_gumbel,
411
+ enable_trace_replay=self.model_config.enable_trace_replay,
412
reasoning_config=self.vllm_config.reasoning_config,
413
return_sampling_mask=self.model_config.return_sampling_mask,
414
)
vllm/v1/worker/gpu/sample/sampler.py
+14
@@ -26,6 +26,7 @@ from vllm.v1.worker.gpu.sample.output import SamplerOutput, SamplingMaskTensors
26
from vllm.v1.worker.gpu.sample.penalties import PenaltiesState
27
from vllm.v1.worker.gpu.sample.states import NO_LOGPROBS, SamplingStates
28
from vllm.v1.worker.gpu.sample.thinking_budget import ThinkingBudgetState
29
+from vllm.v1.worker.gpu.sample.trace_replay import TraceReplayState
30
from vllm.v1.worker.gpu.states import RequestState
31
32
@@ -39,6 +40,7 @@ class Sampler:
40
logprobs_mode: LogprobsMode = "raw_logprobs",
41
num_speculative_tokens: int = 1,
42
use_fp64_gumbel: bool = False,
43
+ enable_trace_replay: bool = False,
44
reasoning_config: ReasoningConfig | None = None,
45
return_sampling_mask: bool = False,
46
):
@@ -53,6 +55,9 @@ class Sampler:
55
self.bad_words_state = BadWordsState(req_states)
56
self.logprob_token_ids_state = LogprobTokenIdsState(max_num_reqs, device)
57
self.thinking_budget_state = ThinkingBudgetState(req_states, reasoning_config)
58
+ self.trace_replay_state = (
59
+ TraceReplayState(req_states) if enable_trace_replay else None
60
+ )
61
self.needs_logits_processing = np.zeros(max_num_reqs, dtype=bool)
62
self.num_speculative_tokens = num_speculative_tokens
63
self.return_sampling_mask = return_sampling_mask
@@ -69,6 +74,8 @@ class Sampler:
74
self.bad_words_state.add_request(req_idx, sampling_params)
75
self.logprob_token_ids_state.add_request(req_idx, sampling_params)
76
self.thinking_budget_state.add_request(req_idx, sampling_params)
77
+ if self.trace_replay_state is not None:
78
+ self.trace_replay_state.add_request(req_idx, sampling_params)
79
80
states = self.sampling_states
81
temperature = states.temperature.np[req_idx]
@@ -93,6 +100,8 @@ class Sampler:
100
self.bad_words_state.apply_staged_writes()
101
self.logprob_token_ids_state.apply_staged_writes()
102
self.thinking_budget_state.apply_staged_writes()
103
+ if self.trace_replay_state is not None:
104
+ self.trace_replay_state.apply_staged_writes()
105
106
def __call__(
107
self,
@@ -128,6 +137,11 @@ class Sampler:
137
return_logprobs=return_logprobs,
138
)
139
140
+ if self.trace_replay_state is not None:
141
+ # Overwrite sampled tokens with the replay trace up-front so that
142
+ # computed logprobs reflect the real distribution of the forced token.
143
+ self.trace_replay_state.apply_trace(sampled, idx_mapping)
144
+
145
if return_logprobs:
146
if self.logprobs_mode in PROCESSED_LOGPROBS_MODES:
147
logits = processed_logits
vllm/v1/worker/gpu/sample/trace_replay.py
new
+113
@@ -0,0 +1,113 @@
1
+# SPDX-License-Identifier: Apache-2.0
2
+# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3
+import torch
4
+
5
+from vllm.sampling_params import SamplingParams
6
+from vllm.triton_utils import tl, triton
7
+from vllm.v1.worker.gpu.buffer_utils import StagedWriteTensor, UvaBackedTensor
8
+from vllm.v1.worker.gpu.states import RequestState
9
+
10
+
11
+class TraceReplayState:
12
+ """Per-request state for inference trace-replay.
13
+
14
+ When a request carries ``SamplingParams.trace_decode_token_ids``, the
15
+ sampler overwrites the sampled token at each decode step with the
16
+ predetermined trace token, while real logprobs and ranks are still computed
17
+ from the unmodified logit distribution. The replay step for a request is
18
+ derived entirely from GPU state (``total_len - prompt_len``), so no CPU
19
+ synchronization or async placeholder handling is needed.
20
+ """
21
+
22
+ def __init__(self, req_states: RequestState):
23
+ self.max_num_reqs = req_states.max_num_reqs
24
+ self.device = req_states.device
25
+ self.req_states = req_states
26
+ self.trace_token_ids = StagedWriteTensor(
27
+ (self.max_num_reqs, req_states.max_model_len),
28
+ dtype=torch.int32,
29
+ device=self.device,
30
+ uva_instead_of_gpu=True,
31
+ )
32
+ self.trace_len = UvaBackedTensor(self.max_num_reqs, dtype=torch.int32)
33
+
34
+ def add_request(self, req_idx: int, sampling_params: SamplingParams) -> None:
35
+ trace = sampling_params.trace_decode_token_ids
36
+ if trace is not None:
37
+ self.trace_len.np[req_idx] = len(trace)
38
+ self.trace_token_ids.stage_write(req_idx, 0, trace)
39
+ else:
40
+ self.trace_len.np[req_idx] = 0
41
+
42
+ def apply_staged_writes(self) -> None:
43
+ self.trace_len.copy_to_uva()
44
+ self.trace_token_ids.apply_write()
45
+
46
+ def apply_trace(
47
+ self,
48
+ sampled: torch.Tensor,
49
+ idx_mapping: torch.Tensor,
50
+ ) -> None:
51
+ apply_trace_tokens(
52
+ sampled,
53
+ idx_mapping,
54
+ self.trace_token_ids.gpu,
55
+ self.trace_len.gpu,
56
+ self.req_states.total_len.gpu,
57
+ self.req_states.prompt_len.gpu,
58
+ )
59
+
60
+
61
+@triton.jit
62
+def _trace_replay_kernel(
63
+ sampled_ptr, # [num_reqs], int64, mutated in place
64
+ idx_mapping_ptr, # [num_reqs] batch_idx -> req_state_idx
65
+ trace_token_ids_ptr, # [max_num_reqs, max_model_len], int32
66
+ trace_token_ids_stride,
67
+ trace_len_ptr, # [max_num_reqs], int32
68
+ total_len_ptr, # [max_num_reqs], int32
69
+ prompt_len_ptr, # [max_num_reqs], int32
70
+):
71
+ batch_idx = tl.program_id(0)
72
+ req_state_idx = tl.load(idx_mapping_ptr + batch_idx)
73
+ if req_state_idx < 0:
74
+ return
75
+
76
+ trace_len = tl.load(trace_len_ptr + req_state_idx)
77
+ if trace_len <= 0:
78
+ return
79
+
80
+ # The token being sampled now is output token number
81
+ # (total_len - prompt_len): total_len reflects tokens committed through the
82
+ # previous step (post_update runs after sampling).
83
+ step = tl.load(total_len_ptr + req_state_idx) - tl.load(
84
+ prompt_len_ptr + req_state_idx
85
+ )
86
+ if step < 0 or step >= trace_len:
87
+ return
88
+
89
+ token_id = tl.load(
90
+ trace_token_ids_ptr + req_state_idx * trace_token_ids_stride + step
91
+ )
92
+ tl.store(sampled_ptr + batch_idx, token_id.to(tl.int64))
93
+
94
+
95
+def apply_trace_tokens(
96
+ sampled: torch.Tensor,
97
+ idx_mapping: torch.Tensor,
98
+ trace_token_ids: torch.Tensor,
99
+ trace_len: torch.Tensor,
100
+ total_len: torch.Tensor,
101
+ prompt_len: torch.Tensor,
102
+) -> None:
103
+ """Overwrite ``sampled`` in place with trace tokens for the current step."""
104
+ num_reqs = idx_mapping.shape[0]
105
+ _trace_replay_kernel[(num_reqs,)](
106
+ sampled,
107
+ idx_mapping,
108
+ trace_token_ids,
109
+ trace_token_ids.stride(0),
110
+ trace_len,
111
+ total_len,
112
+ prompt_len,
113
+ )