Skip to content

Empty _REQ_TYPES_WITH_OPAQUE_FIELDS on the msgpack IPC path (#29465 Task 4) - #30182

Merged
Fridge003 merged 11 commits into
sgl-project:mainfrom
jorgeantonio21:refactor/ja/msgpack-empty-opaque-registry
Jul 15, 2026
Merged

Fridge003 merged 11 commits into
sgl-project:mainfrom
jorgeantonio21:refactor/ja/msgpack-empty-opaque-registry

Conversation

@jorgeantonio21

@jorgeantonio21 jorgeantonio21 commented Jul 5, 2026 •

Copy link
Copy Markdown
Contributor

What this does

Closes Task 4 of #29465. The 13 IPC structs in _REQ_TYPES_WITH_OPAQUE_FIELDS had
Dict[str, Any] / List[Any] fields, so on the msgpack path (SGLANG_USE_PICKLE_IPC=0)
_maybe_wrap_pickle whole-struct-pickled them inside a PickleWrapper frame. This PR tightens each
field to a precise msgspec-native type (or deletes a dead type), then removes the registry and its
_maybe_wrap_pickle branch. End state: the tuple no longer exists and every IPC struct encodes
natively.

Acceptance criterion (_REQ_TYPES_WITH_OPAQUE_FIELDS is empty) is met — the symbol is deleted.

Per-type changes

12 types tightened, 1 deleted:

Type Change
UpdateWeightFromDiskReqInput manifest documented as JSON-native; annotation unchanged
VertexGenerateReqInput instances/parameters documented as JSON-native; annotation unchanged
DumperControlReqInput body documented; isinstance(body, dict) guard added at the /dumper endpoint
DumperControlReqOutput response documented as a JSON-native List[Dict]
GetWeightsByNameReqOutput parameter → Optional[List[Union[float, List[float]]]] (element-level union)
SetInternalStateReq server_args → Dict[str, Union[int, float]]
RpcReqInput parameters → Optional[Dict[str, Union[bool, int, float, str, None]]]
LoadLoRAAdapterFromTensorsReqInput added_tokens_config → Optional[Dict[str, int]]; config_dict kept Dict[str, Any] (see note)
GetInternalStateReqOutput internal_state kept Dict[str, Any]; producer sanitizes via msgspec_to_builtins
SetInternalStateReqOutput dead server_args field removed (only .updated is read)
BackupDramReq weight_pointer_map → Dict[str, ExpertWeightPointer] (payload narrowed — see below)
CheckWeightsReqOutput payload → Optional[List[ChecksumInfo]] (new mirror structs)
SetInjectDumpMetadataReqInput deleted — no producer or consumer in-tree

Supporting changes:

  • msgspec_to_builtins now also converts dataclasses (→ dict), so the live CudaGraphConfig
    dataclass in the vars(ServerArgs) dump sanitizes consistently on both transports instead of
    only on the msgpack path. get_internal_state also drops custom_sigquit_handler from the
    dump — it can hold a bound signal handler and has no reader.
  • CheckWeightsReqOutput: ParallelismInfo/ChecksumInfo msgspec mirrors added in io_struct.py
    (the pydantic models in weight_checker.py stay as the checker's source of truth). The producer
    normalizes the tp>1 all_gather_object list and the tp==1 case to a uniform
    List[ChecksumInfo]; the consumer reads struct attributes and converts back to builtins, so the
    /weights_checker HTTP response shape is unchanged.
  • config_dict on LoadLoRAAdapterFromTensorsReqInput stays Dict[str, Any]: it is the PEFT
    adapter_config.json, already JSON on both paths, and a tighter type would only add decode
    strictness on out-of-tree callers with no consumer benefit.

Deployment / compatibility

These structs cross every sglang zmq socket. Under SGLANG_USE_PICKLE_IPC=0 the wire format changes,
so all nodes and processes must run the same version with the same setting (DP inter-node TCP
handshake, PD pairs, routers, elastic-EP). SGLANG_USE_PICKLE_IPC=1 is a kill-switch for framing
only
.

Three types change the payload on the pickle path too, so the kill-switch restores framing but
not compatibility — revert is the mitigation for these:

  • SetInternalStateReqOutput — the server_args field is gone; an old-code peer fails to
    reconstruct the output.
  • CheckWeightsReqOutput — the payload is ChecksumInfo structs instead of dicts.
  • BackupDramReq — the payload is narrowed (see below).

BackupDramReq payload narrowing: ExpertWeightPointer carries only weight_ptr and byte_size.
The producer previously sent five more keys per entry — name, shape, numel, dtype,
element_size — none of which the consumer reads (it derives those from the local parameter tensor,
and gets the name from the map key). Because BackupDramReq only travels during an elastic-EP weight
refresh, a mixed-version fleet looks healthy until the first refresh, then fails at weight-update
time.

Validation

  • New unit test test/registered/unit/managers/test_msgpack_ipc_roundtrip.py (register_cpu_ci,
    base-c-test-cpu): per-type native round-trip and double-hop re-encode for all 12 types, the
    registry-empty assertion, the ExpertWeightPointer narrowing, field-parity between the
    CheckWeightsReqOutput mirrors and the pydantic models, a tp>1-shaped payload, and the
    GetInternalStateReqOutput dataclass sanitization (materializing CudaGraphConfig).
  • Existing test_io_struct.py passes unchanged.

BackupDramReq needs manual validation before merge. CI has no elastic-EP path, so the wire
change is only unit-covered. TODO before merge: run an elastic-EP backup→refresh cycle on a 2-node
mooncake setup with SGLANG_USE_PICKLE_IPC=0, confirm weights transfer and inference stays correct,
and record the result here. If that environment is unavailable at merge time, land types 1 and 3–13
in this PR (registry down to (BackupDramReq,), machinery not yet deleted) and finish BackupDramReq
plus the deletion in an immediate fast-follow.

Notes

Out of scope (separate follow-ups)

  • continuous_buffer=None crash when a node holds no expert weights
    (expert_backup_manager.py).
  • engine.py base64-encode fix for load_format='flattened_bucket' (out-of-tree Engine callers
    passing raw bytes).
  • Deleting the orphaned SetInjectDumpMetadataReqOutput / LazyDumpTensors pair.
  • The five wrong -> Optional[torch.Tensor] return annotations in model files.
  • save_sharded_state.py --max-file-size type=str bug.

CI States

Latest PR Test (Base): ❌ Run #29377658530
Latest PR Test (Extra): ❌ Run #29377658496

Tighten the types in _REQ_TYPES_WITH_OPAQUE_FIELDS to precise msgspec-native annotations (or delete the dead one), then delete the registry and its _maybe_wrap_pickle branch so every IPC struct encodes natively on the msgpack path.

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request tightens IPC struct types from opaque annotations to precise msgspec-native types, removing the _REQ_TYPES_WITH_OPAQUE_FIELDS registry, and adds a comprehensive round-trip test suite for msgpack IPC serialization. The feedback highlights a critical issue in weight_updater.py where msgspec.convert needs from_attributes=True to successfully convert Pydantic models, and suggests expanding the test suite to cover this conversion path.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment thread python/sglang/srt/managers/scheduler_components/weight_updater.py
Comment thread test/registered/unit/managers/test_msgpack_ipc_roundtrip.py
@jorgeantonio21
jorgeantonio21 force-pushed the refactor/ja/msgpack-empty-opaque-registry branch from 0fd53a1 to 74947dc Compare July 5, 2026 21:17
@jorgeantonio21
jorgeantonio21 marked this pull request as ready for review July 5, 2026 21:20
for field in msgspec.structs.fields(type(obj))
}

if dataclasses.is_dataclass(obj) and not isinstance(obj, type):

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In which senario the object can be dataclass and enum data type?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point. The dataclass branch seems to be needed as internal_state is a vars(ServerArgs) dump. Also cuda_graph_config is a CudaGraphConfig dataclass that can't encode natively without it.

Regarding the enum you are right. Initially I though we needed to handle disaggregation_mode: DisaggregationMode, that said after inspecting again it seems we store disaggregation_mode: str, so we don't need to handle enums at all.

class DumperControlReqOutput(BaseReq, kw_only=True):
success: bool
# JSON-native per-worker response dicts.
response: List[Dict[str, Any]]

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is the real data type for the dictionary, will those type not able to be serialized to bytes?

@jorgeantonio21 jorgeantonio21 Jul 8, 2026 •

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In this case, the body is the raw request.json() (afaik, the dumper method endpoint rejects it unless it is a dict). This value is then passed to dumper.configure(**body). Since it comes straight from json.loads, every value is already JSON-native and encodes fine over msgpack.

I think it should stay Dict[str, Any] because configure() takes open-ended kwargs with no fixed schema.

class DumperControlReqInput(BaseReq, kw_only=True):
method: str
# JSON request body (guarded to be a dict at the /dumper endpoint).
body: Dict[str, Any]

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is the real data type for the dictionary, will those type not able to be serialized to bytes?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

response is what handle_request returns (typed -> list[dict]), e.g. get_state()'s config dict, or {} for configure/reset. It's built and re-serialized with json.dumps, so every value is JSON-native and msgpack-safe.

Like body, it stays Dict[str, Any] because the dumper response has no fixed schema.

"element_size": weight_info["element_size"],
"byte_size": byte_size,
}
self.weight_pointer_map[name] = ExpertWeightPointer(

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why should we drop the shape, numel, dtype element_size etc info here?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I couldn't find any consumer that reads them in the codebase. For example, ExpertBackupClient.update_weights uses only weight_ptr and byte_size. Every other field is derivable: name comes from the map key, and shape/numel/dtype/element_size are re-derived from the local parameter tensor (we even assert numel * element_size == byte_size).

So those five were adding only weight on the wire. With one entry per expert weight, ExpertWeightPointer is array_like too, so each entry is just [weight_ptr, byte_size].

)

No in-tree object reaches the enum path. internal_state is a vars(ServerArgs) dump whose only non-msgpack-native value is the cuda_graph_config dataclass; disaggregation_mode is a str, not the DisaggregationMode enum. Remove the enum branch and its synthetic test injection.
@rainj-me

rainj-me commented Jul 8, 2026

Copy link
Copy Markdown
Collaborator

/tag-run-ci-label

@github-actions github-actions Bot added the run-ci CI: run the baseline test suite on this PR label Jul 8, 2026
@rainj-me

rainj-me commented Jul 8, 2026

Copy link
Copy Markdown
Collaborator

/tag-and-rerun-ci

Resolve two conflicts introduced by upstream sgl-project#30525 (drop legacy
get_loads IPC; move metrics structs to load_snapshot.py):

- io_struct.py: apply both deletions — the PR removes
  SetInjectDumpMetadataReqInput while upstream removes the GetLoads*
  and metrics structs, so the region between BlockReqInput and
  SetInjectDumpMetadataReqOutput collapses to empty.
- scheduler.py set_internal_state: keep the PR's
  `return SetInternalStateReqOutput(updated=if_success)` (the
  server_args field is gone from the struct) on top of upstream's
  get_server_args().override(...) rename.
@rainj-me

Copy link
Copy Markdown
Collaborator

/tag-and-rerun-ci

@rainj-me

Copy link
Copy Markdown
Collaborator

/tag-and-rerun-ci

@Fridge003
Fridge003 merged commit 26cb0fc into sgl-project:main Jul 15, 2026
247 of 280 checks passed
@jorgeantonio21
jorgeantonio21 deleted the refactor/ja/msgpack-empty-opaque-registry branch July 16, 2026 10:24
Chronostasys pushed a commit to MindLab-Research/sglang that referenced this pull request Aug 24, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

run-ci CI: run the baseline test suite on this PR

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants