refactor(wire): generic packed-ndarray codec and an N-field response-body splice - #2234
erictang000 wants to merge 3 commits into
Conversation
There was a problem hiding this comment.
Code Review
This pull request generalizes the serialization of side-channel data by introducing helpers to pack and unpack arbitrary NumPy arrays as base64 envelopes with sidecar fields. It also adds an optimized response-parsing utility (load_packed_body) that splices base64 blobs directly into memoryviews to avoid large string allocations. The review feedback suggests consistently using the .value attribute of StrEnum members (such as PackedArrayKey and PackedField) during dictionary lookups and membership checks to prevent potential issues with strict key matching and third-party serialization libraries.
| dtype_name = payload[PackedArrayKey.DTYPE] | ||
| shape = tuple(payload[PackedArrayKey.SHAPE]) | ||
| data = pybase64.b64decode_as_bytearray(payload[PackedArrayKey.DATA], validate=True) |
There was a problem hiding this comment.
For consistency and to prevent any potential issues with custom mapping implementations or strict key type checks, it is safer to use .value when accessing dictionary keys with StrEnum members, especially since orjson and other libraries can be sensitive to str subclasses.
| dtype_name = payload[PackedArrayKey.DTYPE] | |
| shape = tuple(payload[PackedArrayKey.SHAPE]) | |
| data = pybase64.b64decode_as_bytearray(payload[PackedArrayKey.DATA], validate=True) | |
| dtype_name = payload[PackedArrayKey.DTYPE.value] | |
| shape = tuple(payload[PackedArrayKey.SHAPE.value]) | |
| data = pybase64.b64decode_as_bytearray(payload[PackedArrayKey.DATA.value], validate=True) |
| if isinstance(node, dict): | ||
| for key, value in node.items(): | ||
| queue = blobs.get(key) | ||
| if queue is not None and isinstance(value, dict) and PackedArrayKey.DATA in value: |
There was a problem hiding this comment.
Use PackedArrayKey.DATA.value instead of PackedArrayKey.DATA for consistency with line 248 where .value is explicitly used. This ensures uniform dictionary key lookups and avoids mixing StrEnum members with their underlying string values.
| if queue is not None and isinstance(value, dict) and PackedArrayKey.DATA in value: | |
| if queue is not None and isinstance(value, dict) and PackedArrayKey.DATA.value in value: |
| routed_experts = None | ||
| if return_routed_experts: | ||
| packed_routed_experts = choice.get("routed_experts") | ||
| packed_routed_experts = choice.get(PackedField.ROUTED_EXPERTS) |
There was a problem hiding this comment.
Use PackedField.ROUTED_EXPERTS.value instead of the StrEnum member PackedField.ROUTED_EXPERTS directly when performing dictionary lookups. This maintains consistency with how dictionary keys are handled elsewhere in the codebase (e.g., in vllm_server_actor.py and generate_wire.py) and avoids potential issues with strict key matching.
| packed_routed_experts = choice.get(PackedField.ROUTED_EXPERTS) | |
| packed_routed_experts = choice.get(PackedField.ROUTED_EXPERTS.value) |
|
7dd3ff3 to
ef7afef
Compare
5632720 to
0e48535
Compare
0e48535 to
1b51b86
Compare
1b51b86 to
c5d3cea
Compare
c5d3cea to
1b51b86
Compare
Note
Standalone-reviewable reopen of #2079 — original work by @dyurk-lila.
Commits are pushed unchanged, so author and committer metadata are preserved.
The branch lives on
NovaSky-AI/SkyRLrather than a fork so CI runs.Stack position 4/11 — based on #2233 (branch
dyurk/r3-pooled-route-collation).Because the base is the previous branch in the stack, the Files changed tab above shows only this PR's own changes, not the cumulative stack. Merge bottom-up.
Supersedes #2079.
Problem
The generate endpoint returns large NumPy side channels inside a JSON response. Parsing the entire body normally first materializes each base64 blob as a Python string, adding a large allocation and copy before decoding. The existing optimization recognized only the routed-expert field through one hard-coded byte prefix; adding another packed field would silently leave that second blob on the expensive path.
Wire contract
Packed arrays use one typed envelope:
{"data": "<base64>", "shape": [12, 40, 8], "dtype": "int16"}datais deliberately the first key. The client can then replace registered blobs withmemoryviewobjects before handing the remaining JSON toorjson. Shape, dimensionality, dtype, and decoded byte count are still validated by the field-specific decoder.Implementation
pack_ndarrayandunpack_ndarrayhelpers with explicit allowed-dtype and dimensionality contracts.nullfields, sidecar metadata, multiple choices, and all ordinary JSON content.orjsonparsing.Transport errors and undecodable gateway responses retain the existing retry behavior. A valid JSON response with a malformed packed contract is treated as a deterministic protocol error and is not retried.
Performance
For a representative 121 MiB response containing 90 MiB of routes, parsing and decoding improved from 268 ms to 83 ms. Registering a second packed field did not add another full-body scan. The optimization is therefore tied to the response as a whole rather than to one particular side channel.
Testing
nullvalues, JSON lookalikes, reordered envelopes, unregistered fields, and unterminated data.Note
Medium Risk
Changes the generate response parsing contract and multi-turn routed-expert bookkeeping used for training replay; mistakes could corrupt routes or silently mis-parse payloads, though layout drift is explicitly detected.
Overview
Generate wire format moves from routed-expert-only packing to shared
pack_ndarray/unpack_ndarrayenvelopes (datafirst for scanning) and a centralPackedFieldregistry (includingrollout_sample_support).load_packed_bodysplices registered base64 blobs intomemoryviewbeforeorjsonparses the rest, so multiple large side channels avoid giant Python strings; layout drift fails fast instead of silently falling back.Remote inference pulls raw
/skyrl/v1/generateHTTP intoRemoteGenerateClient, enables packed parsing only when returning routed experts, and forwardsrouted_experts_prompt_startsasrouted_experts_prompt_startin sampling params.Multi-turn R3 adds
TokenMetadataTraceandRoutedExpertTraceso the gym generator records incremental expert rows per turn and finalizes against token count and loss mask; batch preprocessing collates routes in parallel viafill_batch_rowsandcpu_topology.pool_workers(also used for delta weight-sync thread counts).Megatron routing replay now rejects virtual pipeline parallelism (config validation and worker setup) because it desyncs replay FIFOs.
Reviewed by Cursor Bugbot for commit 7dd3ff3. Bugbot is set up for automated code reviews on this repo. Configure here.