renderers 0.2.1.dev8__tar.gz → 0.2.1.dev10__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {renderers-0.2.1.dev8 → renderers-0.2.1.dev10}/PKG-INFO +2 -2
- {renderers-0.2.1.dev8 → renderers-0.2.1.dev10}/pyproject.toml +2 -2
- {renderers-0.2.1.dev8 → renderers-0.2.1.dev10}/renderers/_version.py +2 -2
- {renderers-0.2.1.dev8 → renderers-0.2.1.dev10}/renderers/base.py +1 -1
- {renderers-0.2.1.dev8 → renderers-0.2.1.dev10}/renderers/client.py +159 -47
- {renderers-0.2.1.dev8 → renderers-0.2.1.dev10}/tests/replay.py +4 -1
- {renderers-0.2.1.dev8 → renderers-0.2.1.dev10}/tests/test_client.py +68 -10
- {renderers-0.2.1.dev8 → renderers-0.2.1.dev10}/uv.lock +2 -2
- {renderers-0.2.1.dev8 → renderers-0.2.1.dev10}/.github/workflows/publish-dev.yml +0 -0
- {renderers-0.2.1.dev8 → renderers-0.2.1.dev10}/.github/workflows/publish.yml +0 -0
- {renderers-0.2.1.dev8 → renderers-0.2.1.dev10}/.github/workflows/style.yml +0 -0
- {renderers-0.2.1.dev8 → renderers-0.2.1.dev10}/.github/workflows/test.yml +0 -0
- {renderers-0.2.1.dev8 → renderers-0.2.1.dev10}/.gitignore +0 -0
- {renderers-0.2.1.dev8 → renderers-0.2.1.dev10}/.pre-commit-config.yaml +0 -0
- {renderers-0.2.1.dev8 → renderers-0.2.1.dev10}/LICENSE +0 -0
- {renderers-0.2.1.dev8 → renderers-0.2.1.dev10}/README.md +0 -0
- {renderers-0.2.1.dev8 → renderers-0.2.1.dev10}/docs/renderer-config.md +0 -0
- {renderers-0.2.1.dev8 → renderers-0.2.1.dev10}/examples/README.md +0 -0
- {renderers-0.2.1.dev8 → renderers-0.2.1.dev10}/examples/sglang/multiturn_generate_sglang.py +0 -0
- {renderers-0.2.1.dev8 → renderers-0.2.1.dev10}/examples/sglang/online_multiturn_sglang.py +0 -0
- {renderers-0.2.1.dev8 → renderers-0.2.1.dev10}/examples/tinker/multiturn_generate_tinker.py +0 -0
- {renderers-0.2.1.dev8 → renderers-0.2.1.dev10}/examples/transformers/multiturn_generate_transformers.py +0 -0
- {renderers-0.2.1.dev8 → renderers-0.2.1.dev10}/examples/vllm/multiturn_generate_vllm.py +0 -0
- {renderers-0.2.1.dev8 → renderers-0.2.1.dev10}/renderers/__init__.py +0 -0
- {renderers-0.2.1.dev8 → renderers-0.2.1.dev10}/renderers/configs.py +0 -0
- {renderers-0.2.1.dev8 → renderers-0.2.1.dev10}/renderers/custom.py +0 -0
- {renderers-0.2.1.dev8 → renderers-0.2.1.dev10}/renderers/deepseek_r1.py +0 -0
- {renderers-0.2.1.dev8 → renderers-0.2.1.dev10}/renderers/deepseek_v3.py +0 -0
- {renderers-0.2.1.dev8 → renderers-0.2.1.dev10}/renderers/deepseek_v4.py +0 -0
- {renderers-0.2.1.dev8 → renderers-0.2.1.dev10}/renderers/default.py +0 -0
- {renderers-0.2.1.dev8 → renderers-0.2.1.dev10}/renderers/gemma4.py +0 -0
- {renderers-0.2.1.dev8 → renderers-0.2.1.dev10}/renderers/glm45.py +0 -0
- {renderers-0.2.1.dev8 → renderers-0.2.1.dev10}/renderers/glm5.py +0 -0
- {renderers-0.2.1.dev8 → renderers-0.2.1.dev10}/renderers/gpt_oss.py +0 -0
- {renderers-0.2.1.dev8 → renderers-0.2.1.dev10}/renderers/hy3.py +0 -0
- {renderers-0.2.1.dev8 → renderers-0.2.1.dev10}/renderers/inkling.py +0 -0
- {renderers-0.2.1.dev8 → renderers-0.2.1.dev10}/renderers/kimi_k2.py +0 -0
- {renderers-0.2.1.dev8 → renderers-0.2.1.dev10}/renderers/kimi_k25.py +0 -0
- {renderers-0.2.1.dev8 → renderers-0.2.1.dev10}/renderers/laguna_s21.py +0 -0
- {renderers-0.2.1.dev8 → renderers-0.2.1.dev10}/renderers/laguna_xs2.py +0 -0
- {renderers-0.2.1.dev8 → renderers-0.2.1.dev10}/renderers/llama_3.py +0 -0
- {renderers-0.2.1.dev8 → renderers-0.2.1.dev10}/renderers/minimax_m2.py +0 -0
- {renderers-0.2.1.dev8 → renderers-0.2.1.dev10}/renderers/muse_glimmer.py +0 -0
- {renderers-0.2.1.dev8 → renderers-0.2.1.dev10}/renderers/nemotron3.py +0 -0
- {renderers-0.2.1.dev8 → renderers-0.2.1.dev10}/renderers/parsers.py +0 -0
- {renderers-0.2.1.dev8 → renderers-0.2.1.dev10}/renderers/parsing.py +0 -0
- {renderers-0.2.1.dev8 → renderers-0.2.1.dev10}/renderers/prime_qwen3.py +0 -0
- {renderers-0.2.1.dev8 → renderers-0.2.1.dev10}/renderers/qwen3.py +0 -0
- {renderers-0.2.1.dev8 → renderers-0.2.1.dev10}/renderers/qwen35.py +0 -0
- {renderers-0.2.1.dev8 → renderers-0.2.1.dev10}/renderers/qwen36.py +0 -0
- {renderers-0.2.1.dev8 → renderers-0.2.1.dev10}/renderers/qwen38.py +0 -0
- {renderers-0.2.1.dev8 → renderers-0.2.1.dev10}/renderers/qwen3_vl.py +0 -0
- {renderers-0.2.1.dev8 → renderers-0.2.1.dev10}/renderers/reasoning.py +0 -0
- {renderers-0.2.1.dev8 → renderers-0.2.1.dev10}/renderers/tagged.py +0 -0
- {renderers-0.2.1.dev8 → renderers-0.2.1.dev10}/tests/audit_prefix_stability.py +0 -0
- {renderers-0.2.1.dev8 → renderers-0.2.1.dev10}/tests/conftest.py +0 -0
- {renderers-0.2.1.dev8 → renderers-0.2.1.dev10}/tests/fixtures/prefix_stability.json +0 -0
- {renderers-0.2.1.dev8 → renderers-0.2.1.dev10}/tests/model_assets.py +0 -0
- {renderers-0.2.1.dev8 → renderers-0.2.1.dev10}/tests/parity.py +0 -0
- {renderers-0.2.1.dev8 → renderers-0.2.1.dev10}/tests/reference_continuations.py +0 -0
- {renderers-0.2.1.dev8 → renderers-0.2.1.dev10}/tests/reference_rendering.py +0 -0
- {renderers-0.2.1.dev8 → renderers-0.2.1.dev10}/tests/test_bridge.py +0 -0
- {renderers-0.2.1.dev8 → renderers-0.2.1.dev10}/tests/test_build_helpers.py +0 -0
- {renderers-0.2.1.dev8 → renderers-0.2.1.dev10}/tests/test_custom.py +0 -0
- {renderers-0.2.1.dev8 → renderers-0.2.1.dev10}/tests/test_deepseek_r1.py +0 -0
- {renderers-0.2.1.dev8 → renderers-0.2.1.dev10}/tests/test_deepseek_v4.py +0 -0
- {renderers-0.2.1.dev8 → renderers-0.2.1.dev10}/tests/test_disabled_thinking_stability.py +0 -0
- {renderers-0.2.1.dev8 → renderers-0.2.1.dev10}/tests/test_gemma4.py +0 -0
- {renderers-0.2.1.dev8 → renderers-0.2.1.dev10}/tests/test_glm53_flash.py +0 -0
- {renderers-0.2.1.dev8 → renderers-0.2.1.dev10}/tests/test_glm_tool_name_validation.py +0 -0
- {renderers-0.2.1.dev8 → renderers-0.2.1.dev10}/tests/test_hy3.py +0 -0
- {renderers-0.2.1.dev8 → renderers-0.2.1.dev10}/tests/test_incremental.py +0 -0
- {renderers-0.2.1.dev8 → renderers-0.2.1.dev10}/tests/test_inkling.py +0 -0
- {renderers-0.2.1.dev8 → renderers-0.2.1.dev10}/tests/test_is_content.py +0 -0
- {renderers-0.2.1.dev8 → renderers-0.2.1.dev10}/tests/test_kimi_k25_tool_schema.py +0 -0
- {renderers-0.2.1.dev8 → renderers-0.2.1.dev10}/tests/test_laguna_m1.py +0 -0
- {renderers-0.2.1.dev8 → renderers-0.2.1.dev10}/tests/test_laguna_xs21.py +0 -0
- {renderers-0.2.1.dev8 → renderers-0.2.1.dev10}/tests/test_llama_3.py +0 -0
- {renderers-0.2.1.dev8 → renderers-0.2.1.dev10}/tests/test_load_tokenizer.py +0 -0
- {renderers-0.2.1.dev8 → renderers-0.2.1.dev10}/tests/test_message_indices.py +0 -0
- {renderers-0.2.1.dev8 → renderers-0.2.1.dev10}/tests/test_message_tool_names.py +0 -0
- {renderers-0.2.1.dev8 → renderers-0.2.1.dev10}/tests/test_multimodal.py +0 -0
- {renderers-0.2.1.dev8 → renderers-0.2.1.dev10}/tests/test_muse_glimmer.py +0 -0
- {renderers-0.2.1.dev8 → renderers-0.2.1.dev10}/tests/test_nemotron35_vlm.py +0 -0
- {renderers-0.2.1.dev8 → renderers-0.2.1.dev10}/tests/test_nemotron3_ultra.py +0 -0
- {renderers-0.2.1.dev8 → renderers-0.2.1.dev10}/tests/test_offsetless_tokenizers.py +0 -0
- {renderers-0.2.1.dev8 → renderers-0.2.1.dev10}/tests/test_parity.py +0 -0
- {renderers-0.2.1.dev8 → renderers-0.2.1.dev10}/tests/test_parse_response.py +0 -0
- {renderers-0.2.1.dev8 → renderers-0.2.1.dev10}/tests/test_parse_response_robustness.py +0 -0
- {renderers-0.2.1.dev8 → renderers-0.2.1.dev10}/tests/test_parsers.py +0 -0
- {renderers-0.2.1.dev8 → renderers-0.2.1.dev10}/tests/test_preserve_thinking.py +0 -0
- {renderers-0.2.1.dev8 → renderers-0.2.1.dev10}/tests/test_qwen35_size_coverage.py +0 -0
- {renderers-0.2.1.dev8 → renderers-0.2.1.dev10}/tests/test_qwen38.py +0 -0
- {renderers-0.2.1.dev8 → renderers-0.2.1.dev10}/tests/test_reasoning_boundaries.py +0 -0
- {renderers-0.2.1.dev8 → renderers-0.2.1.dev10}/tests/test_renderer_config.py +0 -0
- {renderers-0.2.1.dev8 → renderers-0.2.1.dev10}/tests/test_roundtrip.py +0 -0
- {renderers-0.2.1.dev8 → renderers-0.2.1.dev10}/tests/test_sampled_mask.py +0 -0
- {renderers-0.2.1.dev8 → renderers-0.2.1.dev10}/tests/test_tagged_rollout.py +0 -0
- {renderers-0.2.1.dev8 → renderers-0.2.1.dev10}/tests/test_tokens_per_message.py +0 -0
- {renderers-0.2.1.dev8 → renderers-0.2.1.dev10}/tests/test_tool_arg_type_preservation.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: renderers
|
|
3
|
-
Version: 0.2.1.
|
|
3
|
+
Version: 0.2.1.dev10
|
|
4
4
|
Summary: Chat template renderers — deterministic message-to-token conversion for LLM training
|
|
5
5
|
License-Expression: Apache-2.0
|
|
6
6
|
License-File: LICENSE
|
|
@@ -12,7 +12,7 @@ Requires-Dist: openai>=1.108.1
|
|
|
12
12
|
Requires-Dist: prime-pydantic-config>=0.3.0.dev83
|
|
13
13
|
Requires-Dist: tiktoken
|
|
14
14
|
Provides-Extra: multimodal
|
|
15
|
-
Requires-Dist: pillow>=
|
|
15
|
+
Requires-Dist: pillow>=13.0.0; extra == 'multimodal'
|
|
16
16
|
Requires-Dist: transformers>=4.50.0; extra == 'multimodal'
|
|
17
17
|
Provides-Extra: transformers
|
|
18
18
|
Requires-Dist: transformers>=4.50.0; extra == 'transformers'
|
|
@@ -52,7 +52,7 @@ transformers = [
|
|
|
52
52
|
# Image/audio renderers also need Pillow to resolve media inputs. Keep this as
|
|
53
53
|
# a separate convenience extra so tokenizer-only users do not pull it in.
|
|
54
54
|
multimodal = [
|
|
55
|
-
"pillow>=
|
|
55
|
+
"pillow>=13.0.0",
|
|
56
56
|
"transformers>=4.50.0",
|
|
57
57
|
]
|
|
58
58
|
|
|
@@ -96,7 +96,7 @@ packages = ["renderers"]
|
|
|
96
96
|
|
|
97
97
|
[dependency-groups]
|
|
98
98
|
dev = [
|
|
99
|
-
"pillow>=
|
|
99
|
+
"pillow>=13.0.0",
|
|
100
100
|
"pre-commit",
|
|
101
101
|
"pytest>=7.0.0",
|
|
102
102
|
"pytest-asyncio>=0.21.0",
|
|
@@ -18,7 +18,7 @@ version_tuple: tuple[int | str, ...]
|
|
|
18
18
|
commit_id: str | None
|
|
19
19
|
__commit_id__: str | None
|
|
20
20
|
|
|
21
|
-
__version__ = version = '0.2.1.
|
|
22
|
-
__version_tuple__ = version_tuple = (0, 2, 1, '
|
|
21
|
+
__version__ = version = '0.2.1.dev10'
|
|
22
|
+
__version_tuple__ = version_tuple = (0, 2, 1, 'dev10')
|
|
23
23
|
|
|
24
24
|
__commit_id__ = commit_id = None
|
|
@@ -2230,7 +2230,7 @@ def build_trajectory_step(
|
|
|
2230
2230
|
"completion_mask": [True] * len(completion_ids),
|
|
2231
2231
|
"completion_logprobs": [0.0] * len(completion_ids),
|
|
2232
2232
|
"routed_experts": None,
|
|
2233
|
-
"
|
|
2233
|
+
"sampler_support": None,
|
|
2234
2234
|
}
|
|
2235
2235
|
if (
|
|
2236
2236
|
full_rendered.multi_modal_data is not None
|
|
@@ -169,9 +169,10 @@ def _parse_packed_completion_logprobs(
|
|
|
169
169
|
return values.astype(np.float32, copy=False)
|
|
170
170
|
|
|
171
171
|
|
|
172
|
-
def
|
|
173
|
-
choice: Mapping[str, Any], completion_ids: list[int]
|
|
174
|
-
) -> tuple[np.ndarray, list[
|
|
172
|
+
def _decode_compact_logprobs(
|
|
173
|
+
choice: Mapping[str, Any], completion_ids: list[int]
|
|
174
|
+
) -> tuple[np.ndarray, np.ndarray, list[int], int]:
|
|
175
|
+
"""Flat ids and logprobs of every compact row, the row offsets and the head size."""
|
|
175
176
|
packed = choice.get("compact_logprobs")
|
|
176
177
|
if not isinstance(packed, Mapping) or packed.get("format") != "u32-f32-le-v1":
|
|
177
178
|
raise MalformedGenerateResponseError("Unsupported compact logprob format")
|
|
@@ -198,13 +199,115 @@ def _parse_compact_logprobs(
|
|
|
198
199
|
values = np.frombuffer(raw, dtype="<f4", count=count, offset=count * 4)
|
|
199
200
|
if not np.isfinite(values).all() or (values > 1e-6).any():
|
|
200
201
|
raise MalformedGenerateResponseError("Invalid compact logprob values")
|
|
202
|
+
return ids, values, offsets, num_top
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
def _sampler_support(
|
|
206
|
+
ids: np.ndarray, counts: np.ndarray, logprobs: np.ndarray | None, replayed: bool
|
|
207
|
+
) -> dict[str, Any]:
|
|
208
|
+
"""The sampler's per-token support in CSR form: completion token ``i`` owns ``counts[i]`` entries of
|
|
209
|
+
the flat ``ids`` (and ``logprobs``, the sampler's logprob of each id). ``replayed`` rows are the exact
|
|
210
|
+
set the token was sampled from; other rows are a head of the sampler's distribution."""
|
|
211
|
+
return {
|
|
212
|
+
"ids": ids.astype(np.int32, copy=False),
|
|
213
|
+
"counts": counts.astype(np.int32, copy=False),
|
|
214
|
+
"logprobs": None
|
|
215
|
+
if logprobs is None
|
|
216
|
+
else logprobs.astype(np.float32, copy=False),
|
|
217
|
+
"replayed": replayed,
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
def _pack_sampler_rows(
|
|
222
|
+
ids: list[list[int]], logprobs: list[list[float]], replayed: bool
|
|
223
|
+
) -> dict[str, Any]:
|
|
224
|
+
return _sampler_support(
|
|
225
|
+
np.fromiter((i for row in ids for i in row), dtype=np.int32),
|
|
226
|
+
np.fromiter((len(row) for row in ids), dtype=np.int32, count=len(ids)),
|
|
227
|
+
np.fromiter((p for row in logprobs for p in row), dtype=np.float32),
|
|
228
|
+
replayed,
|
|
229
|
+
)
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
def _sampled_entries(
|
|
233
|
+
ids: np.ndarray, counts: np.ndarray, completion_ids: list[int]
|
|
234
|
+
) -> np.ndarray:
|
|
235
|
+
"""Which entry of each replayed row is the sampled token. Every row must be non-empty, hold distinct
|
|
236
|
+
ids and contain its sampled token."""
|
|
237
|
+
if (
|
|
238
|
+
len(counts) != len(completion_ids)
|
|
239
|
+
or int(counts.sum()) != len(ids)
|
|
240
|
+
or (counts <= 0).any()
|
|
241
|
+
):
|
|
242
|
+
raise MalformedGenerateResponseError("Sampling mask token count mismatch")
|
|
243
|
+
ids = ids.astype(np.int64)
|
|
244
|
+
rows = np.repeat(np.arange(len(counts)), counts)
|
|
245
|
+
order = np.lexsort((ids, rows))
|
|
246
|
+
sorted_ids, sorted_rows = ids[order], rows[order]
|
|
247
|
+
repeated = (sorted_ids[1:] == sorted_ids[:-1]) & (
|
|
248
|
+
sorted_rows[1:] == sorted_rows[:-1]
|
|
249
|
+
)
|
|
250
|
+
sampled = ids == np.asarray(completion_ids, dtype=np.int64)[rows]
|
|
251
|
+
if repeated.any() or np.count_nonzero(sampled) != len(counts):
|
|
252
|
+
raise MalformedGenerateResponseError("Invalid recorded sampling support")
|
|
253
|
+
return sampled
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
def _parse_replayed_support(
|
|
257
|
+
choice: Mapping[str, Any], completion_ids: list[int]
|
|
258
|
+
) -> tuple[np.ndarray, dict[str, Any]]:
|
|
259
|
+
"""A replayed request's sampled logprobs and support: the recorded sampling mask with the sampler's
|
|
260
|
+
logprob of every kept id, which must normalize over it."""
|
|
261
|
+
ids, values, offsets, _ = _decode_compact_logprobs(choice, completion_ids)
|
|
262
|
+
mask = choice.get("sampling_mask")
|
|
263
|
+
if not isinstance(mask, Mapping):
|
|
264
|
+
raise MalformedGenerateResponseError("Invalid packed sampling mask")
|
|
265
|
+
mask_ids, counts = _decode_packed_sampling_mask(mask)
|
|
266
|
+
if not np.array_equal(counts, np.diff(offsets)) or not np.array_equal(
|
|
267
|
+
mask_ids, ids
|
|
268
|
+
):
|
|
269
|
+
raise MalformedGenerateResponseError(
|
|
270
|
+
"Sampler logprobs must cover exactly the recorded sampling support"
|
|
271
|
+
)
|
|
272
|
+
if (values == VLLM_LOGPROB_SENTINEL).any():
|
|
273
|
+
raise MalformedGenerateResponseError("Missing sampling-support probabilities")
|
|
274
|
+
sampled = _sampled_entries(mask_ids, counts, completion_ids)
|
|
275
|
+
if len(counts):
|
|
276
|
+
mass = np.add.reduceat(np.exp(values.astype(np.float64)), offsets[:-1])
|
|
277
|
+
if not np.allclose(mass, 1.0, rtol=0, atol=1e-4):
|
|
278
|
+
raise MalformedGenerateResponseError(
|
|
279
|
+
"Sampler probabilities must normalize over the recorded support"
|
|
280
|
+
)
|
|
281
|
+
return values[sampled], _sampler_support(mask_ids, counts, values, replayed=True)
|
|
282
|
+
|
|
283
|
+
|
|
284
|
+
def _parse_mask_support(
|
|
285
|
+
choice: Mapping[str, Any], completion_ids: list[int]
|
|
286
|
+
) -> dict[str, Any]:
|
|
287
|
+
"""The recorded sampling mask of a request that returned no sampler logprobs for it."""
|
|
288
|
+
mask = choice.get("sampling_mask")
|
|
289
|
+
if isinstance(mask, Mapping):
|
|
290
|
+
ids, counts = _decode_packed_sampling_mask(mask)
|
|
291
|
+
_sampled_entries(ids, counts, completion_ids)
|
|
292
|
+
return _sampler_support(ids, counts, None, replayed=True)
|
|
293
|
+
rows = _parse_sampling_mask(choice, completion_ids)
|
|
294
|
+
assert rows is not None
|
|
295
|
+
return _sampler_support(
|
|
296
|
+
np.fromiter((i for row in rows for i in row), dtype=np.int32),
|
|
297
|
+
np.fromiter((len(row) for row in rows), dtype=np.int32, count=len(rows)),
|
|
298
|
+
None,
|
|
299
|
+
replayed=True,
|
|
300
|
+
)
|
|
301
|
+
|
|
302
|
+
|
|
303
|
+
def _parse_compact_logprobs(
|
|
304
|
+
choice: Mapping[str, Any], completion_ids: list[int], k: int | None
|
|
305
|
+
) -> tuple[np.ndarray, list[list[int]] | None, list[list[float]] | None]:
|
|
306
|
+
ids, values, offsets, num_top = _decode_compact_logprobs(choice, completion_ids)
|
|
201
307
|
sampled = []
|
|
202
308
|
top_ids: list[list[int]] = []
|
|
203
309
|
top_logprobs: list[list[float]] = []
|
|
204
|
-
|
|
205
|
-
for index, (token_id, start, end) in enumerate(
|
|
206
|
-
zip(completion_ids, offsets, offsets[1:])
|
|
207
|
-
):
|
|
310
|
+
for token_id, start, end in zip(completion_ids, offsets, offsets[1:]):
|
|
208
311
|
# FlatLogprobs can repeat the sampled ID. Match vLLM's dict order/value semantics.
|
|
209
312
|
items = list(zip(ids[start:end].tolist(), values[start:end].tolist()))
|
|
210
313
|
row = dict(items)
|
|
@@ -218,11 +321,7 @@ def _parse_compact_logprobs(
|
|
|
218
321
|
if k is not None:
|
|
219
322
|
head = dict(islice(row.items(), max(num_top, 1)))
|
|
220
323
|
selected_ids, selected_logprobs = _select_sampler_head(
|
|
221
|
-
head,
|
|
222
|
-
token_id,
|
|
223
|
-
row[token_id],
|
|
224
|
-
k,
|
|
225
|
-
supports[index] if supports is not None else None,
|
|
324
|
+
head, token_id, row[token_id], k
|
|
226
325
|
)
|
|
227
326
|
top_ids.append(selected_ids)
|
|
228
327
|
top_logprobs.append(selected_logprobs)
|
|
@@ -300,6 +399,28 @@ def _parse_completion_logprobs(
|
|
|
300
399
|
return np.asarray(completion_logprobs, dtype=np.float32)
|
|
301
400
|
|
|
302
401
|
|
|
402
|
+
def _decode_packed_sampling_mask(
|
|
403
|
+
mask: Mapping[str, Any],
|
|
404
|
+
) -> tuple[np.ndarray, np.ndarray]:
|
|
405
|
+
arrays = {}
|
|
406
|
+
for name in ("ids", "counts"):
|
|
407
|
+
packed = mask.get(name)
|
|
408
|
+
if not isinstance(packed, Mapping) or packed.get("dtype") != "int32":
|
|
409
|
+
raise MalformedGenerateResponseError("Invalid packed sampling mask")
|
|
410
|
+
try:
|
|
411
|
+
values = np.frombuffer(
|
|
412
|
+
base64.b64decode(packed["data"], validate=True), dtype=np.int32
|
|
413
|
+
)
|
|
414
|
+
except (KeyError, TypeError, ValueError, binascii.Error) as exc:
|
|
415
|
+
raise MalformedGenerateResponseError(
|
|
416
|
+
"Invalid packed sampling mask"
|
|
417
|
+
) from exc
|
|
418
|
+
if packed.get("shape") != [len(values)] or (values < 0).any():
|
|
419
|
+
raise MalformedGenerateResponseError("Invalid packed sampling mask")
|
|
420
|
+
arrays[name] = values
|
|
421
|
+
return arrays["ids"], arrays["counts"]
|
|
422
|
+
|
|
423
|
+
|
|
303
424
|
def _parse_sampling_mask(
|
|
304
425
|
choice: Mapping[str, Any], completion_ids: list[int]
|
|
305
426
|
) -> list[list[int]] | None:
|
|
@@ -307,23 +428,7 @@ def _parse_sampling_mask(
|
|
|
307
428
|
if mask is None:
|
|
308
429
|
return None
|
|
309
430
|
if isinstance(mask, Mapping):
|
|
310
|
-
|
|
311
|
-
for name in ("ids", "counts"):
|
|
312
|
-
packed = mask.get(name)
|
|
313
|
-
if not isinstance(packed, Mapping) or packed.get("dtype") != "int32":
|
|
314
|
-
raise MalformedGenerateResponseError("Invalid packed sampling mask")
|
|
315
|
-
try:
|
|
316
|
-
values = np.frombuffer(
|
|
317
|
-
base64.b64decode(packed["data"], validate=True), dtype=np.int32
|
|
318
|
-
)
|
|
319
|
-
except (KeyError, TypeError, ValueError, binascii.Error) as exc:
|
|
320
|
-
raise MalformedGenerateResponseError(
|
|
321
|
-
"Invalid packed sampling mask"
|
|
322
|
-
) from exc
|
|
323
|
-
if packed.get("shape") != [len(values)] or (values < 0).any():
|
|
324
|
-
raise MalformedGenerateResponseError("Invalid packed sampling mask")
|
|
325
|
-
arrays[name] = values
|
|
326
|
-
counts, ids = arrays["counts"], arrays["ids"]
|
|
431
|
+
ids, counts = _decode_packed_sampling_mask(mask)
|
|
327
432
|
if len(counts) != len(completion_ids) or int(counts.sum()) != len(ids):
|
|
328
433
|
raise MalformedGenerateResponseError("Sampling mask token count mismatch")
|
|
329
434
|
mask = [row.tolist() for row in np.split(ids, np.cumsum(counts)[:-1])]
|
|
@@ -500,7 +605,7 @@ async def generate(
|
|
|
500
605
|
Returns a dict with: request_id, prompt_ids, renderer_prompt_ids,
|
|
501
606
|
mm_placeholders, completion_ids, completion_logprobs, content,
|
|
502
607
|
reasoning_content, tool_calls, finish_reason, routed_experts,
|
|
503
|
-
weight_versions, multi_modal_data, prompt_attribution.
|
|
608
|
+
sampler_support, weight_versions, multi_modal_data, prompt_attribution.
|
|
504
609
|
``completion_logprobs`` is a float32 numpy array, one value per completion
|
|
505
610
|
token: vLLM samples in float32, so the packed form decodes without a copy
|
|
506
611
|
and the JSON forms narrow losslessly.
|
|
@@ -509,9 +614,13 @@ async def generate(
|
|
|
509
614
|
is vLLM's (>= 0.32) list of ``{version, start, end}`` spans over
|
|
510
615
|
completion-token indexes, or ``None`` when the engine labels none.
|
|
511
616
|
|
|
512
|
-
|
|
513
|
-
``
|
|
514
|
-
|
|
617
|
+
``sampler_support`` is the sampler's per-token support in CSR form, or ``None`` when
|
|
618
|
+
the engine returned none: ``counts[i]`` entries of the flat int32 ``ids`` (and float32
|
|
619
|
+
``logprobs``, or ``None``) per completion token. With ``replayed`` the rows are the
|
|
620
|
+
recorded sampling masks of truncated sampling, with logprobs normalized over each
|
|
621
|
+
mask when the engine returned them. Otherwise, when ``sampling_params["logprobs"]``
|
|
622
|
+
is a positive int, the rows are that many top candidates of the sampler's
|
|
623
|
+
distribution, independent of the sampled token and full-vocabulary normalized.
|
|
515
624
|
|
|
516
625
|
``prompt_attribution`` is the renderer's :class:`RenderedTokens` for
|
|
517
626
|
the prompt — either the one this call computed via
|
|
@@ -646,18 +755,27 @@ async def generate(
|
|
|
646
755
|
"Engine response must include mm_placeholders when process_multimodal=False."
|
|
647
756
|
)
|
|
648
757
|
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
if choice.get("compact_logprobs") is not None:
|
|
652
|
-
completion_logprobs,
|
|
653
|
-
|
|
758
|
+
sampler_support = None
|
|
759
|
+
replayed = choice.get("sampling_mask") is not None
|
|
760
|
+
if choice.get("compact_logprobs") is not None and replayed:
|
|
761
|
+
completion_logprobs, sampler_support = _parse_replayed_support(
|
|
762
|
+
choice, completion_ids
|
|
763
|
+
)
|
|
764
|
+
elif choice.get("compact_logprobs") is not None:
|
|
765
|
+
completion_logprobs, top_ids, top_logprobs = _parse_compact_logprobs(
|
|
766
|
+
choice, completion_ids, head_size
|
|
654
767
|
)
|
|
768
|
+
if top_ids is not None and top_logprobs is not None:
|
|
769
|
+
sampler_support = _pack_sampler_rows(top_ids, top_logprobs, replayed=False)
|
|
655
770
|
else:
|
|
656
771
|
completion_logprobs = _parse_completion_logprobs(choice, completion_ids)
|
|
657
772
|
if head_size is not None:
|
|
658
|
-
|
|
659
|
-
_parse_completion_top_logprobs(choice, completion_ids, head_size)
|
|
773
|
+
sampler_support = _pack_sampler_rows(
|
|
774
|
+
*_parse_completion_top_logprobs(choice, completion_ids, head_size),
|
|
775
|
+
replayed=replayed,
|
|
660
776
|
)
|
|
777
|
+
elif replayed:
|
|
778
|
+
sampler_support = _parse_mask_support(choice, completion_ids)
|
|
661
779
|
|
|
662
780
|
parsed = renderer.parse_response(
|
|
663
781
|
completion_ids,
|
|
@@ -666,10 +784,6 @@ async def generate(
|
|
|
666
784
|
)
|
|
667
785
|
|
|
668
786
|
routed_experts = choice.get("routed_experts")
|
|
669
|
-
# vLLM's native kept-set sampling masks (``--return-sampling-mask``):
|
|
670
|
-
# one list of surviving vocab ids per completion token, or (prime-rl's
|
|
671
|
-
# server) packed CSR ``{"ids", "counts"}`` base64 int32 arrays.
|
|
672
|
-
sampling_mask = choice.get("sampling_mask")
|
|
673
787
|
|
|
674
788
|
# /inference/v1/generate returns finish_reason in {"stop","length",...} —
|
|
675
789
|
# never "tool_calls" (a chat-completions concept). Promote stop→tool_calls
|
|
@@ -694,15 +808,13 @@ async def generate(
|
|
|
694
808
|
"mm_placeholders": mm_placeholders,
|
|
695
809
|
"completion_ids": list(completion_ids),
|
|
696
810
|
"completion_logprobs": completion_logprobs,
|
|
697
|
-
"
|
|
698
|
-
"completion_top_logprobs": completion_top_logprobs,
|
|
811
|
+
"sampler_support": sampler_support,
|
|
699
812
|
"content": parsed.content,
|
|
700
813
|
"reasoning_content": parsed.reasoning_content,
|
|
701
814
|
"tool_calls": parsed.tool_calls,
|
|
702
815
|
"finish_reason": finish_reason,
|
|
703
816
|
"reasoning_complete": parsed.reasoning_complete,
|
|
704
817
|
"routed_experts": routed_experts,
|
|
705
|
-
"sampling_mask": sampling_mask,
|
|
706
818
|
"weight_versions": choice.get("weight_versions"),
|
|
707
819
|
# The mm sidecar consumed on the request side, surfaced back so
|
|
708
820
|
# callers can persist it on the trajectory step for downstream
|
|
@@ -45,4 +45,7 @@ def replay_client(completions, requests):
|
|
|
45
45
|
def assert_evidence(result, ids):
|
|
46
46
|
assert result["completion_ids"] == ids
|
|
47
47
|
assert result["completion_logprobs"].tolist() == [-0.25] * len(ids)
|
|
48
|
-
|
|
48
|
+
support = result["sampler_support"]
|
|
49
|
+
assert support["replayed"] and support["logprobs"] is None
|
|
50
|
+
assert support["ids"].tolist() == ids
|
|
51
|
+
assert support["counts"].tolist() == [1] * len(ids)
|
|
@@ -16,6 +16,7 @@ from renderers.base import (
|
|
|
16
16
|
from renderers.client import (
|
|
17
17
|
_parse_compact_logprobs,
|
|
18
18
|
_parse_completion_top_logprobs,
|
|
19
|
+
_parse_replayed_support,
|
|
19
20
|
generate,
|
|
20
21
|
parse_generate_response,
|
|
21
22
|
)
|
|
@@ -59,6 +60,64 @@ def _parse_test_head(choice, sampled, k, compact):
|
|
|
59
60
|
return _parse_completion_top_logprobs(choice, sampled, k)
|
|
60
61
|
|
|
61
62
|
|
|
63
|
+
def _replayed_choice(rows, sampled):
|
|
64
|
+
"""A paired replay response: each row's kept ids and their sampler probabilities."""
|
|
65
|
+
ids = np.asarray([i for row in rows for i, _ in row], dtype=np.int32)
|
|
66
|
+
counts = np.asarray([len(row) for row in rows], dtype=np.int32)
|
|
67
|
+
values = np.log(np.asarray([p for row in rows for _, p in row], dtype=np.float32))
|
|
68
|
+
return {
|
|
69
|
+
"token_ids": sampled,
|
|
70
|
+
"compact_logprobs": {
|
|
71
|
+
"data": base64.b64encode(
|
|
72
|
+
ids.astype("<u4").tobytes() + values.astype("<f4").tobytes()
|
|
73
|
+
).decode("ascii"),
|
|
74
|
+
"format": "u32-f32-le-v1",
|
|
75
|
+
"offsets": [0, *np.cumsum(counts).tolist()],
|
|
76
|
+
"num_top_logprobs": int(counts.max(initial=0)),
|
|
77
|
+
},
|
|
78
|
+
"sampling_mask": {
|
|
79
|
+
name: {
|
|
80
|
+
"data": base64.b64encode(array.tobytes()).decode(),
|
|
81
|
+
"shape": [len(array)],
|
|
82
|
+
"dtype": "int32",
|
|
83
|
+
}
|
|
84
|
+
for name, array in (("ids", ids), ("counts", counts))
|
|
85
|
+
},
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
@pytest.mark.parametrize(
|
|
90
|
+
"error", [None, "mass", "unsampled", "duplicate", "mask", "empty"]
|
|
91
|
+
)
|
|
92
|
+
def test_replayed_support_keeps_the_recorded_mask_with_its_probabilities(error):
|
|
93
|
+
rows = [[(3, 0.2), (1, 0.6), (2, 0.2)], [(9, 1.0)]]
|
|
94
|
+
sampled = [3, 9]
|
|
95
|
+
if error == "mass":
|
|
96
|
+
rows[0][1] = (1, 0.5)
|
|
97
|
+
elif error == "unsampled":
|
|
98
|
+
sampled = [4, 9]
|
|
99
|
+
elif error == "duplicate":
|
|
100
|
+
rows[0] = [(3, 0.2), (1, 0.6), (3, 0.2)]
|
|
101
|
+
choice = _replayed_choice(rows, sampled)
|
|
102
|
+
if error == "mask":
|
|
103
|
+
choice["sampling_mask"] = _replayed_choice(
|
|
104
|
+
[[(3, 0.2), (1, 0.6), (5, 0.2)], [(9, 1.0)]], sampled
|
|
105
|
+
)["sampling_mask"]
|
|
106
|
+
elif error == "empty":
|
|
107
|
+
choice = _replayed_choice([], [])
|
|
108
|
+
if error not in (None, "empty"):
|
|
109
|
+
with pytest.raises(MalformedGenerateResponseError):
|
|
110
|
+
_parse_replayed_support(choice, sampled)
|
|
111
|
+
return
|
|
112
|
+
logprobs, support = _parse_replayed_support(choice, choice["token_ids"])
|
|
113
|
+
expected = [] if error == "empty" else [0.2, 1.0]
|
|
114
|
+
np.testing.assert_allclose(np.exp(logprobs), expected, rtol=1e-6)
|
|
115
|
+
assert support["replayed"]
|
|
116
|
+
assert support["ids"].tolist() == [i for row in rows for i, _ in row] * (not error)
|
|
117
|
+
assert support["counts"].tolist() == [len(row) for row in rows] * (not error)
|
|
118
|
+
assert len(support["logprobs"]) == len(support["ids"])
|
|
119
|
+
|
|
120
|
+
|
|
62
121
|
@pytest.mark.parametrize(
|
|
63
122
|
"update",
|
|
64
123
|
[
|
|
@@ -168,10 +227,9 @@ def test_top_logprobs_head_is_independent_of_sampled_action(
|
|
|
168
227
|
_parse_test_head({"logprobs": {"content": [entry]}}, [sampled], 2, compact)
|
|
169
228
|
|
|
170
229
|
|
|
171
|
-
@pytest.mark.parametrize("compact", [False, True])
|
|
172
230
|
@pytest.mark.parametrize("packed", [False, True])
|
|
173
231
|
@pytest.mark.parametrize("error", [None, "missing", "mass", "mask"])
|
|
174
|
-
def test_top_logprobs_preserve_complete_replay_support(
|
|
232
|
+
def test_top_logprobs_preserve_complete_replay_support(packed, error):
|
|
175
233
|
import math
|
|
176
234
|
|
|
177
235
|
probabilities = {1: 0.6, 2: 0.2, 3: 0.2}
|
|
@@ -200,9 +258,9 @@ def test_top_logprobs_preserve_complete_replay_support(compact, packed, error):
|
|
|
200
258
|
choice = {"logprobs": {"content": [entry]}, "sampling_mask": mask}
|
|
201
259
|
if error is not None:
|
|
202
260
|
with pytest.raises(MalformedGenerateResponseError):
|
|
203
|
-
|
|
261
|
+
_parse_completion_top_logprobs(choice, [3], 2)
|
|
204
262
|
else:
|
|
205
|
-
ids, logprobs =
|
|
263
|
+
ids, logprobs = _parse_completion_top_logprobs(choice, [3], 2)
|
|
206
264
|
assert ids == [[1, 2, 3]]
|
|
207
265
|
np.testing.assert_allclose(np.exp(logprobs), [[0.6, 0.2, 0.2]])
|
|
208
266
|
|
|
@@ -425,13 +483,13 @@ def test_generate_builds_request_body_and_parses_response(
|
|
|
425
483
|
assert result["completion_ids"] == [7, 8]
|
|
426
484
|
np.testing.assert_allclose(result["completion_logprobs"], [-0.1, -0.2])
|
|
427
485
|
if head_size is not None:
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
)
|
|
486
|
+
support = result["sampler_support"]
|
|
487
|
+
assert not support["replayed"]
|
|
488
|
+
assert support["ids"].tolist() == [7, 20, 8, 20]
|
|
489
|
+
assert support["counts"].tolist() == [2, 2]
|
|
490
|
+
np.testing.assert_allclose(support["logprobs"], [-0.1, -4.0, -0.2, -4.0])
|
|
432
491
|
else:
|
|
433
|
-
assert result["
|
|
434
|
-
assert result["completion_top_logprobs"] is None
|
|
492
|
+
assert result["sampler_support"] is None
|
|
435
493
|
if extra_args is not None:
|
|
436
494
|
assert extra_args == {"prl_compact_logprobs": False, "custom_arg": 7}
|
|
437
495
|
assert result["routed_experts"]["shape"] == [2, 1, 1]
|
|
@@ -1385,7 +1385,7 @@ requires-dist = [
|
|
|
1385
1385
|
{ name = "numpy" },
|
|
1386
1386
|
{ name = "openai", specifier = ">=1.108.1" },
|
|
1387
1387
|
{ name = "openai-harmony", specifier = ">=0.0.4" },
|
|
1388
|
-
{ name = "pillow", marker = "extra == 'multimodal'", specifier = ">=
|
|
1388
|
+
{ name = "pillow", marker = "extra == 'multimodal'", specifier = ">=13.0.0" },
|
|
1389
1389
|
{ name = "prime-pydantic-config", specifier = ">=0.3.0.dev83" },
|
|
1390
1390
|
{ name = "tiktoken" },
|
|
1391
1391
|
{ name = "transformers", marker = "extra == 'multimodal'", specifier = ">=4.50.0" },
|
|
@@ -1395,7 +1395,7 @@ provides-extras = ["multimodal", "transformers"]
|
|
|
1395
1395
|
|
|
1396
1396
|
[package.metadata.requires-dev]
|
|
1397
1397
|
dev = [
|
|
1398
|
-
{ name = "pillow", specifier = ">=
|
|
1398
|
+
{ name = "pillow", specifier = ">=13.0.0" },
|
|
1399
1399
|
{ name = "pre-commit" },
|
|
1400
1400
|
{ name = "pytest", specifier = ">=7.0.0" },
|
|
1401
1401
|
{ name = "pytest-asyncio", specifier = ">=0.21.0" },
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|