Skip to content

feat(maxtext): Maxtext & MaxDiffusion refactor - #925

Merged
wenxie-amd merged 52 commits into
mainfrom
jax-maxtext-v26.6
Aug 21, 2026
Merged

feat(maxtext): Maxtext & MaxDiffusion refactor#925
wenxie-amd merged 52 commits into
mainfrom
jax-maxtext-v26.6

Conversation

@yeandy

@yeandy yeandy commented Jul 27, 2026

Copy link
Copy Markdown
Contributor

Adds per-precision MaxText experiment configs (fp8, nanoo_fp8) derived from the existing bf16 configs by setting quantization in overrides, for 9 models across MI300X and MI355X. Upstream MaxText previously shipped only bf16 (<model>-pretrain.yaml); this fills the fp8/nanoo_fp8 gap so quantized runs can be launched by config path.

Adds per-precision MaxText experiment configs (fp8, nanoo_fp8) derived from the
existing bf16 configs by setting `quantization` in overrides, for 9 models across
MI300X and MI355X. Upstream MaxText previously shipped only bf16 (`<model>-pretrain.yaml`);
this fills the fp8/nanoo_fp8 gap so quantized runs can be launched by config path.
yeandy and others added 11 commits July 27, 2026 17:23
…MI355X fp8)

MI300X uses NANOO FP8 and MI355X uses FP8, so drop the unused plain-fp8 configs on
MI300X and the unused nanoo_fp8 configs on MI355X. Each device keeps bf16 plus its
supported quantization.
Adds the fp8 quantized variant for Llama 3.1 405B on MI355X (bf16 already existed);
MI355X uses FP8.
…mem_fraction 0.96

Add a single, launcher-agnostic hook for per-config environment overrides:
TrainRuntime._apply_config_env() reads a top-level `env:` mapping from the
experiment YAML and exports it to os.environ right after config load, before
runtime env / distributed init / any `import jax`. The block lives outside
`modules:`, so it is never swept into module params nor forwarded to MaxText.
Per-config `env:` wins over image-baked defaults.

MaxTextAdapter.prepare_backend() applies arch defaults via os.environ.setdefault
(gfx950 -> RCCL_WARP_SPEED_AUTO=0) so they never override a per-config `env:`.

Precedence (highest wins): per-config env: > arch defaults > image bake.

Use it to run mixtral_8x7B at per_device_batch_size=11 on a single MI355X:
set XLA_PYTHON_CLIENT_MEM_FRACTION=0.96 (image default 0.9 is too low; bs11
peaks ~270-274 GB / 288 GB) and moe_dispatch_no_expert_sharding=true. Applied
to both the bf16 (mixtral_8x7B-pretrain) and fp8 (mixtral_8x7B-fp8-pretrain)
MI355X configs.
Run Google's MaxDiffusion WAN/FLUX trainers through primus/cli, mirroring the
MaxText backend so JAX diffusion follows the same launch pattern (distinct from
the in-tree PyTorch `diffusion` backend).

- primus/backends/maxdiffusion: adapter, argument_builder, pretrain trainer
  (pyconfig.initialize -> WanTrainer/FluxTrainer inside transformer_engine_context)
- primus/configs/models|modules/maxdiffusion: native maxdiffusion configs as
  model presets + module preset
- examples/maxdiffusion: MI300X + MI355X wrapper configs (top-level env:) and
  prepare.py hook
- examples/run_pretrain.sh: generalize the JAX-launch checks (MaxText ->
  MaxText | MaxDiffusion) so JAX backends launch without torchrun
…pec (Option C)

Make Primus the single source of truth for all MaxText/JAX environment. A
declarative registry (env_spec.py) is applied by a reusable mechanism
(env_registry.py) through an opt-in BackendAdapter hook, replacing env exports
scattered across run_pretrain.sh, the maxtext prepare hook, and ad-hoc adapter logic.

- env_registry.py (new): EnvVar dataclass, gpu arch detection (gfx950/gfx942),
  and XLA_FLAGS merge where managed knobs win over image-baked flags (e.g.
  xla_gpu_autotune_level, which fixes the fp8 MoE NaN) while preserving unmanaged ones.
- env_spec.py (new): MaxText spec incl. arch-gated RCCL_WARP_SPEED_AUTO (gfx950)
  and HSA_NO_SCRATCH_RECLAIM (gfx942), plus multi-node JAX coordinator derivation.
- backend_adapter.py: opt-in env_defaults() (empty by default, so other backends
  are unchanged) and apply_env_defaults(), called in prepare_backend before run_setup.
- maxtext_adapter.py: supplies maxtext_env_defaults(); drops the old prepare_backend override.
- run_pretrain.sh / maxtext prepare hook: stop emitting perf/arch env; Primus owns it.

Validated across run_pretrain.sh, primus-cli, and MAD: 18/18 models on both the
rock ci and rocm/jax-training:maxtext-v26.5 images.
Run Google's MaxDiffusion WAN/FLUX trainers through primus/cli, mirroring the
MaxText backend so JAX diffusion follows the same launch pattern (distinct from
the in-tree PyTorch `diffusion` backend).

- primus/backends/maxdiffusion: adapter, argument_builder, pretrain trainer
  (pyconfig.initialize -> WanTrainer/FluxTrainer inside transformer_engine_context)
- primus/configs/models|modules/maxdiffusion: native maxdiffusion configs as
  model presets + module preset
- examples/maxdiffusion: MI300X + MI355X wrapper configs (top-level env:) and
  prepare.py hook
- examples/run_pretrain.sh: generalize the JAX-launch checks (MaxText ->
  MaxText | MaxDiffusion) so JAX backends launch without torchrun
…ext Option C)

Bring the MaxDiffusion backend in line with the jax-maxtext-v26.6 env refactor:
replace the ad-hoc prepare_backend override / _apply_arch_env_defaults /
_is_gfx950 rocminfo logic with the shared env_registry mechanism.

- env_spec.py (new): declarative maxdiffusion_env_defaults() owning the single
  arch-gated knob (RCCL_WARP_SPEED_AUTO=0 on gfx950). All other JAX/XLA/NVTE
  tuning stays in each wrapper config's top-level env: block.
- maxdiffusion_adapter.py: implement env_defaults() (applied by the base adapter
  before JAX/XLA import); drop the custom prepare_backend + arch-detection code.
…every step

The MI355X wan2.1_1.3b / wan2.1_14b / flux_dev configs were missing the ROCm/HIP
+ RCCL correctness vars that the verified-good base_wan_*/base_flux_dev env
scripts set (NCCL_PROTO=Simple, HSA_NO_SCRATCH_RECLAIM=1, GPU_MAX_HW_QUEUES=2,
HIP_FORCE_DEV_KERNARG=1, RCCL_MSCCL_ENABLE=0, NCCL_MAX_NCHANNELS=112,
NCCL_IB_TC/SL, NCCL_DEBUG, LIBTPU_INIT_ARGS, JAX/MIOpen cache dirs). Dropping
them caused an RCCL clique-init hang (GPUs 0%, CPU busy-wait) during model
construction. Restoring the full 30-var env block lets wan2.1_1.3b / wan2.1_14b
train to completion on gfx950 (exclusive-node allocation still required to avoid
GPU co-tenancy RCCL deadlock).

Also set log_period: 1 so per-step throughput lines are emitted at the 20-step
benchmark length (image default log_period=100 logged nothing, leaving the perf
CSV empty). env_spec.py now returns [] so the per-config env: block is the
single source of truth and never injects RCCL_WARP_SPEED_AUTO, which the
known-good baseline does not set.
…f capture

Bind each MI355X config's metrics_file to ${PERF_METRICS_FILE} (set by
scripts/jax-maxdiffusion/run.sh). MaxDiffusion's per-step stdout line
("completed step: ..., TFLOP/s/device: ...") does not survive the Primus
launcher's stdout handling, so the perf CSV came back empty. With metrics_file
set, the trainer writes per-step JSON (perf/step_time_seconds,
perf/per_device_tflops_per_sec, ...) via max_utils.write_metrics_locally, which
the extractor parses instead. Empty when PERF_METRICS_FILE is unset (no-op).
The MI355X WAN/FLUX runs OOM'd on the first step trying to allocate a
~221 GiB attention matrix, even though the effective config (attention:
cudnn_flash_te, ici_fsdp=8/context=1, 720x1280x85, all NVTE_* + XLA_FLAGS)
was byte-identical to the known-good standalone launch.

Root cause: MaxDiffusionPretrainTrainer.run() imported the trainer class
(which pulls in the TransformerEngine attention layers) BEFORE entering
transformer_engine_context(). That context installs TE's global_shard_guard
MeshResource (fsdp_resource="fsdp", cp_resource="context", ...), which is
what shards the attention across the mesh. Upstream train_wan.py /
train_flux.py import the trainer INSIDE the guard (train() does the import
within `with transformer_engine_context()`). Importing it outside left the
attention unsharded, so TE's CK fused-attention kernel was unavailable for
the full per-device sequence and fell back to a materialized S^2 matmul.

Move the trainer import inside the guard (parity with upstream). Verified
on wan2.1_1.3b: no OOM, 19 steps, 4.47s/step, 913.8 TFLOP/s/gpu.

Also point JAX_COMPILATION_CACHE_DIR/MIOPEN_CUSTOM_CACHE_DIR at the
bind-mounted /myworkspace/.jax_cache (NFS) instead of the ephemeral
in-container /app/.cache, so the ~50-min cold compile (autotune_level=5 on
the long-sequence fused-attention graph) is paid once and reused across runs
instead of recompiling every run and risking a launcher timeout.
origin/jax-maxdiffusion-v26.6 (dcf9725) was an earlier standalone push of
the MaxDiffusion backend, based directly on jax-maxtext-v26.6 without the
declarative "Option C" env refactor. This local branch supersedes it: it
carries the Option C env spec, the full WAN/FLUX backend, per-step metrics
capture, and the TE mesh-guard OOM fix -- verified end-to-end on MI355X
(wan2.1_1.3b 913.8 TFLOP/s/gpu, wan2.1_14b ~795, flux_dev 760.8).

Reconcile with `-s ours` so the branch fast-forwards cleanly onto origin
while keeping this verified tree intact (origin's diverged content remains
recoverable from history / the bak/ tag).
Comment thread primus/backends/maxtext/env_spec.py Fixed
Comment thread primus/backends/maxdiffusion/maxdiffusion_pretrain_trainer.py Fixed
except Exception: # noqa: BLE001 - detection must never abort a run
pass

_ARCH_CACHE = arch
Andy Ye and others added 5 commits August 3, 2026 22:43
Add Primus model configs (gemma4_31B, gemma4_26B -> MaxText gemma4-31b/26b,
bundled tokenizer.gemma3) and MI355X + MI300X example configs (bf16 + fp8) for
pretraining on synthetic data. Discovered automatically by MAD's jax-maxtext
generator (tags: maxtext/jax/<arch>/<precision>).

Known limitation (gfx950 / jax_the_rock_ci_45156b7 + jaxlib 0.10.0): gemma4 does
not yet run on this JAX-ROCm stack -- the efficient attention kernels
(cudnn_flash_te default, and flash) crash natively during XLA compile for
gemma4's head_dim=256 + bidirectional sliding-window attention (fails even at
per_device_batch_size=1, so not a memory/batch-size issue), and dot_product
cannot compile in reasonable time at seq len 8192. Configs are committed ready
for when gemma4 attention support lands; excluded from the active MAD sweep.
The MAD v26.6 sweep confirmed gemma4 trains on maxtext release/v26.6 via
the reference dot_product attention (31B dense ~395 TFLOP/s/gpu, 26B MoE
~117 TFLOP/s/gpu, seq 4096). The fused/flash kernels still native-crash on
gemma4's head_dim=256 + bidirectional sliding-window on the gfx950
ROCm/jaxlib 0.10.0 stack.

Sets attention=dot_product, max_target_length=4096, per_device_batch_size=1
on the MI355X bf16 pretrain configs, and updates the gemma4 model-config
notes to reflect the validated status (previously marked as
not-running/excluded-from-sweep, which the v26.6 sweep disproved).
The v26.6 base.yml flipped pure_nnx_decoder false->true. The legacy
per-module linen Fp8Einsum used by the MoE quant path requires an active
Linen binding scope; under the pure-NNX decoder it is invoked unbound and
crashes at the first step. deepseek_v2_16B-fp8, mixtral_8x7B-fp8 and
qwen3_30B_A3B-fp8 all regressed vs v26.5 for this reason.

Override pure_nnx_decoder: false in the three fp8-MoE MI355X configs to
run the decoder on the bridged path (v26.5 behavior). Validated on
gfx950/MI355X with base ce580ea_20260804: deepseek ~456 TFLOP/s and
qwen3_30B_A3B ~328 TFLOP/s train cleanly; mixtral no longer crashes.
Make Primus self-contained for the MaxDiffusion (JAX) backend so it can run
from a bare checkout on the MaxText JAX base image, without the separate MAD
primus_maxdiffusion image. Its env stays isolated from MaxText's.

- third_party/maxdiffusion submodule (pin 68e06965)
- requirements-maxdiffusion.txt: MaxDiffusion-only deps (transformers==4.57.3,
  scikit-image, imageio-ffmpeg, loguru, wandb); torch/torchvision installed
  separately from the ROCm wheel index
- examples/maxdiffusion/setup_maxdiffusion_env.sh: idempotent installer that
  brings up the stack (torch + requirements + editable submodule) and applies
  the four historical patches (Flax-T5 clip rename, TF-preload-before-TE,
  Shardy-on, TE empty-CP-axis) previously baked into the MAD Dockerfile
- run_pretrain.sh: BACKEND=maxdiffusion runs the setup script (unless
  PRIMUS_SKIP_PIP=1) and exports NVTE_FRAMEWORK=jax + MAXDIFFUSION_PATH; the
  MaxText path is unchanged (guarded by _is_maxdiffusion_backend)

Also bump third_party/maxtext a7c6c7e (release/v26.5) -> 59a87dca
(release/v26.6), the validated release commit, so the branch pin matches the
release it is named for.
- Document running MaxDiffusion directly from a bare Primus checkout
  (vendored submodule + setup_maxdiffusion_env.sh via run_pretrain.sh),
  including MI355X config status, in pretraining.md and examples/README.md.
- Document the fp8-MoE requirement to set pure_nnx_decoder: false under
  the v26.6 pure-NNX decoder default (parameters ref + pretraining note).
- Document that gemma4 on gfx950 (MI355X) must use attention: dot_product
  (head_dim=256 + sliding-window has no fused/flash kernel coverage).
@yeandy yeandy changed the title feat(maxtext): add fp8 and nanoo_fp8 pretrain configs for MI300X/MI355X feat(maxtext): Maxtext & MaxDiffusion refactor + FP8 separate configs Aug 5, 2026
yeandy and others added 8 commits August 5, 2026 22:18
MaxDiffusion's GPU init (max_utils.initialize_jax_for_gpu) only calls
jax.distributed.initialize() when JAX_COORDINATOR_IP is set, keyed on
num_processes=NNODES / process_id=NODE_RANK (one process per node). The
launcher exported NNODES/NODE_RANK/MASTER_ADDR but not JAX_COORDINATOR_IP/PORT,
so a multi-node MaxDiffusion job had each node initialize as a standalone
single-node run. Export JAX_COORDINATOR_IP/PORT (from MASTER_ADDR/PORT) in the
maxdiffusion branch, mirroring the MaxText env_spec, so 2+ node runs rendezvous.
The 14B graph makes RCCL instantiate 448 collective channels, over WarpSpeed's
256-channel ceiling. RCCL warns on every rank ("WarpSpeed does not support more
than 256 channels ... To avoid hang, run with RCCL_WARP_SPEED_AUTO=0") and the
launcher dies at comm-init (exit 1) right after re-enabling GC. Set
RCCL_WARP_SPEED_AUTO=0 in this config's env block (per-config, per the
MaxDiffusion env-policy in backends/maxdiffusion/env_spec.py), mirroring the
MaxText gfx950 default. wan2.1_1.3b/flux stay under 256 channels and are
untouched.
The MI355X fp8-MoE configs were pinned in 6038de6 but their MI300X
counterparts were missed, so mixtral_8x7B-nanoo_fp8 crashed before step 1
with:

  AttributeError: "Fp8Einsum" object has no attribute "input_scale"

v26.6 flipped the base.yml default to pure_nnx_decoder: true, which runs
the decoder in pure NNX and invokes the legacy Linen Fp8Einsum (the MoE
sparse-matmul quant path) without a Linen binding scope.

Applied to all three MoE + fp8 configs on MI300X. Dense fp8 and bf16
configs are unaffected and left alone.

Co-authored-by: Cursor <cursoragent@cursor.com>
The MI300X configs set JAX_PERSISTENT_CACHE_ENABLE_XLA_CACHES=all but no
cache directory, so the cache was never initialized:

  Not writing persistent cache entry with key 'jit_train_step-...'
  since cache is disabled/not initialized

Every run therefore recompiled the whole graph. Measured on a 10-step
wan2.1_1.3b run: ~41 min of 46 min wall clock was compilation.

Paths are relative to the Primus root, which is the working directory on
every launch path and is bind-mounted in container mode, so the cache
survives the container. The MI355X configs use absolute /myworkspace
paths, which only exist in the MAD image.

Deliberately no CK_JIT_CACHE_DIR: TransformerEngine builds its CK kernels
via ck_jit_compile.sh, spawned with a working directory that is not the
Primus root, so a relative path there resolves elsewhere and the build
fails with "mktemp: ... No such file or directory", taking the run down.
Recorded as a warning comment in the 1.3B config.

Co-authored-by: Cursor <cursoragent@cursor.com>
A crashing run exited silently. init_worker_logger rebinds builtins.print
to debug_rank_all, which discarded the file= kwarg and logged everything
at DEBUG. traceback.print_exc() emits via print(..., file=sys.stderr), so
the fatal stack landed at DEBUG in debug.log while the console (INFO) and
error.log showed nothing -- error.log stayed 0 bytes on a failed run.

Affects every backend: the patch site is core runtime with no backend
branching.

Promote to ERROR only for stderr writes made while an exception is being
handled. That is precisely the traceback case and it excludes backends
that use stderr for ordinary chatter outside any handler (e.g.
fused_residual_rmsnorm logs "disabled (set PRIMUS_FUSED_RESIDUAL_NORM=1
...)" on every Megatron run), which would otherwise put a spurious ERROR
on the console of healthy runs.

Also wire up --debug, which was parsed and never read: its help promised
"verbose error output" while the console stayed pinned at the
module_base.yaml default. It now publishes PRIMUS_LOG_LEVEL=DEBUG, which
the runtime logger honours as a verbosity *floor* -- never an override,
because runner/lib/common.sh exports PRIMUS_LOG_LEVEL unconditionally as
${PRIMUS_LOG_LEVEL:-INFO}, and treating that always-present default as an
override would silently disable the stderr_sink_level / sink_level config
knobs. WARN/FATAL are aliased to loguru's WARNING/CRITICAL, and unknown
values fall back to the config instead of raising.

Verified against real sinks: the traceback reaches the console and
error.log at every level, and informational stderr stays hidden at the
default verbosity.

Co-authored-by: Cursor <cursoragent@cursor.com>
MaxDiffusion's max_logging.log is a bare print, and Primus rebinds
builtins.print to a DEBUG-level logger call. Every per-step
"completed step: N, ... loss: ..." line was therefore emitted at DEBUG
and dropped by the console sink (INFO), surviving only in debug.log --
so a healthy run looked like it produced no output at all.

MaxText already ships the equivalent patch; MaxDiffusion had no patches
package, which its own runs reported unnoticed as
"[Patch] Applied 0/0 patches for maxdiffusion/setup".

Rebinding the module attribute is sufficient because every MaxDiffusion
call site is module-qualified. log_rank_0 is used rather than MaxText's
primus_logger.info: it resolves the sink at call time instead of
capturing _logger at import, gates on rank 0, and attributes each line to
the calling trainer rather than to max_logging itself.

The rebinding happens before the confirmation is logged, so the patch
cannot be skipped by a failure in the very logging path it repairs.

Co-authored-by: Cursor <cursoragent@cursor.com>
gemma4 uses bidirectional sliding-window attention in its local layers, and
with dataset_type: synthetic the data is unpacked, which MaxText's efficient
attention path rejects outright:

  AssertionError: Sliding window attention requires context parallelism with
  load-balanced ring strategy and packing enabled.

fc4dc19 fixed this for MI355X but only touched the MI355X configs. Mirror it
here: dot_product avoids the cudnn path so the restriction never applies, and
max_target_length drops to 4096 because dot_product compilation is very heavy
at 8192.

Verified on MI300X / gfx942, 5 steps each:
  gemma4_31B (Dense)  3.22 s/step, 245 TFLOP/s/device
  gemma4_26B (MoE)    1.28 s/step, 80.4 TFLOP/s/device
(MI355X reports ~395 and ~117 on gfx950 for comparison.)

Co-authored-by: Cursor <cursoragent@cursor.com>
yeandy and others added 10 commits August 10, 2026 19:48
- gemma4_26B-fp8: max_target_length 8192 -> 4096 and set attention:
  dot_product to match the bf16 config (fused/flash native-crashes on
  gemma4 head_dim=256 + sliding-window).
- qwen3_30B_A3B (bf16 + fp8): max_target_length 8192 -> 4096, mirroring
  the MI300X seq-length change.
Derive the precision token from each config's own quantization setting rather
than its previous filename, so a name can no longer disagree with what the
config actually runs: no quantization key means bf16, "fp8" means fp8, and
"nanoo_fp8" means nanoo_fp8.

This corrects three files that were previously misdescribed. Both MI300X gemma4
configs set nanoo_fp8 while being named -fp8-, and grok1 carried no precision in
its name at all despite quantizing. The MI355X -fp8- configs keep their token
because they genuinely use OCP fp8, which is a different format from nanoo_fp8.

grok1 also switches from fp8 to nanoo_fp8, the one behavior change here; every
other config is a pure rename. Updates the 28 references in the benchmark
workflow, the maxtext trainer tests, and the docs.

Co-authored-by: Cursor <cursoragent@cursor.com>
- gemma4 (26B bf16/fp8, 31B bf16/fp8): attention dot_product ->
  cudnn_flash_te. The fused path used to abort at startup because maxtext
  routed synthetic batches through the context-parallel branch, which
  rejects AttentionType.LOCAL_SLIDING; maxtext now gives synthetic data its
  own mask-free causal branch, so the window reaches the kernel as
  window_size.
- The pin stays explicit rather than inheriting autoselected: on GPU that
  resolves to a pallas kernel that builds a plain causal mask, which would
  silently drop the window on gemma4's local layers.
- gemma4_31B (bf16): max_target_length 4096 -> 8192, which had only been
  lowered to keep dot_product's O(S^2) step tractable.
- gemma4_26B-fp8: pure_nnx_decoder false, as on the other MI355X fp8-MoE
  configs. This config still does not run, but it now reaches the same
  upstream flax tracer leak MI300X hits instead of failing in attention.
- docs: drop the claim that gemma4 must use dot_product on MI355X.

Validated on MI355X (8x gfx950) through both primus-cli and MAD, agreeing
to within 0.1%: 26B bf16 115, 31B bf16 281, 31B fp8 341 TFLOP/s/device.

Needs the matching third_party/maxtext bump, which lands separately.
Picks up the synthetic-data mask-free causal branch the MI355X gemma4 configs
depend on. On the previous pin, synthetic batches took the context-parallel
path, which rejects AttentionType.LOCAL_SLIDING, so gemma4 aborted at startup
under cudnn_flash_te.

Tree-identical to the commit the MI355X sweep validated against, so this
changes no code relative to what was tested. It also moves off 59a87dca,
which was left unreferenced when release/v26.6 was rewritten upstream.
The two arch config dirs drifted in both directions: the MI355X hardening in
91552d6/30f5648 never reached MI300X, and the MI300X collective-timeout fix in
53790aa never reached MI355X or the MI300X wan configs.

- Raise both first_collective_call timeouts in all six configs. Every config runs
  autotune_level=5, so step 1 spends >10 min in per-device autotune and the 8
  device threads reach the first kAllGather minutes apart; XLA's 20s/40s
  rendezvous defaults then abort a process where every thread is still making
  progress (SIGABRT, exit 134). Only MI300X/flux_dev carried the raised values.

- Port the 11-var ROCm/HIP + RCCL/NCCL block to the MI300X configs. Dropping five
  of these caused an RCCL init hang on MI355X (GPUs 0%, CPU busy-wait) and MI300X
  carried none of them. Nothing else supplies them: the MaxDiffusion adapter
  contributes no env (env_spec.py returns []) and MAD's run.sh exports no tuning,
  so the per-config env: block really is the only source.

- Bind metrics_file to PERF_METRICS_FILE and set log_period: 1 on MI300X. run.sh
  always exports PERF_METRICS_FILE and passes --metrics-file to the extractor,
  but only a config that binds metrics_file makes the trainer write it, and the
  per-step stdout line does not survive the Primus launcher -- so MI300X perf
  capture had no working source.

- Unify the compile-cache dirs on the relative ./.jax_cache/<model> form, which
  resolves against the Primus root (the working directory on every launch path,
  bind-mounted in container mode), instead of /myworkspace, which only exists in
  the MAD image.

RCCL_WARP_SPEED_AUTO stays MI355X-only, since WarpSpeed is gfx950. The MI300X
comments claiming run.sh applies it were stale and are corrected here.
Mirrors e29d78b, which did the same for MI355X. The assertion that forced
dot_product here ("Sliding window attention requires context parallelism with
load-balanced ring strategy and packing enabled") was a maxtext bug: synthetic
batches were routed through the context-parallelism branch, which rejects
sliding-window attention. That is fixed in the pinned release/v26.6 at 2ec83add
and the fix is not arch-specific, so both arches now take the same path --
cudnn_flash_te is the only setting that passes the local layers' sliding window
to the kernel, as window_size. The global layers' head_dim=512 is past TE's
fused limit and still falls back to unfused attention.

Validated on gfx950; not yet re-run on gfx942, which each config records.

Also rewrites the gemma4_31B-nanoo_fp8 status note. Its kAllGather rendezvous
abort after a ~10 min compile matches the first-collective-call watchdog analysis
in examples/maxdiffusion/configs/MI300X/flux_dev-pretrain.yaml, not the missing
NCCL env it blamed: maxtext already applies GPU_MAX_HW_QUEUES,
HIP_FORCE_DEV_KERNARG and HSA_FORCE_FINE_GRAIN_PCIE on every arch (plus
HSA_NO_SCRATCH_RECLAIM on gfx942) from primus/backends/maxtext/env_spec.py, so
only NCCL_PROTO and RCCL_MSCCL_ENABLE are absent there.
The pre-commit thirdparty-lock hook rewrites this file from the submodule pins,
and it was stale on two counts: maxtext still recorded a7c6c7e5 rather than the
2ec83add bumped in ba052cf, and maxdiffusion had no entry at all despite being
vendored in 963a7f6.
from pathlib import Path
from typing import Any, List, Optional

import primus.backends.maxdiffusion.patches # noqa: F401 # Register patches
@yeandy yeandy changed the title feat(maxtext): Maxtext & MaxDiffusion refactor + FP8 separate configs feat(maxtext): Maxtext & MaxDiffusion refactor Aug 12, 2026
yeandy added 15 commits August 12, 2026 16:02
The shipped batch sizes were leaving most of the card idle on 26B-bf16. Walking each
model up until it OOMs on this 288GB host gives 26B-bf16 20 (was 1), 31B-bf16 3 (was 1),
and 31B-fp8 3 (was 2); one rung higher OOMs in every case. Measured over 50 steps
through MAD:

  gemma4_26B-bf16  114.5 -> 204.5 TFLOP/s  (+78.6%)
  gemma4_31B-bf16  277.9 -> 300.9 TFLOP/s   (+8.3%)
  gemma4_31B-fp8   341.4 -> 348.5 TFLOP/s   (+2.1%)

Note this changes the recipe, not just the speed: per-device batch multiplies by the 8
devices, so 26B-bf16's global batch goes from 8 to 160. These three models also take 64
minutes instead of 36, since a step now does 20x or 3x the work.

gemma4_26B-fp8 keeps batch 2 because it does not train at any batch size (flax fp8-MoE
tracer leak, upstream). The ceilings are specific to MI355X's 288GB, so the MI300X
configs are untouched.

The comment explaining the cudnn_flash_te pin is dropped here and recorded with the
sweep results instead.
On an image that bakes maxdiffusion there is nothing to install, and often no submodule
to install from, so the script failed its source check on a perfectly good environment.
Detect an importable maxdiffusion first and exit early. PRIMUS_SKIP_SETUP_CHECK=1 forces
the install path.

PRIMUS_SKIP_PIP=1 already short-circuits this script from the launcher, so this only
matters when it is invoked directly.
# Conflicts:
#	.gitmodules
#	docs/02-user-guide/training-recipes.md
#	examples/maxtext/configs/MI300X/qwen3_30B_A3B-bf16-pretrain.yaml
#	primus/_thirdparty.lock
cudnn_flash_te is now validated; remove verbose history of the earlier
dot_product workaround. Condense STATUS blocks and model YAML headers.
Add a table in pretraining.md and a header comment in the script itself
explaining the three scenarios (image has it, image doesn't, submodule
missing).
Fp8Einsum.setup() allocates Flax variables (amax_history, scales) inside
jax.lax.scan when gemma4's scanned local layers invoke RoutedMoE.dense_matmul,
causing UnexpectedTracerError. The new patch makes get_einsum() return plain
jnp.einsum for FP8 MoE matmuls when scan_layers is true, so expert matmuls
run in bf16 while dense layers still use FP8 via dot_general_cls.

Validated on MI355X: 148.7 TFLOP/s/device, 5940 tok/s/device (gemma4_26B).
mixtral_8x7b and qwen3-30b-a3b explicitly set scan_layers: false in
their upstream model configs, so Fp8Einsum.setup() runs outside any
scan transform and doesn't leak. gemma4 inherits the default
scan_layers: true and uses its own _scan_local_layers, which is why
the patch is needed only for gemma4.
Reverts 4982d9b and 453897b. The bypass patch made MoE expert matmuls
silently fall back to bf16 while labeling the config as "fp8", which is
misleading. Mark gemma4_26B fp8 configs as DOES NOT RUN on both MI300X
and MI355X until the upstream Flax Fp8Einsum + scan tracer leak is
resolved.
The Dockerfile blanks LD_LIBRARY_PATH to prevent TE 2.17's broken
hipblaslt Tensile kernels from being loaded on gfx950. However,
ensure_rocm_ld_library_path() re-introduces _rocm_sdk_devel/lib at
runtime, undoing that fix and causing SIGSEGV during training.

Gate the call behind a BACKEND check so JAX backends (MaxText,
MaxDiffusion) keep LD_LIBRARY_PATH empty and resolve ROCm libs via
wheel RPATH as intended.
Prevents OOM on 70B models by allowing JAX to use 96% of device memory.
…vel env block

- Move XLA_PYTHON_CLIENT_MEM_FRACTION from overrides (wrong) to top-level
  env: section (.97) for llama2_70B, llama2_70B-nanoo_fp8, llama3_70B,
  llama3.3_70B MI300X configs
- Add status comment to qwen3_30B_A3B-fp8 noting possible NaN from FP8
  dynamic range overflow in MoE expert layers
@wenxie-amd
wenxie-amd merged commit 874891c into main Aug 21, 2026
9 checks passed
GeneDer added a commit that referenced this pull request Aug 22, 2026
Adds per-precision MaxText experiment configs (fp8, nanoo_fp8) derived
from the existing bf16 configs by setting `quantization` in overrides,
for 9 models across MI300X and MI355X. Upstream MaxText previously
shipped only bf16 (`<model>-pretrain.yaml`); this fills the
fp8/nanoo_fp8 gap so quantized runs can be launched by config path.

---------

Co-authored-by: Andy <14128880+yeandy@users.noreply.github.com>
Co-authored-by: Andy Ye <andyye12@crs-m2m-cpu-spur-009.us-east2-a.compute.internal>
Co-authored-by: Fuyuan Jing <Fuyuan.Jing@amd.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
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