Skip to content

[Trellis]Qwen3.5-35B; Weight Conversion and Raiden Weight Sync Integration - #5089

Open
YixuanWang-99 wants to merge 12 commits into
mainfrom
yixuann-debug-raiden
Open

[Trellis]Qwen3.5-35B; Weight Conversion and Raiden Weight Sync Integration#5089
YixuanWang-99 wants to merge 12 commits into
mainfrom
yixuann-debug-raiden

Conversation

@YixuanWang-99

@YixuanWang-99 YixuanWang-99 commented Sep 1, 2026

Copy link
Copy Markdown
Collaborator

Description

Integrates Raiden weight synchronization into MaxTextTrainingEngine for distributed RL post-training (Trellis / GRPO). Introduces trainer-side target-free weight conversion, MoE kernel layout alignment, hybrid KV cache mapping, and aggressive host memory reclamation during device-to-host staging.

Companion Tunix PR: google/tunix#2083


Key Changes

  • Target-Free Weight Conversion (weight_converter.py, convert_utils.py): Synthesizes rollout target shapes and keys directly from trainer parameter state—unrolling scanned layers and prefusing MoE gate/up weights (wi_0 + wi_1 $\to$ wi) without requiring rollout engine target_state.
  • MoE 128-Lane Chunking & Padding (convert_utils.py, moe_padding.py): Implements 128-element lane interleaving (TPU_V5P_SUBCORE_LANE_SIZE = 128) for fused MoE weights to satisfy TPU GMM v2 kernel requirements, and dynamically pads intermediate dimensions.
  • Hybrid KV Cache Mapping (hybrid_cache_utils.py, decoders.py, nnx_decoders.py): Maps physical layer names to KV cache slot indices for hybrid architectures (e.g., Qwen 3.5 GDN recurrent + attention layers) and supports inhomogeneous layer unrolling.
  • Host Memory Reclamation & Pathways FFI (maxtext_engine.py): Adds reclaim_host_memory() (gc.collect() + malloc_trim(0)) and release_host_arrays() to eliminate host OOMs during D2H transfer; adds Pathways FFI support (RAIDEN_USE_FFI).
  • Drift Protection & Diagnostics: Adds cross_repo_drift_test.py to guard against parameter/contract drift with Tunix, and replaces silent failure fallbacks with fail-fast exceptions.

Testing

  • Unit Tests: Added cross_repo_drift_test.py, convert_utils_test.py, weight_converter_test.py, raiden_unscan_test.py, vllm_hybrid_cache_test.py, and prepare_weight_sync_test.py.
  • E2E Verification: Verified distributed GRPO training on Cloud TPU v5p (2×2×2 trainer, 2×2×1 rollout). logs

Checklist

Before submitting this PR, please make sure (put X in square brackets):

  • I have performed a self-review of my code. For an optional AI review, add the gemini-review label.
  • I have necessary comments in my code, particularly in hard-to-understand areas.
  • I have run end-to-end tests tests and provided workload links above if applicable.
  • I have made or will make corresponding changes to the doc if needed, including adding new documentation pages to the relevant Table of Contents (toctree directive) as explained in our documentation.

@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 adds support for Qwen 3.5 hybrid cycle layers in the weight unscanning and synchronization pipeline, updates Raiden synchronizer import paths, refactors vLLM integration to use MaxTextVllmSampler, and introduces warnings for replicated batch dimensions. The code review feedback highlights a critical bug where self._raiden_syncs was accidentally removed from maxtext_engine.py's initialization, causing an AttributeError. Additionally, the reviewer pointed out outdated import paths in error messages and test probes, a risky string-stripping operation (rstrip('s')) in raiden_unscan.py, a potential unhandled case for cycle-slot matching, and an unused parameter in _fuse_and_unstack_moe.

Comment on lines 340 to +341
self._signature_compare_warned: bool = False
self._raiden_syncs: Any = None
self._replicated_batch_warned: bool = False

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

critical

The initialization of self._raiden_syncs was accidentally removed from __init__ when adding self._replicated_batch_warned. This will cause an immediate AttributeError when prepare_weight_sync, release_weight_sync, or close is called. Please restore self._raiden_syncs: Any = None in __init__.

    self._signature_compare_warned: bool = False
    self._replicated_batch_warned: bool = False
    self._raiden_syncs: Any = None

Comment on lines +1176 to +1178
raise RuntimeError(
"staging_transport='raiden' requires tunix.experimental.worker."
"raiden_synchronizer, which the installed tunix does not provide. Install a"

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 error message still refers to the old module path tunix.experimental.worker.raiden_synchronizer. Since the import path was updated to tunix.experimental.weight_sync.raiden_synchronizer, please update the error message to match the new path to avoid confusion during debugging.

Suggested change
raise RuntimeError(
"staging_transport='raiden' requires tunix.experimental.worker."
"raiden_synchronizer, which the installed tunix does not provide. Install a"
raise RuntimeError(
"staging_transport='raiden' requires tunix.experimental.weight_sync."
"raiden_synchronizer, which the installed tunix does not provide. Install a"

Comment on lines +41 to +43
try:
importlib.import_module("tunix.experimental.worker.raiden_synchronizer")
_RAIDEN_AVAILABLE = True

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 test still probes the old module path tunix.experimental.worker.raiden_synchronizer to determine _RAIDEN_AVAILABLE. Since the engine now imports from tunix.experimental.weight_sync.raiden_synchronizer, this probe will evaluate to False even if the synchronizer is available at the new path, causing the test to bypass the real staging path. Please update the probe to use the new module path.

Suggested change
try:
importlib.import_module("tunix.experimental.worker.raiden_synchronizer")
_RAIDEN_AVAILABLE = True
try:
importlib.import_module("tunix.experimental.weight_sync.raiden_synchronizer")
_RAIDEN_AVAILABLE = True

# suffix and fold it into the global layer index below.
slot = None
if cycle_interval > 1 and suffix and isinstance(suffix[0], str):
match = re.fullmatch(rf"{re.escape(layer_container.rstrip('s'))}_(\d+)", suffix[0])

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

Using rstrip('s') to strip the trailing 's' from layer_container is risky because it removes all trailing 's' characters (e.g., "class" would become "cla", "loss" would become "lo"). A safer and more robust approach is to only strip a single trailing 's' if it exists.

Suggested change
match = re.fullmatch(rf"{re.escape(layer_container.rstrip('s'))}_(\d+)", suffix[0])
match = re.fullmatch(rf"{re.escape(layer_container[:-1] if layer_container.endswith('s') else layer_container)}_(\d+)", suffix[0])

Comment on lines +127 to +128
expected_axis_len = num_reps if slot is not None else num_layers
if arr.shape[scan_axis] != expected_axis_len:

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

If cycle_interval > 1 but slot is None (e.g., due to a regex mismatch or a homogeneous parameter), expected_axis_len defaults to num_layers. However, the actual scanned axis length is num_reps. This mismatch will cause a confusing ValueError claiming a shape mismatch (expecting num_layers instead of num_reps), or an out-of-bounds error during slicing. Consider raising a descriptive error directly if slot is None when cycle_interval > 1.

Suggested change
expected_axis_len = num_reps if slot is not None else num_layers
if arr.shape[scan_axis] != expected_axis_len:
if cycle_interval > 1 and slot is None:
raise ValueError(
f"unscan_layers: {'.'.join(str(k) for k in key)!r} is missing the expected cycle-slot prefix "
f"under {layer_container!r}."
)
expected_axis_len = num_reps if slot is not None else num_layers
if arr.shape[scan_axis] != expected_axis_len:

Comment on lines 533 to 534
scan_fused_axis: int,
tgt_fused_axis: int,

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 scan_fused_axis argument is no longer used in the new implementation of _fuse_and_unstack_moe. Since weight_converter.py is not part of this PR's diff, we cannot safely remove it from the signature without breaking the caller. However, please consider cleaning this up in a future refactoring of both files to remove the dead code and unused argument.

@codecov

codecov Bot commented Sep 1, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 33.33333% with 10 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
src/maxtext/training_engine/maxtext_engine.py 35.71% 6 Missing and 3 partials ⚠️
src/maxtext/training_engine/checkpointing.py 0.00% 1 Missing ⚠️

📢 Thoughts on this report? Let us know!

@YixuanWang-99 YixuanWang-99 changed the title [WIP] Debug Gibberish output after Raiden Sync [WIP] [Trellis] Weight Conversion that is compatible with Raiden Sep 2, 2026
@YixuanWang-99
YixuanWang-99 force-pushed the yixuann-debug-raiden branch 2 times, most recently from 0e43634 to 2e967ce Compare September 3, 2026 19:14
A9isha and others added 2 commits September 3, 2026 19:19
Each change targets a failure that produced no usable signal at the point of
cause: weights that never transfer, metadata that fails in another process, and
a performance cliff recorded only in a docstring.

unscan_layers had no tests, and it is the piece that decides whether trainer and
sampler tensor names agree. Both sides name tensors with jax.tree_util.keystr,
and nothing cross-checks the two sets: raiden_handler._validate_metadata only
validates one manifest's internal consistency (mesh rank, duplicate
variable/layer keys, sharding specs). A naming error therefore surfaces as
weights that silently never transfer. Two cases pin non-obvious invariants:
unscan_layers returns a plain nested dict while the sampler binds an nnx.State,
and keystr renders those identically only because the transform rewraps leaves
in nnx.Param -- dropping that rewrap would rename every tensor ("['k']" vs
"['k'].value"); and an already-unscanned state must raise, since without that
guard it would return unchanged and bind under scanned names.

prepare_weight_sync returned empty metadata on two paths: a missing
raiden_synchronizer (warning-level) and an unrecognised staging_transport (no
log at all). Neither is silent end to end -- WeightSyncCoordinator rejects an
empty side -- but the failure lands far from the cause, surfacing in another
process as "metadata collection returned an empty side", a count that never
names the missing module or the bad transport. The import case is the common one
rather than a corner: raiden_synchronizer ships only on tunix's Raiden branch,
so any released tunix takes it. Both now raise where the cause is known, with
the ImportError chained so the traceback keeps the module name. Because
staging_transport defaults to "raiden", this also reaches callers that never
asked for it, so the engine e2e test now probes for the synchronizer the way the
engine does -- exercising the real staging path where Raiden exists and the
documented failure where it does not.

_batch_data_shardings falls back to replicating the batch dimension when it does
not divide the batch axis's mesh size. That is correct -- every device along the
axis computes the whole micro-batch -- but it costs N times the work a sharded
one would do there. An invisible performance cliff is harder to notice than a
wrong number, because XLA's caching can make it look like nothing worse than a
slow run; the file already warns once per instance when a signature half cannot
be compared, and this extends that treatment. Warned once per instance rather
than per leaf, since the check runs under a tree_map over every loss input and
they normally share a batch dim. A sequence-packed micro-batch is always size 1
and has no alternative, so the message says the fallback may well be deliberate.

Verification. The unscan suite has teeth: renaming the emitted key from
layers_{i} to layer_{i} fails 5 of its 11 tests, including the name-equality
one. Marked post_training and left in tests/unit, which is already in
cpu-post-training-unit's path list, so the marker alone routes it -- tests/ and
tests/integration are not in that list, which is how the engine tests once ended
up collected by no job at all; collection confirms 11 tests in
cpu-post-training-unit and 0 in cpu-unit. The staging and sharding tests fail
without their respective changes. The sharding tests stub both the data spec and
the axis size: a single-device test mesh returns None in the batch position,
making the branch unreachable as configured, and an earlier draft asserted
`spec[0] is None` and passed without running the code under test at all.
- Support target-free key synthesis and unrolling in WeightConverter / MaxTextToMaxTextConverter for hybrid-cycle and MoE layers
- Add MoE padding utility for TPU GMM_v2 kernel alignment
- Cache staged weight sync metadata in MaxTextTrainingEngine and clean up host memory with gc and malloc_trim
- Add comprehensive TargetFreeConversionTest unit test suite
…rics recorder, and add cache invalidation

- Handle nested vllm dict/object in HyperParameters for use_weight_converter and rollout_backend
- Restore self._metrics_recorder = metrics_module.MetricsRecorder() in MaxTextTrainingEngine
- Invalidate staged metadata cache in release_weight_sync()
- Gate unroll_gemma_scanned_weights by Gemma model identity in MaxTextVllmSampler
- Set default num_lanes=128 in compute_padded_moe_mlp_dim
- Clarify memory lifecycle in WeightConverter convert docstrings and enhance test_case_5 memory profiling
…sync

- Add convert_streaming() to WeightConverter and MaxTextToMaxTextConverter for incremental transformation and eager memory release per group
- Add unscan_layers_streaming() to raiden_unscan with shared _unscan_one_key() helper
- Refactor MaxTextTrainingEngine.prepare_weight_sync() to stream piece-by-piece with unique strided worker indices
- Support RAIDEN_STREAM_PIECE_BATCH env var and deprecate RAIDEN_WEIGHT_SYNC_CHUNKS
- Add unit test coverage across weight converter, raiden unscan, and prepare weight sync suites
- Drop src_root ('base') prefix from target-free piece outputs in MaxTextToMaxTextConverter.convert_streaming()
- Remove out_root workaround in weight_converter_test.py test cases 1-4
- Remove dead code in WeightConverter.convert_streaming()
- Clean up _warned_raiden_sync_chunks check and simplify piece count mismatch validation in prepare_weight_sync()
…ase root in streaming converter

- Revert streaming piece-by-piece conversion in MaxTextTrainingEngine to single-piece convert and bind
- Restore base root prefix in MaxTextToMaxTextConverter.convert_streaming()
- Update prepare_weight_sync_test suite to reflect single sync instance
@YixuanWang-99 YixuanWang-99 changed the title [WIP] [Trellis] Weight Conversion that is compatible with Raiden [Trellis] Weight Conversion that is compatible with Raiden Sep 3, 2026
… and consolidate sync instances

- Under Pathways (JAX_PLATFORMS=proxy), require weight_synchronizer_ffi to avoid client host OOM
- Consolidate to single RaidenSynchronizer instance in MaxTextTrainingEngine
- Add reclaim_host_memory() utility invoking gc.collect() and malloc_trim(0)
- Add weight_sync_debug flag to HyperParameters config
- Update unit tests across maxtext_engine, prepare_weight_sync, and weight_converter
…ight sync

- Map layer names to KV cache indices for hybrid attention/GDN architectures in decoders and adapter
- Support cycle_interval in raiden_unscan and optimize memory during unflattening to prevent host OOM
- Honor DISABLE_CHECKPOINTING and USE_RAIDEN_FFI in MaxTextTrainingEngine
- Preserve gate and router parameter dtype during weight conversion
- Add unit tests for map_layer_names_to_indices
- Centralize tunix_compat_context and resolve_rollout_tp in convert_utils
- Streamline MaxTextVllmSampler and MaxTextVllmRollout by delegating sync lifecycle
- Support RAIDEN_USE_FFI env var and host array release in MaxTextTrainingEngine
- Enforce strict layer mapping validation in decoders and nnx_decoders
- Add convert_utils_test and cross_repo_drift_test unit tests
@YixuanWang-99 YixuanWang-99 changed the title [Trellis] Weight Conversion that is compatible with Raiden [Trellis]Qwen3.5-35B; Weight Conversion and Raiden Weight Sync Integration Sep 8, 2026
self._last_staged_step: Optional[int] = None
self._staged_metadata: Any = None
self._use_weight_converter = bool(
getattr(self._config, "use_weight_converter", False)

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.

I am always very suspicious when AI Agents add "getattr" or "hasattr", it is usually an indications that they are referencing an old implementation or hallucinating APIs that don't exist. Our config either has a use_weight_converter flag or it does not. Using getattr is error-prone in case of misspelling or flag name change.
AI agents also tend to add those statements because they are lazy to update the configs in the unit tests.

"compatible tpu_raiden_jax wheel with FFI support is installed."
)

raiden_ffi_env = os.environ.get("RAIDEN_USE_FFI")

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 do we ever want to override this? IIUC, for any reasonable large model we must use FFI or we will OOM.

job_name="trainer",
worker_index=jax.process_index(),
auto_h2d=False,
host_stage=host_stage,

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.

IIRC, "host_stage" was removed from the API in the last week. Do we have to add it back or is it a hallunication?

@YixuanWang-99

Copy link
Copy Markdown
Collaborator Author

Thanks everyone for the review feedback!

Per the feedback on PR size and scope, this PR is being split into a stacked chain of focused, reviewable PRs targeting main:

Stacked PR Chain:

  1. [Raiden Weight Sync 1/7] Add cross-repo drift guard and FFI resolution tests #5166 (M7): Cross-repo drift guard & FFI resolution matrix tests (tests/post_training/unit/cross_repo_drift_test.py)
    • Test-only drift guard verifying cross-repo interface compatibility and FFI transport resolution across platforms.
  2. [Raiden Weight Sync 2/7] Support inhomogeneous layer cycle interval in raiden_unscan #5167 (M1): Inhomogeneous layer cycle support in raiden_unscan (raiden_unscan.py + tests)
    • Supports cycle_interval in unscan_layers. Fixes slot is None with a descriptive ValueError naming the key and cycle-slot prefix when cycle_interval > 1.
  3. [Raiden Weight Sync 3/7] MoE 128-lane layout and padding for TPU GMM kernels #5168 (M2): MoE 128-lane layout and padding for TPU GMM kernels (convert_utils.py, moe_padding.py, qwen35_moe.py, model_creation_utils.py)
    • Implements 128-lane interleaving and padding matching TPU GMM layout. Removes dead scan_fused_axis parameter.
    • Pairs with Tunix Add script to convert fp4 #2150 (T5).
  4. [Raiden Weight Sync 4/7] Target-free and streaming weight conversion for vLLM #5169 (M3): Target-free streaming weight conversion for vLLM (weight_converter.py, validate_converter.py + tests)
    • Directly converts MaxText weights to target formats and streams per parameter to minimize host peak RSS.
  5. [Raiden Weight Sync 5/7] Pass weight_dtype in Qwen3NextSparseMoeBlock shared expert gate #5170 (M5): Shared expert gate weight_dtype fix (qwen3.py)
    • Explicitly passes weight_dtype=cfg.weight_dtype to Qwen3NextSparseMoeBlock.shared_expert_gate.
  6. [Raiden Weight Sync 6/7] Integrate Raiden weight sync in MaxTextTrainingEngine with FFI support #5171 (M4): TrainingEngine Raiden FFI integration (maxtext_engine.py, types.py, prepare_weight_sync_test.py)
    • Addresses review comments: Replaces getattr on config attributes with direct typed access (@igorts); defines use_raiden_ffi: Optional[bool] on config types.
    • Replaces ad-hoc env-var reading with Tunix's centralized resolve_use_ffi and normalize_host_stage.
    • Single synchronizer instance and rebind optimization.
    • Pairs with Tunix Regression: HF JSONL training halts early with #2147 (T2a).
  7. [Raiden Weight Sync 7/7] Clean up rollout path and remove redundant weight sync logic #5172 (M6): Rollout path cleanup (maxtext_vllm_rollout.py, train_rl.py)
    • Net −354 lines: deletes legacy rollout duplication superseded by M3/M4.

Notes on review comments & dropped duplicates:

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.

3 participants