Skip to content

gemma4: fix 16x audio soft-token magnitude on NPU via single-source scaling - #726

Merged
HenryNdubuaku merged 6 commits into
mainfrom
gemma4-npu-scale-single-source
Jun 16, 2026
Merged

HenryNdubuaku merged 6 commits into
mainfrom
gemma4-npu-scale-single-source

Conversation

@ncylich

@ncylich ncylich commented Jun 16, 2026 •

Copy link
Copy Markdown
Collaborator

Problem

a8f634a6 (#723) folded the gemma4 audio soft-token 1/16 out of the runtime graph and into the converted embed_audio_proj.weights file. Correct for the CPU path (it binds that scaled file at runtime), but silently wrong for the NPU path: the audio .mlpackage is traced directly from the in-memory torch module, which holds the original, unscaled projection weight, and the .mlpackage never reads that converted file.

gemma4 multimodal embedders have no post-projection norm (only a pre-projection one) — verified: embed_vision/embed_audio expose only embedding_projection + embedding_pre_projection_norm, and no embed_*_post_proj_norm.weights is produced. So the projection magnitude is the soft-token magnitude and nothing renormalizes it. Both audio and vision NPU soft tokens were 16x too large (vision was previously assumed safe via a post-projection RMSNorm — that norm does not exist for this family, so the assumption was wrong).

The deeper issue is structural: the ÷16 reparam lived in two disjoint copies (the canonical gemma4_scale_factor() and a hardcoded block in tensor_io.py), and a third weight consumer (NPU baked weights) consulted neither.

Fix

Make gemma4_scale_factor() the single source of truth, consumed by all three weight-emission paths.

  • convert: add the projection basenames to GEMMA4_DIV_BASENAMES and collapse the hardcoded tensor_io.py gemma4 block to call gemma4_scale_factor(). Factors are identical for every converted weight (verified over 2133 names), so stored weights are byte-for-byte unchanged.
  • transpile: reparameterize both projection weights with that same function before the NPU trace, applied as a reversible context manager so the shared module is restored before CPU IR capture. Wired in via one optional npu_reparam field on ComponentModuleSpec, keeping the family-agnostic NPU pipeline free of gemma4 specifics.

Verification

  • Static equivalence: gemma4_scale_factor matches the old hardcoded block on all 2133 real converted filenames (0 mismatches) → CPU conversion output unchanged.
  • NPU audio (.mlpackage vs torch adapter, same test.wav inputs): RMS(NPU)/RMS(torch unscaled) = 0.0626 ≈ 1/16; RMS(NPU)/RMS(torch ÷16) = 1.002 ≈ 1.0; cosine identical scaled-vs-unscaled → change is magnitude-only.
  • NPU vision (direct .mlpackage ratio, with vs without reparam): RMS ratio = 0.062500 = exactly 1/16, cosine 1.000000. Confirmed gemma4 vision has no post-projection norm (so it was 16x hot too, now matches CPU).
  • Relevant unit suites pass (test_transpile_weight_compat, test_component_partition, test_optimize_gemma4_attention).

Deploy

Convert output is unchanged, so no reconvert is required. Affected gemma4 NPU bundles must be re-transpiled (--npu) to pick up the corrected audio and vision .mlpackages.

ncylich added 2 commits June 15, 2026 14:07
The gemma4 16x weight reparameterization was encoded twice: once in
gemma4_scale_factor() and again as a hardcoded branch in
save_tensor_with_header(). The two had to be kept in lockstep by hand,
and gemma4_scale_factor() omitted the projection weights entirely.

Add the vision/audio projection basenames to GEMMA4_DIV_BASENAMES and
collapse the tensor_io branch to call gemma4_scale_factor(), so a single
function decides every gemma4 weight's factor. Factors are identical for
all converted weights (verified over 2133 names), so stored weights are
byte-for-byte unchanged.

Signed-off-by: Noah Cylich <noahcylich@gmail.com>
The NPU audio and vision encoders are traced directly from the in-memory
torch module, which holds the original projection weights, while the
soft-token 1/16 lives only in the converted embed_*_proj.weights files
that the .mlpackage never reads. gemma4 multimodal embedders have no
post-projection norm (only a pre-projection one), so the projection
magnitude is the soft-token magnitude and nothing renormalizes it: both
audio and vision soft tokens came out 16x too large on NPU.

Reparameterize the projection weights with the same gemma4_scale_factor
used at conversion time, applied as a reversible context manager around
the NPU trace so the shared module is restored before CPU IR capture.
Verified at the .mlpackage level: reparam divides both encoders' output
by exactly 1/16 (cosine 1.0), matching the CPU path.

Signed-off-by: Noah Cylich <noahcylich@gmail.com>
@ncylich
ncylich force-pushed the gemma4-npu-scale-single-source branch from 57844ec to cf453c4 Compare June 16, 2026 01:38
kar-m
kar-m previously approved these changes Jun 16, 2026

@kar-m kar-m left a comment

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.

tested pre and post PR NPU performance, audio is fixed from a bad regression, vision is just a tiny bit better post-PR. LGTM

@HenryNdubuaku

Copy link
Copy Markdown
Collaborator

@ncylich just resolve the conflicts here

…le-source

Signed-off-by: Noah Cylich <noahcylich@gmail.com>
@ncylich
ncylich force-pushed the gemma4-npu-scale-single-source branch from 7481862 to 3f88749 Compare June 16, 2026 19:27
kar-m
kar-m previously approved these changes Jun 16, 2026

@kar-m kar-m left a comment

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.

tested, lgtm

@ncylich
ncylich force-pushed the gemma4-npu-scale-single-source branch from ef7f4e1 to 91573de Compare June 16, 2026 20:06
Signed-off-by: Noah Cylich <noahcylich@gmail.com>
@ncylich
ncylich force-pushed the gemma4-npu-scale-single-source branch from 91573de to 713374e Compare June 16, 2026 20:06
kar-m
kar-m previously approved these changes Jun 16, 2026

@kar-m kar-m left a comment

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.

again looks good

Signed-off-by: Noah Cylich <noahcylich@gmail.com>
kar-m
kar-m previously approved these changes Jun 16, 2026

@kar-m kar-m left a comment

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.

approve

Signed-off-by: Noah Cylich <noahcylich@gmail.com>
@HenryNdubuaku
HenryNdubuaku merged commit f37a9a5 into main Jun 16, 2026
7 checks passed
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