Skip to content

[Trellis]Weight conversion that compatible with raiden - #2083

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

[Trellis]Weight conversion that compatible with raiden#2083
YixuanWang-99 wants to merge 19 commits into
mainfrom
yixuann-debug-raiden

Conversation

@YixuanWang-99

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

Copy link
Copy Markdown
Collaborator

This PR integrates Raiden weight synchronization with MaxText trainer and vLLM rollout workers for distributed RL workloads (GRPO / Trellis) in Tunix.

Companion MaxText PR: AI-Hypercomputer/maxtext#5089


Key Changes

1. MaxText Trainer Integration (run_trainer_node.py)

  • MaxText Backend Support: Added trainer_backend="maxtext" handling to the distributed RL trainer node.
  • Topology & Checkpoint Plumbing: Configures MaxText model parameters (--maxtext_model_name, --maxtext_ckpt_path, --maxtext_output_directory), mesh topologies (mesh_fsdp, mesh_tp, mesh_expert), and automatic MoE padded intermediate dimension calculation.
  • Weight Converter Integration: Automatically injects vLLM weight converter configuration (vllm.use_weight_converter=True, vllm.rollout_backend="maxtext") into the MaxText engine configuration.
  • Iteration Optimization: Supports DISABLE_CHECKPOINTING to bypass disk I/O when iterating on weight-sync performance.

2. vLLM Rollout & Sampler Adapter (vllm_sampler_adapter.py, run_rollout_node.py)

  • VllmSamplerAdapter: Bridges RLVllmSampler from tpu-inference (running in separate EngineCore subprocesses) with Tunix's WeightSyncDestination interface.
  • Metadata Wire Conversion: Reconstructs dataclass WorkUnitMetadata from the plain dicts passed across process boundaries.
  • Subprocess & Chip Isolation: Resolves --tensor_parallel_size after CLI parsing without eagerly opening JAX TPU devices in the parent process, avoiding "Device or resource busy" collisions with vLLM's EngineCore.
  • Extension Load Ordering: Loads tpu_sync native extensions prior to torch/vllm imports to prevent free(): invalid pointer crashes.
  • Name Normalization: Strips .value suffixes from parameter names across both sampler metadata and weight manifests so that Flax/NNX variable paths align 1:1 with vLLM.

3. Raiden Weight Synchronization Core (raiden_synchronizer.py, weight_sync.py, weight_sync_coordinator.py)

  • Step-0 Weight Synchronization: Pushes initial trainer weights before dispatching rollout collection on step 0 in StandardRLProgram, guaranteeing rollouts never generate tokens from uninitialized dummy weights.
  • Checksum Verification: Extends checksums() to report __tensor_count__ and __element_count__ alongside grand totals, making tensor count parity explicitly checkable against tpu-inference's RaidenWorkerSync.
  • Host Memory Management:
    • Implemented RaidenSynchronizer.release_host_arrays() to release host-side Python array references immediately after device-to-host staging (d2h()).
    • Added incremental transfer, explicit garbage collection, and malloc_trim to minimize peak host RSS.
  • Pathways / Proxy Compatibility: Added host_stage mode under proxy backends, bypassing direct native FFI transport calls when host staging is managed externally.
  • Diagnostics: Preflight validation failure messages now explicitly name the mismatched variables and print samples from both manifests.

4. Orchestrator & Runtime Robustness (distributed_rl_engine.py)

  • Event Loop Safety: Implemented _submit_worker() using a ThreadPoolExecutor to dispatch synchronous worker RPCs when an asyncio event loop is already running on the current thread, preventing RuntimeError: This event loop is already running.
  • Extended Timeouts: Increased PhaseTimeouts for bind and metadata to 180s to accommodate larger model architectures.

5. Unified Launch & Triage Tooling (launch_raiden.sh)

  • Added tunix/experimental/examples/math_gsm8k_dist/launch_raiden.sh:
    • One-command launcher supporting start, stop, restart, status, logs, and triage.
    • Built-in tested presets for qwen3-0.6b, qwen3.5-35b, and qwen3-1.7b.
    • Automated triage parser detecting conversion failures, OOMs, and hardware/driver faults.
  • Added cloud.google.com/gke-nodepool tolerations in CPU jobset definitions.
  • Tracked generated discovery service protobuf stubs (discovery_service_pb2.py, discovery_service_pb2_grpc.py).

Tests

E2E Test passed. (2×2×2 v5p trainer, 2×2×1 v5p rollout, MAX_STEPS=2)
Set time around 9/3 3:00pm

Trainer logs

Rollout logs

Rollout responses are sensible like:

[RolloutNode] [collector] traj=traj_prompt_4_g0 completion_tokens=128 prompt_tokens=157 logprobs=128 text='<reasoning>\nWill catches 16 catfish and 10 eels, giving a total of 26 fish.\nHenry challenges him to catch 3 trout for every catfish Will catches. Since Will cau'

Metrics makes sense as well:

Step 0: loss: -0.0000 | reward_mean: 0.0625 | advantage_mean: -0.0000 | perplexity: 1.0000 | step_time: 53.64s
Step 1: loss:  0.0000 | reward_mean:  0.0000 | advantage_mean:  0.0000 | perplexity: 1.0000 | step_time: 15.46s

Checklist

  • I have added all the necessary unit tests for my change.
  • I have verified that my change does not break existing code and all unit tests pass.
  • I have added all appropriate doc-strings/documentation.
  • My PR is based on the latest changes of the main branch (if unsure, rebase the code).
  • I have signed the Contributor License Agreement.
  • I have followed Contribution Guidelines.

Note: Standard CPU unit tests, package builds, and documentation checks will run automatically on pull requests. Once the PR is approved and ready for submission, maintainers will add the ready-to-submit label to trigger full TPU testing.

A9isha and others added 11 commits September 3, 2026 19:18
The distributed runtime imports discovery_service_pb2 and
discovery_service_pb2_grpc, but only the .proto was tracked. That works in an
editable install, where the generated files sit in the working tree, and fails
anywhere the package is installed from a GitHub archive -- an archive contains
only tracked files, so the stubs are absent and every worker process dies at
import:

  ImportError: cannot import name 'discovery_service_pb2' from
  'tunix.experimental.distributed.runtime.discovery' (unknown location)

Observed on all three worker pods of a GKE run. Nothing in the packaging
generates them: there is no protoc step in pyproject.toml, and the .proto is not
shipped in the wheel either, so generating at install time is not an option
without also packaging the source.

Generated with grpcio-tools 1.81.1 / protobuf 6.33.6, which stamps gencode
6.33.5. The gencode version must not exceed the protobuf runtime wherever these
execute, and the post-training image ships protobuf 6.33.6; stubs built with a
7.x toolchain load fine locally but abort there with

  VersionError: Detected incompatible Protobuf Gencode/Runtime versions ...
  gencode 7.35.1 runtime 6.33.6

Regenerate with a protobuf 6.x toolchain, from the repo root so the imports are
fully qualified.
CPU_MACHINE, TPU_SLICE and GCS_SCRATCH_LOCATION were hardcoded to what
trellis-demo-0810 happened to have, so the launcher only ran on that
cluster. mlperf-v5p has no n2-standard-64 pool and no 2x2x2 topology, and
Pathways' default compilation-cache bucket is not writable from this
project -- which kills the compilation service rather than degrading it.
All three are now ${VAR:-<previous default>}, so existing invocations are
unchanged.

The jobsets gain HF_TOKEN from a secretKeyRef. tunix/oss/utils.py calls
hf.login() whenever HF_TOKEN is unset, even for a public model, which in a
pod polls for a device code until it fails. Marked optional: true so pods
without the secret still start.

The secret name is still hardcoded to anisha-hf-token and should be
parameterised before this goes anywhere beyond the current cluster.
maxtext_engine.py's release_weight_sync() docstring already claimed to
release staged weight buffers after transfer completion, but the
implementation only logged metrics -- self.arrays (the host-staged
copy) kept lingering for the entire idle window between rounds with
nothing to actually free it.

This alone does not shrink the native transport's own hold lifetime
(BindWeights only releases the previous round's hold atomically with
acquiring the next one -- see the chunking fix in maxtext, commit
7f90d9c53, for the actual peak-memory fix), but it keeps our own
Python-side reference from outliving its purpose once d2h() has
already copied the data into the native transport's persistent
buffer, and it makes the intent of release_weight_sync() match what
its docstring already claimed. self.names is left untouched so `bound`
keeps reporting whether bind() has ever run.

(cherry picked from commit 1fc6ff9)
Ports vllm_sampler_adapter.py from mohit/raiden-maxtext-rlvllm, which
drives tpu-inference's RLVllmSampler (SAMPLER=vllm) instead of running
a vLLM engine in-process. Imports are repointed at the weight_sync
package, since the orchestrator module this branch was written against
has since been split out.

That repointing is also why dict_to_metadata comes along here: it lives
in orchestrator/weight_sync.py on the source branch and had no
counterpart under weight_sync/, so the adapter resolved its module but
not the symbol, and only failed once weight sync round 0 actually
called it. The two paths need it for different reasons -- an in-process
destination returns WorkUnitMetadata directly, but this one binds
Raiden inside a separate EngineCore process, so tpu-inference flattens
the same content to plain dicts to cross that boundary. Rebuilding the
dataclasses here lets both present one type to manifest preflight.
Four fixes, each needed before the rollout node would start under
--sampler=vllm. All were found by running it; none are speculative.

Import tpu_sync's native extension first. Loading it after vLLM/torch
pull in their own copy aborts the process with "free(): invalid
pointer" inside tpu_inference.rl.raiden_worker_sync, which imports the
same module. Bisecting the three sampler modes showed the crash is
independent of weight sync, so the guarded import goes above every
other import and is load-bearing rather than stylistic.

Resolve --tensor_parallel_size after parsing instead of defaulting it
to jax.device_count(). Evaluating that default opens the TPU in the
parent while the parser is still being built; the vllm path then hands
those same chips to a separate EngineCore process, which either cannot
reopen them ("Device or resource busy") or inherits them half-owned via
fork and hangs on its first compile. The launcher already says which
chips we own, so read that and only fall back to JAX when it is unset.

Set NEW_MODEL_DESIGN for the MaxText branch. MaxText's inference
vllm.yml declares a five-axis mesh, but tpu-inference only builds one
under that env; its default 2D ('data','model') mesh fails model
loading with "Resource axis: attn_dp ... is not found in mesh". Scoped
to this branch so the HF path keeps the 2D mesh its MoE kernel wants.

Thread MAXTEXT_MODEL_NAME from one launcher variable into both the
trainer and the rollout. Defaulting the two sides separately is how
they drift, and drift here is not a clean failure: Raiden pairs tensors
by exact name, so a MaxText trainer against a non-MaxText rollout
matches zero names.
The failures ride on WeightSyncError structurally, but the default
renderer prints only the outer message, so a mismatch reported
"preflight failed" without naming anything. Problems are name-sorted,
so a whole-convention mismatch fills the head of the list with one side
only; sample both manifests explicitly, since that is what identifies
the convention gap and the problem lines alone cannot show it.
Nothing on this path records the completion, so a run that generates
nothing but newlines reports the same "rollouts=N" summary as a real
one -- which is exactly how a broken Mode 2 rollout was mistaken for a
working one. Logs token counts and a text prefix per trajectory.

Revert once the vllm sampler path is trusted; this is a debugging aid,
not a feature.
train_stage only syncs after a step, so step-0 trajectories came from
whatever the rollout worker built for itself. On the MaxText-in-vLLM path
that is a randomly initialized model: MaxTextForCausalLM is constructed
with an empty load_parameters_path, so from_pretrained skips the Orbax
restore. The resulting degenerate rollouts were still scored and trained
on, so the run looked healthy while learning from noise.

StandardRLProgram now pushes the trainer's starting weights out before
dispatching anything, retrying until the vLLM EngineCore subprocess comes
up (~20s locally) and failing loudly at the deadline rather than rolling
out from uninitialized weights.

That sync needs the engine, which RLVllmSampler only builds lazily on the
first sample(), so VllmSamplerAdapter starts it on demand from the two
weight-sync entry points. Without that the round finds no worker, reports
an empty destination manifest, and the run deadlocks with the engine
waiting for a sample that dispatch is waiting on the sync to allow.
The source and destination grand totals are only comparable if both sides
bound the same tensors, which the checksum dict left implicit. Emitting
__tensor_count__ and __element_count__ alongside makes that checkable
rather than assumed; tpu-inference's RaidenWorkerSync.checksums() reports
the same keys.
…istributed configs

- In RaidenSynchronizer, flatten state with incremental device-to-cpu transfers, GC, and malloc_trim to minimize peak host memory
- Support flat_state attribute in transfer_state_directly for nnx state updates
- Make is_bounded check idempotent in sampler adapters
- Increase PhaseTimeouts bind and metadata limits to 180s
- Add CLI options and auto MoE padding calculation for MaxText trainer/rollout nodes
…ode.py

- Set vllm.use_weight_converter and vllm.rollout_backend in MaxText config argv in run_trainer_node.py
…depool toleration

- Add gke-nodepool tolerations in jobset.cpu.yaml
- Replace TP arguments with --tensor_parallel_size in run_rollout_node.py
- Use thread pool executor for submit_worker in DistributedRLEngine when loop is running
- Strip .value suffix from variable names in VllmSamplerAdapter, flatten_weights, and WorkUnitMetadata
- Support host_stage mode in RaidenSynchronizer under proxy backend
…th Raiden sync

- Provide one-command launcher, monitor, triage, and teardown for MaxText + Tunix + vLLM + Raiden workloads
- Support model presets (qwen3-0.6b, qwen3.5-35b, qwen3-1.7b) with verified slice topologies and checkpoints
- Include automated log triaging for weight conversion and hardware errors
@YixuanWang-99 YixuanWang-99 changed the title [DO NOT REVIEW][Trellis]Weight conversion that compatible with raiden [Trellis]Weight conversion that compatible with raiden Sep 3, 2026
…mands in launch_raiden.sh

- Truncate default RUN_ID username prefix to stay within Kubernetes 63-character label value limit for coordinator labels
- Add explicit length check rejecting TRAINER_ID > 26 characters
- Add start-trainer and start-rollout subcommands
- Handle render and dry-run commands cleanly
- Wait for JobSet deletion during workload teardown
…replica unit ID

- Thread --rollout_replicas from launch_raiden.sh to run_gsm8k_dist_grpo.py
- Wait for all rollout worker replicas in cluster.wait_for_workers()
- Set unit=server_id in VllmSamplerAdapter.get_raiden_metadata() to disambiguate replicas
- Eagerly drain completed responses in DistributedRLEngine.poll_rollouts() with non-blocking poll loop
…date test assertions

- Dynamically register custom call targets via libraiden_ffi_bridge.so in RaidenSynchronizer
- Parameterize HF_TOKEN_SECRET_NAME in yaml_generator and jobset templates with maxRestarts=3
- Deduplicate dict_to_metadata in weight_sync.py
- Update checksums test assertions in raiden_synchronizer_test.py for tensor and element counts
- Clean up orphaned batch jobs and default SYNC_CODE=true in launch_raiden.sh
…oyment configuration

- Support comma-separated multi-axis sharding specifications and monkey-patch TPUWorker in raiden_synchronizer
- Parameterize k8s namespace and improve status/log triage in launch_raiden.sh and deployment YAMLs
- Respect DISABLE_CHECKPOINTING environment variable in run_trainer_node and rl_program
- Add base_num_kv_heads configuration parameter in maxtext_utils
- Lower bind and metadata timeouts and improve shard metadata logging in weight_sync_coordinator
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.

4 participants