Skip to content

feat: support Qwen3-MoE weight conversion and dtype-only IPC grouping - #113

Merged
chaokunyang merged 4 commits into
inclusionAI:mainfrom
dingzhiqiang:pr/qwen3-moe-support
Jul 23, 2026
Merged

feat: support Qwen3-MoE weight conversion and dtype-only IPC grouping#113
chaokunyang merged 4 commits into
inclusionAI:mainfrom
dingzhiqiang:pr/qwen3-moe-support

Conversation

@dingzhiqiang

Copy link
Copy Markdown
Contributor

What does this PR do?

Two self-contained additions on top of #112 (only the last 2 commits are new; the diff collapses automatically once #112 merges).

feat: register Qwen3MoeForCausalLM mcore converter

AWEX had no name mapping for Qwen3-MoE, so the model could not go through colocate transfer. Differences from the bailing converters:

  • Stock HF/SGLang qwen3_moe serves canonical attention names (self_attn.{q,k,v,o}_proj + q/k_norm), so names are kept verbatim instead of the bailing-flavored renames.
  • Megatron's fused linear_qkv is split with GQA-aware group strides: the base equal-thirds split only holds when q/k/v head counts match, which is not the case under GQA (Qwen3-30B-A3B: 32 q-heads / 4 kv-heads). Row-count validation raises on any mismatch instead of mis-splitting silently.

Registered via the standard module-level CONFIG dict (same discovery pattern as ling.py); no existing file is modified.

fix: group IPC payload tensors by dtype only

group_tensors_by_shape_and_dtype keyed buckets by (shape, dtype). When payload entries have unique lengths this degenerates to nearly one group (= one CUDA IPC handle) per parameter. Observed on a 30B MoE model: 4888 params -> 4636 groups, and the receiver's cudaIpcOpenMemHandle fails with a driver OOM despite >50 GB free GPU memory.

Reconstruction slices each entry by element offset/size and reshapes from per-entry metadata, so same-shape packing was never a requirement. This change keys groups by dtype alone and flattens entries (reshape(-1)) before concatenation; group count drops to O(dtypes x size-cap) and the handle-exhaustion failure disappears. Dense payloads benefit identically: fewer, larger IPC handles.

Testing

  • End to end on Qwen3-30B-A3B colocate RL training: weight-sync correctness verified bitwise against a full-transfer snapshot across multi-step runs.
  • The reconstruction path (reconstruct_tensors_from_groups) is offset/metadata-driven and unchanged.

Related issues

None.

Does this PR introduce any user-facing change?

  • Does this PR introduce any public API change?
  • Does this PR introduce any binary protocol compatibility change?

group_tensors_by_shape_and_dtype keeps its signature; only the internal grouping key changes. Producer and consumer must run the same version, as with any change to the group metadata layout.

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

Copy link
Copy Markdown

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 introduces the Awex SGLang adapter and plugin to support colocated weight transfer, metadata collection, and memory management, alongside adding support for Qwen3MoE and fixing several tensor conversion, sharding, and synchronization issues. The code review highlights critical issues that need to be addressed, including potential serialization errors when returning custom metadata objects via FastAPI, missing None checks for process handles and KV store lookups, and type mismatch bugs when handling string-serialized torch.dtype fields during chunked transfers. Additionally, the reviewer suggests adding divisibility checks for robust tensor splitting and removing unused attributes to clean up dead code.

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 awex/engine/sglang/plugin.py Outdated
Comment thread awex/engine/sglang/plugin.py Outdated
Comment thread awex/transfer/nccl_stream_batch.py Outdated
Comment thread awex/transfer/nccl_stream_batch.py
Comment thread awex/models/qwen3_moe.py
Comment thread awex/engine/sglang/adapter.py Outdated
Comment thread awex/sglang_awex_adapter.py Outdated
Comment on lines +68 to +69
self._timeout_s: float = 120.0
self._http_client = None

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

The self._http_client attribute is initialized and closed, but never used anywhere else in the class. Remove it to clean up dead code and reduce complexity.

Suggested change
self._timeout_s: float = 120.0
self._http_client = None
self._timeout_s = timeout_s

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.

Removed in 2d94a5d (PR #112).

Comment thread awex/engine/sglang/adapter.py Outdated
Comment thread awex/engine/sglang/adapter.py Outdated
@dingzhiqiang
dingzhiqiang force-pushed the pr/qwen3-moe-support branch from 19793a8 to 024c007 Compare July 21, 2026 08:55
Comment thread awex/engine/sglang/plugin.py Outdated
self._adapter = None

def bind(self) -> None:
methods = [

@chaokunyang chaokunyang Jul 21, 2026

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.

Implement the reader-facing worker task protocol

This bridge exposes dedicated metadata, initialization, and update RPCs, but AWEX already dispatches InferParamMetaResolver._get_model_param_info, WeightsReader._init_in_tp_worker, and _update_parameters_in_tp_worker through SGLangEngine.execute_task_in_model_worker(). The child process only needs to resolve the task and inject model plus a complete model_context. Because this bridge does not implement that protocol and instead owns conversion and updates itself, it bypasses converter selection, backend selection, validation, and memory sequencing in the standard reader. Replace these dedicated update RPCs with generic task dispatch and keep conversion and updates in WeightsReader/WorkerWeightsReader.

Comment thread awex/engine/sglang/adapter.py Outdated
"num_engines": 1,
}

def _unfuse_params(

@chaokunyang chaokunyang Jul 21, 2026

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.

Keep parameter conversion in the converter registry

_unfuse_params() creates a second conversion path outside get_infer_weights_converter. Model-specific SGLang converters therefore never run here: for example, the word_embeddings -> embed_tokens normalization added in ling_linear.py is skipped, as are MoE layouts and device-specific transpose/FP8 handling. This can make inference metadata names or shapes diverge from writer metadata. The plugin should pass model and model_context to the AWEX task and let the registry-selected converter perform conversion.

Comment thread awex/engine/sglang/adapter.py Outdated
self._timeout_s = timeout_s

from awex.meta.meta_server import MetaServerClient
infer_meta_key = f"infer_params_meta_{pair_name}"

@chaokunyang chaokunyang Jul 21, 2026

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.

Use the existing reader/writer handshake

This path introduces pair-suffixed metadata keys, then reads colocate_weights_rank* and writes colocate_done_rank*. The existing WeightsReader/NCCLWeightsWriter protocol uses infer_params_meta, training_params_meta, training_serialized_weights_{ip}_{device}_{step}, weights_update_finished*, and write_finished*. The two sides never rendezvous on the same keys, so this path still times out after the initialization errors are fixed. Do not duplicate the transfer protocol in the plugin; invoke the existing NCCLWorkerWeightsReader through model_context.

Comment thread awex/engine/sglang/adapter.py Outdated
world_size = tp_size * pp_size
global_rank = int(getattr(self._scheduler, "tp_rank", 0))

return {

@chaokunyang chaokunyang Jul 21, 2026

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.

Provide one complete model_context

_build_rank_info() immediately passes this dictionary to get_sglang_rank_info(), which unconditionally reads attn_dp_rank and local_rank. Neither key is present, so the first metadata call raises KeyError on attn_dp_rank. Do not assemble a reduced context inside the conversion adapter. The SGLang child-process interface should build the complete model_context once from the real process groups and provide it to every AWEX reader task.

Comment thread awex/engine/sglang/launcher.py Outdated
def main():
from awex.engine.sglang.plugin import register_awex_sglang_plugin

register_awex_sglang_plugin()

@chaokunyang chaokunyang Jul 21, 2026

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.

Register the advertised HTTP routes

The launcher patches run_scheduler_process and then starts SGLang, but nothing in the repository calls register_awex_endpoints(). As a result, none of the documented /awex/* routes are attached to the FastAPI application and this control plane is unreachable. The primary integration should use the generic child-process task protocol; if the HTTP layer remains, attach it explicitly during app construction and add an integration test that asserts the routes exist.

Comment thread awex/engine/sglang/adapter.py Outdated
infer_meta_key = f"infer_params_meta_{pair_name}"
train_meta_key = f"training_params_meta_{pair_name}"

meta_client = MetaServerClient(kv_store_url)

@chaokunyang chaokunyang Jul 21, 2026

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.

Construct MetaServerClient with address and port

MetaServerClient.__init__ requires (address, port), but this call and the second call in the execution path pass only kv_store_url. The initialization RPC therefore raises TypeError before reading any metadata. The better fix is to remove duplicate meta-client ownership from the plugin and reuse the reader. If this path remains temporarily, parse and pass the host and port separately and cover initialization with a CPU-only test.

Comment thread awex/engine/sglang/adapter.py Outdated
step_id=version,
)

done_key = f"colocate_done_rank{paired_train_rank}_{version}"

@chaokunyang chaokunyang Jul 21, 2026

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.

Close IPC mappings before acknowledging completion

This writes colocate_done before deleting deserialized_weights and group_shared, synchronizing, and running garbage collection. Once the training side observes the acknowledgement, it may release and reallocate the exported storage while inference still holds CUDA IPC mappings. That reintroduces the stale-mapping and illegal-memory-access failure that this PR fixes in the canonical NCCLWorkerWeightsReader path. Release every IPC reference and synchronize before publishing completion, or route the update through the existing worker reader.

Comment thread awex/engine/sglang/adapter.py Outdated
gc.collect()
torch.cuda.empty_cache()

logger.info(

@chaokunyang chaokunyang Jul 21, 2026

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.

Flush SGLang caches after an in-place weight update

The parameters have been modified in place, but this path completes without calling scheduler.flush_cache(). WorkerWeightsReader.update_weights() flushes the cache after every update and verifies that it succeeded. Bypassing that step allows prefix or KV cache entries generated with the old weights to be reused under the new model version. Route the update through the worker reader instead of completing it inside this adapter.

Comment thread awex/engine/sglang/adapter.py Outdated
)

os.environ["TORCHELASTIC_USE_AGENT_STORE"] = "False"
from awex.transfer.nccl_comm import init_weights_update_group

@chaokunyang chaokunyang Jul 21, 2026

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.

Import the process-group helper from its owning module

init_weights_update_group is defined in awex.util.process_group; awex.transfer.nccl_comm does not export it. After the meta-client call is fixed, this import still raises ImportError and prevents the colocate group from being created. Prefer dispatching NCCLWorkerWeightsReader.initialize() through the generic worker-task protocol so the existing reader owns the process-group lifecycle instead of duplicating initialization here.

@dingzhiqiang

Copy link
Copy Markdown
Contributor Author

Pushed 4bc2ff3 which closes a correctness gap found while auditing the adapter's naming logic:

Problem: the colocate adapter's inline _unfuse_params only handled qkv_proj/gate_up_proj, leaving MoE expert parameters (experts.w13_weight / experts.w2_weight) in SGLang fused form. The train side reports canonical per-expert HF names (experts.<i>.{gate,up,down}_proj.weight), so on the full-transfer path expert weights matched nothing in the transfer plan and were silently skipped.

Fix: route adapter weight naming through the per-model converter registry instead of ad-hoc unfusing:

  • base SGlangToHFWeightConverter qkv split is now GQA-aware (head-count-weighted instead of equal thirds; fused paths and MHA behavior unchanged)
  • Qwen3-MoE registers an SGLang converter (unfused qkv, q_norm/k_norm support), so expert params are expanded per expert by the existing converter machinery
  • extracted the duplicated IPC deserialize/reconstruct sequence into tensor_util.reconstruct_ipc_weights, shared by the NCCL reader and the adapter

Added CPU name-contract tests (test_qwen3_moe_sglang_converter.py, 6 cases) pinning the inference/train name-set alignment, including global expert ids under EP.

@dingzhiqiang

Copy link
Copy Markdown
Contributor Author

Follow-up 1858790 on top of the converter-registry change: correct shard geometry for expanded per-expert parameters under EP.

The strategy's expert branch describes the fused layout (experts stacked along dim0, EP splitting that dim). Once experts are expanded to per-expert HF names, EP placement is already encoded in the global expert id inside the name, so re-applying EP_SHARDING in the adapter's geometry derivation would double-encode it (offset = ep_rank * dim0, global_shape = dim0 * ep_size — both wrong for a complete per-expert tensor).

Rules now applied for expanded experts.<id>.* names:

  • EP_SHARDING -> NO_SHARDING (pure EP; tensor is complete, name carries placement)
  • EP_TP_SHARDING -> kept (already means intra-expert TP: ep_tp_rank over ep_tp_size shards)
  • TP_SHARDING -> kept (ep_size == 1, experts split internally by TP)

Fused stacked names and shared_experts.* are untouched. The offset/global_shape derivation is extracted into module-level derive_shard_geometry and covered by 6 CPU tests (test_sglang_adapter_expert_meta.py).

@dingzhiqiang

Copy link
Copy Markdown
Contributor Author

E2E validation complete — the expert-expansion fix (4bc2ff3) and EP shard-geometry fix (1858790) have now been validated end-to-end with real colocate training.

Setup: Qwen3-30B-A3B (MoE, 128 experts), GSM8K PPO, 1 node, train attn:d2p1t4 | ffn:d2p1e4 (Megatron EP=4), rollout sglang d2t4p1, 5 training steps, colocate weight transfer via the SGLang plugin, with bitwise snapshot verification enabled.

Two runs, both finished cleanly (exit code 0, no errors):

Signal full transfer delta transfer
behav-vs-recompute logp_diff_abs/avg (steps 2-5) 2.0-2.1e-2 1.8-2.1e-2
bitwise snapshot verification — (baseline seed path) 32/32 OK, 0 failed, 0 false negatives
reward learning signal present 0 → 0.125 → 0.222 → 0.0625

Key points:

  • logp_diff_abs for delta matches the full-transfer baseline, i.e. the converter-routed expert expansion introduces no additional weight-transfer error beyond normal bf16 SGLang-vs-Megatron numerical noise.
  • Snapshot verification cross-checks detector masks against a resident bitwise baseline every step: zero false negatives means no changed weight was ever dropped from the transfer — this directly exercises the per-expert expanded names end-to-end (previously the fused/expanded name mismatch silently dropped all expert weights).
  • The adapter now contains no bespoke conversion code; all naming goes through the shared converter registry.

This closes the E2E gap noted in my previous comment.

Stock HF/SGLang qwen3_moe serves canonical attention names
(self_attn.{q,k,v,o}_proj + q/k_norm), so keep them verbatim instead
of the bailing-flavored renames, and split Megatron's fused
linear_qkv with GQA-aware group strides — the base equal-thirds
split only holds when q/k/v head counts match. Row-count validation
raises on any mismatch instead of mis-splitting silently.
Shape-keyed grouping degenerates to nearly one group (= one CUDA IPC
handle) per parameter when payload entries have unique lengths.
Observed on a 30B MoE model: 4888 params -> 4636 groups, and the
receiver's cudaIpcOpenMemHandle fails with a driver OOM despite
>50GB free GPU memory.

Reconstruction slices each entry by element offset/size and reshapes
from per-entry metadata, so same-shape packing was never required.
Key groups by dtype alone and flatten entries before concatenation;
group count drops to O(dtypes x size-cap) and the handle-exhaustion
failure disappears.
Address review: fail fast with clear messages when num_key_value_heads
is not divisible by attn_tp_size or num_attention_heads is not
divisible by num_key_value_heads, instead of relying on the downstream
row-count mismatch error.
The base SGlangToHFWeightConverter split fused qkv into equal thirds,
which is wrong for GQA models. The train side reports canonical
per-expert HF names, so Qwen3-MoE expert weights in SGLang fused form
matched nothing in the transfer plan.

Key changes:
- Make the base SGlangToHFWeightConverter qkv split GQA-aware
  (head-count-weighted sizes instead of equal thirds; fused paths and
  MHA behavior unchanged)
- Register a Qwen3-MoE SGLang converter that unfuses qkv and accepts
  q_norm/k_norm layer norms
- Extract reconstruct_ipc_weights into tensor_util, shared by the NCCL
  reader colocate receive path
- Add CPU name-contract tests pinning the inference/train name set
  alignment for Qwen3-MoE, including EP global expert ids

(Adapter-side changes from the original commit are dropped along with
the removed engine/sglang integration layer.)
@dingzhiqiang
dingzhiqiang force-pushed the pr/qwen3-moe-support branch from 1858790 to 2d3b774 Compare July 22, 2026 09:30
@chaokunyang
chaokunyang merged commit e8e306c into inclusionAI:main Jul 23, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants