Skip to content

Fix NNX MTP zero-loss, qwix MTP crash, and DeepSeek scan-axis sharding corruption#4525

Open
ecnal-cienet wants to merge 1 commit into
mainfrom
fix/nnx-mtp-and-scan-axis-sharding
Open

Fix NNX MTP zero-loss, qwix MTP crash, and DeepSeek scan-axis sharding corruption#4525
ecnal-cienet wants to merge 1 commit into
mainfrom
fix/nnx-mtp-and-scan-axis-sharding

Conversation

@ecnal-cienet

@ecnal-cienet ecnal-cienet commented Jul 17, 2026

Copy link
Copy Markdown
Collaborator

Fixes three bugs in the NNX path surfaced by running DeepSeek-V3 with multi-token prediction. They are independent; the third is not an MTP bug at all and was hiding behind the second.

1. mtp_loss is silently 0 under NNX (b/535659954)

mtp_losses / mtp_acceptance subclass nnx.Intermediate, and NNX type filters match by isinstance. So the generic nnx.pop(model, nnx.Intermediate) in loss_fn consumed them before the dedicated pop ran, which then returned {}; calculate_mtp_loss fell through to its return 0.0 default. No error, just a wrong number.

The comment claiming these variables are "not Intermediate, so the nnx.pop above misses them" was the false premise behind the bug.

Fix: pop the specific subclasses before the generic nnx.Intermediate pop. Linen is unaffected — it keys mutable collections by literal name.

2. TypeError: roll requires ndarray ... got NoneType with qwix (b/535662227)

NNX modules are stateful, so qwix.quantize_model must run a real forward pass. MaxText passed only tokens/positions/segment_ids, so the MTP block received target_ids=None and jnp.roll raised. Linen never hits this because quantize_linen_model rejects model inputs and never traces a forward.

Fix: supply dummy targets at that call site, gated on mtp_num_layers > 0, restoring the semantics of the is_initializing() guard that the NNX port left commented out in models.py.

3. DeepSeek params silently replicated under NNX (pre-existing)

Surfaced once (2) was fixed: AssertionError: Unsharded parameter percentage (2.60%) exceeds tolerance (2.00%).

_create_scanned_layers records each stack's axis name in nnx.PARTITION_NAMEdense_layers / moe_layers for DeepSeek — but _apply_layers_sequentially hardcoded the literal "layers" when round-tripping the scan axis. The name never matched, so the fallback while len(l) > ndim: l.pop(0) deleted the real leading logical axis and nnx_add_scan_axis inserted a bogus 'layers':

decoder/dense_layers/mlp/wi_0/kernel
  before: ('embed', 'dense_layers', 'mlp')
  after : ('dense_layers', 'layers', 'mlp')   <- after ONE forward pass

'embed' is the only carrier of fsdp, so the params became P(None, ...).

This is neither an MTP bug nor a qwix bug: it reproduces at mtp_num_layers=0 and with qwix entirely absent (29 of 32 params corrupted by a single plain forward). It normally stays invisible because get_abstract_state_nnx snapshots shardings before the first train step, so the damage lands on a discarded traced copy. qwix makes it fatal only because it quantizes from inside model construction, i.e. inside the traced snapshot. Standard models pass metadata_axis_name="layers", matching the literal — only DeepSeek-style multi-stack models are exposed.

Fix: resolve the axis name per-variable from nnx.PARTITION_NAME rather than a caller literal, and raise instead of blindly stripping an axis when the metadata is inconsistent. Flax's own nnx.spmd.remove_axis raises on this same mismatch; this reimplementation corrupted silently.

sharding_tolerance is untouched — the params are genuinely sharded again.

Test

python3 -m maxtext.trainers.pre_train.train src/maxtext/configs/base.yml \
  base_output_directory=/home/wanglance_google_com/tmp/out run_name=nnx_mtp \
  model_name=deepseek3-tiny per_device_batch_size=1 max_target_length=128 steps=20 \
  dataset_type=synthetic enable_checkpointing=false sparse_matmul=false \
  mtp_num_layers=1 mtp_loss_scaling_factor=0.1
  • enable_nnx=false pure_nnx_decoder=false pure_nnx=false for Linen, + use_qwix_quantization=true quantization=fp8_full for qwix.

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

Copy link
Copy Markdown

Caution

The consumer version of Gemini Code Assist on GitHub has been sunset. All code review activity has officially ceased.

@codecov

codecov Bot commented Jul 17, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 89.28571% with 3 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
src/maxtext/utils/maxtext_utils_nnx.py 80.00% 1 Missing and 2 partials ⚠️

📢 Thoughts on this report? Let us know!

@ecnal-cienet
ecnal-cienet force-pushed the fix/nnx-mtp-and-scan-axis-sharding branch from 38e9f54 to f89c21e Compare July 17, 2026 19:57
@ecnal-cienet
ecnal-cienet marked this pull request as ready for review July 17, 2026 21:25
@gemini-code-assist

Copy link
Copy Markdown

Caution

The consumer version of Gemini Code Assist on GitHub has been sunset. All code review activity has officially ceased.

Comment thread src/maxtext/utils/maxtext_utils_nnx.py
Comment thread src/maxtext/utils/maxtext_utils_nnx.py
@ecnal-cienet
ecnal-cienet force-pushed the fix/nnx-mtp-and-scan-axis-sharding branch from f89c21e to 207cb23 Compare July 18, 2026 01:20
…g corruption

Three independent bugs in the NNX path, surfaced by running DeepSeek-V3 with
multi-token prediction.

mtp_loss was silently 0. mtp_losses/mtp_acceptance subclass nnx.Intermediate and
nnx type filters match by isinstance, so the generic nnx.pop(model,
nnx.Intermediate) in loss_fn consumed them before the dedicated pop ran. That pop
then returned empty and calculate_mtp_loss fell through to its 0.0 default. Pop
the subclasses first. Linen is unaffected; it keys mutable collections by name.

qwix quantization crashed with "roll requires ndarray, got NoneType". NNX modules
are stateful, so quantize_model must run a real forward pass, but only tokens,
positions and segment ids were passed and the MTP block reads the decoder targets
unconditionally. Pass dummy targets, restoring the semantics of the
is_initializing() guard the NNX port left commented out. Linen never traces a
forward during quantization, so it never hit this.

DeepSeek params were left replicated, tripping assert_params_sufficiently_sharded
once the crash above was fixed. _create_scanned_layers records each stack's axis
name in nnx.PARTITION_NAME ("dense_layers"/"moe_layers"), but the scan-axis
round-trip hardcoded the literal "layers". The name never matched, so the fallback
stripped a real logical axis instead of the scan axis and a bogus "layers" was
inserted: ('embed','dense_layers','mlp') became ('dense_layers','layers','mlp').
'embed' is the only carrier of fsdp, hence P(None, ...). This is neither an MTP
nor a qwix bug: it corrupts on every forward for multi-stack models, but is
normally invisible because the sharding snapshot is taken before the first train
step. qwix makes it fatal only because it quantizes inside model construction,
within the traced snapshot. Resolve the axis name per-variable from
nnx.PARTITION_NAME, and raise on inconsistent metadata rather than blindly
stripping an axis, as flax's own nnx.spmd.remove_axis does.

Verified on deepseek3-tiny (v6e-8) with sharding_tolerance untouched: nnx+mtp and
nnx+mtp+qwix now both report mtp_loss 1.224, matching the Linen reference. The
sharding metadata round-trip goes from 29 of 32 params corrupted to 0. gpt3-52k,
gpt3-52k+qwix, gemma2-2b and deepseek3-tiny with scan_layers=false all train clean.

Add regression tests for all three. multi_token_prediction_test.py drives the real
loss_fn and asserts a non-zero MTP loss; quantizations_nnx_mtp_test.py quantizes an
MTP model through qwix without crashing; maxtext_utils_nnx_test.py checks the
scan-axis helpers keep the real logical axis. Each fails on the pre-fix code.
@ecnal-cienet
ecnal-cienet force-pushed the fix/nnx-mtp-and-scan-axis-sharding branch from 207cb23 to b347f81 Compare July 18, 2026 02:25
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