renderers 0.2.1.dev8__py3-none-any.whl → 0.2.1.dev9__py3-none-any.whl

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/_version.py CHANGED
@@ -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.dev8'
22
- __version_tuple__ = version_tuple = (0, 2, 1, 'dev8')
21
+ __version__ = version = '0.2.1.dev9'
22
+ __version_tuple__ = version_tuple = (0, 2, 1, 'dev9')
23
23
 
24
24
  __commit_id__ = commit_id = None
renderers/base.py CHANGED
@@ -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
- "sampling_mask": None,
2233
+ "sampler_support": None,
2234
2234
  }
2235
2235
  if (
2236
2236
  full_rendered.multi_modal_data is not None
renderers/client.py CHANGED
@@ -169,9 +169,10 @@ def _parse_packed_completion_logprobs(
169
169
  return values.astype(np.float32, copy=False)
170
170
 
171
171
 
172
- def _parse_compact_logprobs(
173
- choice: Mapping[str, Any], completion_ids: list[int], k: int | None
174
- ) -> tuple[np.ndarray, list[list[int]] | None, list[list[float]] | None]:
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
- supports = _parse_sampling_mask(choice, completion_ids) if k is not None else None
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
- arrays = {}
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
- When ``sampling_params["logprobs"]`` is a positive int, also
513
- ``completion_top_ids`` / ``completion_top_logprobs``
514
- (original sampler probabilities, retaining full-vocabulary normalization).
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
- completion_top_ids = None
650
- completion_top_logprobs = None
651
- if choice.get("compact_logprobs") is not None:
652
- completion_logprobs, completion_top_ids, completion_top_logprobs = (
653
- _parse_compact_logprobs(choice, completion_ids, head_size)
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
- completion_top_ids, completion_top_logprobs = (
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
- "completion_top_ids": completion_top_ids,
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
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: renderers
3
- Version: 0.2.1.dev8
3
+ Version: 0.2.1.dev9
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
@@ -1,7 +1,7 @@
1
1
  renderers/__init__.py,sha256=Ti81-To_6zlqryQ5aKxQeuJTtkXD89FKrMDNUu94kMQ,7575
2
- renderers/_version.py,sha256=dzjz2q4zSSLLzM-JewkkijjhCsUiJ8HU-9lTzeeG2Pk,533
3
- renderers/base.py,sha256=-tYbi-mh6gpm2qsHovKGK8Q6wW1PTUqEjPP5iFWdJds,96104
4
- renderers/client.py,sha256=z89KYLKpLlviGP6UqNvZX4fCZdr1KHWNiJT1pQOYrdM,43666
2
+ renderers/_version.py,sha256=GmUA9E_4oKM6yzYbkMFjjMl7KG0RqexjdSLxohFQSBI,533
3
+ renderers/base.py,sha256=FzkAxXVYEb_EoHGMcUCOI78vDmAoXdD1G0_6aykrlJ0,96106
4
+ renderers/client.py,sha256=fGDjUnuXrloeokwW82ldVdO4WAw0LOzw6GCzhPjdvjo,48613
5
5
  renderers/configs.py,sha256=ZY7IchF0KDQ3WOvNv_uSbMthWfvVWgC4Sj7H89kH04U,46829
6
6
  renderers/custom.py,sha256=TC08fa2x8dJ-EF-8do9TCrq1uSBKyh0R6hwpXKdgXwo,3613
7
7
  renderers/deepseek_r1.py,sha256=mYpRRx4WkSg642FT0UDK-R8ha8Lk0GBSWirZG2PafJA,2660
@@ -32,7 +32,7 @@ renderers/qwen38.py,sha256=Kyl-NlxXteyEHiMJgs8ZMYYnjmtV8nenmGVhGdL5GjQ,3247
32
32
  renderers/qwen3_vl.py,sha256=F66Rt26BM123C8USHE1dxAaGcFmw8f87-Hre8bzIPxE,43208
33
33
  renderers/reasoning.py,sha256=mdPnEg99y2ialU4APdL7eubjQ4WRF75wHWmKz94N9DA,8738
34
34
  renderers/tagged.py,sha256=91NgN61TvzWB2Y6UTEvpH-UNC0XGUrYIfcS8MskTSas,6357
35
- renderers-0.2.1.dev8.dist-info/METADATA,sha256=-bPw8jO1-3u3AtIu8YaeFfTJiLr4mNeUyOvbICxo_nw,22254
36
- renderers-0.2.1.dev8.dist-info/WHEEL,sha256=2iZrJkDIkeDh0k9SikKMqFuRP-ua6mb4Ir1AsER4BEw,87
37
- renderers-0.2.1.dev8.dist-info/licenses/LICENSE,sha256=u3UTCrYI-lDtgtxpbCiEyDzK_E6vS8U10W_IEDH2mdE,11287
38
- renderers-0.2.1.dev8.dist-info/RECORD,,
35
+ renderers-0.2.1.dev9.dist-info/METADATA,sha256=-NunNM_YD_pC3SgdSm32dFEyoWBPz1H85LQ8EuQKu2s,22254
36
+ renderers-0.2.1.dev9.dist-info/WHEEL,sha256=2iZrJkDIkeDh0k9SikKMqFuRP-ua6mb4Ir1AsER4BEw,87
37
+ renderers-0.2.1.dev9.dist-info/licenses/LICENSE,sha256=u3UTCrYI-lDtgtxpbCiEyDzK_E6vS8U10W_IEDH2mdE,11287
38
+ renderers-0.2.1.dev9.dist-info/RECORD,,