Gemma-4 vision: correct std_bias/std_scale classification per HF standardize (fidelity fix) - #4807
Conversation
There was a problem hiding this comment.
Code Review
This pull request introduces a new configuration parameter standardize_for_vit to control whether the Gemma-4 vision encoder standardizes exit embeddings with learned std_bias and std_scale parameters. When disabled (the default), these parameters are not created, preventing them from incorrectly entering the gradient filter as trainable parameters. Unit tests have been added to verify this behavior. I have no additional feedback to provide.
|
Note on the checkpoint-conversion path (for reviewers):
This PR is intentionally scoped to the model-side guard + test; it does not modify the (currently dead) conversion hooks. |
|
Additional validation — backward pass confirmed on real TPU (single-host tpu7x): Beyond the CPU result, the same R2-fixed trainer was run on a real TPU (tpu7x-standard-1t, single host, batch 1) restoring the real Gemma-4 E2B checkpoint via the production full-state path: Finite, decreasing loss and a saved θ1 (with populated |
…dardize (fidelity fix) The Gemma-4 vision encoder unconditionally constructed `std_bias`/`std_scale` as trainable `nnx.Param` and always applied the standardize op. This diverges from HF `modeling_gemma4`, which registers them via `register_buffer` (non-trainable) and ONLY when `vision_config.standardize=True`. Per-variant HF truth: 26B/31B ship standardize=True (2 std tensors); E2B/E4B ship standardize=False (0 std tensors). The old code (a) fabricated two identity trainable leaves for E2B/E4B that are absent from the HF model/checkpoint and wrongly enter the `nnx.Param` gradient filter, and (b) modeled the 26B/31B state as trainable rather than as non-trainable buffers. Fix: - Explicit, fail-closed config contract: `standardize_for_vit: bool | None = None`; a model_validator hard-fails any `gemma4-*` model with it unset (a per-family semantic distinction must not inherit a global default). All four Gemma-4 YAMLs set it explicitly (E2B/E4B=false, 26B/31B=true). - Correct state: standardize=false constructs no std state (exact identity); standardize=true constructs `std_bias`/`std_scale` as `VisionStdVar` (a plain `nnx.Variable`) — checkpoint-resident and Orbax-restored, but non-trainable (excluded from optimizer/weight-decay/grad, which key on `nnx.Param`), matching HF `register_buffer`. Reuses MaxText's existing non-trainable-buffer idiom (MoEBiasVar/Tid2EidVar). Tests (real model configs): E2B has no std state anywhere; 26B has exactly two VisionStdVar leaves and zero in `nnx.Param`; all four configs resolve to their HF truth. Also verified the optimizer excludes std, Orbax round-trip preserves values, and the standardize op transforms output exactly. 26B/31B behavior is preserved (std constructed + used + saved), only its Variable type is corrected. Signed-off-by: Loki Chen <dengcchi@meta.com>
d4c3dd1 to
3f313d1
Compare
|
Hardening update (force-pushed with
Correction on the earlier TPU comment: there is no TPU-side pre/post 448-bound byte receipt. CPU byte-proves the 448 clip-bound immutability (pre/post value-hash identical); TPU independently proves backward + optimizer-apply + θ1 save. The PR body reflects this. Still draft; not marking ready-for-review pending human owner decision. |
Summary
Correct the classification and construction of the Gemma-4 vision standardization state (
std_bias/std_scale) so it matches HuggingFacemodeling_gemma4per model variant, and make the contract explicit and fail-closed for every Gemma-4 config.This is a standalone fidelity fix. It does not add or change the multimodal feature itself (that is #4790), and it does not fix the separate params-only checkpoint-loader issue (tracked separately, see Non-goals).
Exact model variants affected
vision_config.standardizestd_bias,std_scale)(Measured from
checkpoint_conversion/utils/hf_model_configs.py, which mirrors the HF configs: 26B/31Bstandardize=True, E2B/E4Bstandardize=False.)The bug
The vision encoder unconditionally constructed
std_bias/std_scaleas trainablennx.Paramleaves and always applied the standardize op. For E2B/E4B (standardize=False) this fabricates two identitynnx.Paramleaves that are absent from the HF model and its checkpoint, and that wrongly enter thennx.Paramgradient filter. For 26B/31B (standardize=True) the state existed but was modeled as trainable, whereas HF registers it viaregister_buffer(non-trainable).Separately, the previous version of this field defaulted to
Falseglobally. Since a per-model-family semantic distinction must not inherit a global default, a 26B/31B config that failed to set the field would silently drop its std buffers.The fix
standardize_for_vit: bool | None = None. Amodel_validatorhard-fails anygemma4-*model whosestandardize_for_vitis unset. All four Gemma-4 model YAMLs set it explicitly (E2B/E4Bfalse, 26B/31Btrue).standardize_for_vit=False: construct no std state; the standardize op is an exact identity.standardize_for_vit=True: constructstd_bias/std_scaleasVisionStdVar— a plainnnx.Variable(notnnx.Param). It is checkpoint-resident and restored by Orbax, but non-trainable: excluded from the optimizer, weight decay, and gradient filter (which all key onnnx.Param), matching HF'sregister_buffersemantics. This reuses the existing MaxText idiom for non-trainable buffers (MoEBiasVar/Tid2EidVarinlayers/moe.py).26B/31B compatibility
standardize_for_vit=true; the std state is still constructed, used in the forward standardize op, and saved/restored — only its Variable type changes fromnnx.Paramto a non-trainablennx.Variable, which is the HF-faithful classification.Tests
tests/unit/gemma4_vision_standardize_test.py(real model configs):standardize=false) → no std state anywhere (Param or Variable), no attribute.standardize=true) → exactly twoVisionStdVarleaves, zero innnx.Param.standardize_for_vitto their HF truth from the real YAMLs.Additional verified (state semantics, §A2): optimizer built with
wrt=nnx.Paramcontains zero std leaves in its opt_state; Orbax round-trip preservesstd_bias/std_scaleexactly and keeps them asVisionStdVar;(x - std_bias) * std_scalematches expected within 1e-6.Composite integration validation (backprop evidence provenance)
The end-to-end backward/optimizer-apply evidence was produced by the composite stack — not by checking out this PR alone:
750460d3…); 751/751 non-clip trainable leaves changed.opt_state(proves gradients flowed + optimizer applied); trainer exit 0; no array deletion.Clip-immutability provenance: CPU byte-proves the 448-bound immutability (pre/post value-hash identical). TPU independently proves backward, optimizer apply, and θ1 save. (A TPU-side pre/post 448-bound byte receipt is not asserted here.)
Scope / non-goals
load_parameters_pathparams-only checkpoint issue (a converted params-only checkpoint currently drops a 69-leaf vision tail on restore; the E2E evidence used a full-state remap/splice bridge). That is a distinct checkpoint-loader issue tracked separately; the remap/splice bridge is evidence-only, not a proposed upstream loader path.value_and_grad" symptom seen in earlier debugging was an artifact of extracting/donating state from a live committed Orbax-restored model inside the transform (eagervalue_and_grad/nnx.jit/nnx.split); the productiontrain.pydoes not follow that pattern. It is not the claim of this PR; the claim is the std-state fidelity correction above.