[quantization] Add prefill verification step to StaticGemma4Runtime#840
Open
dvsav wants to merge 1 commit into
Open
[quantization] Add prefill verification step to StaticGemma4Runtime#840dvsav wants to merge 1 commit into
dvsav wants to merge 1 commit into
Conversation
dvsav
force-pushed
the
prefill
branch
4 times, most recently
from
July 24, 2026 08:31
25e8847 to
72bc4b8
Compare
Compare runtime prefill logits against HF reference. Co-authored-by: Cline TICO-DCO-1.0-Signed-off-by: d.savchenkov <d.savchenkov@partner.samsung.com>
mhs4670go
reviewed
Jul 25, 2026
Comment on lines
+1270
to
+1277
| ref_out = runtime.model( | ||
| input_ids=raw_input_ids, | ||
| pixel_values=pixel_values, | ||
| image_position_ids=image_position_ids, | ||
| attention_mask=ref_attention_mask, | ||
| return_dict=True, | ||
| ) | ||
| ref_logits = ref_out.logits[:, -1, :] # (1, vocab_size) |
Contributor
There was a problem hiding this comment.
The reference forward computes logits for every input position and slices the final position afterward. With the default max_seq=2048 and a 262,144-token vocabulary, this can allocate 1 GiB in BF16 or 2 GiB in FP32 just for the logits tensor. Please pass logits_to_keep=1 so the LM head runs only for the final token. use_cache=False should also be set because this verification does not consume the reference KV cache.
Suggested change
| ref_out = runtime.model( | |
| input_ids=raw_input_ids, | |
| pixel_values=pixel_values, | |
| image_position_ids=image_position_ids, | |
| attention_mask=ref_attention_mask, | |
| return_dict=True, | |
| ) | |
| ref_logits = ref_out.logits[:, -1, :] # (1, vocab_size) | |
| ref_out = runtime.model( | |
| input_ids=raw_input_ids, | |
| pixel_values=pixel_values, | |
| image_position_ids=image_position_ids, | |
| attention_mask=ref_attention_mask, | |
| logits_to_keep=1, | |
| use_cache=False, | |
| return_dict=True, | |
| ) | |
| ref_logits = ref_out.logits[:, -1, :] |
Comment on lines
+1236
to
+1242
| # Apply final_logit_softcapping to match HF (the LMHead adapter does not) | ||
| final_logit_softcapping = getattr( | ||
| runtime.text_config, "final_logit_softcapping", None | ||
| ) | ||
| if final_logit_softcapping is not None: | ||
| sc = float(final_logit_softcapping) | ||
| rt_logits = torch.tanh(rt_logits / sc) * sc |
Contributor
There was a problem hiding this comment.
I think the runtime might as well does this instead. How about like this?
- Introduce a shared helper.
def _apply_final_logit_softcapping(
self, logits: torch.Tensor
) -> torch.Tensor:
"""Apply Gemma4 final logit softcapping."""
softcap = getattr(
self.text_config,
"final_logit_softcapping",
None,
)
if softcap is None:
return logits
softcap = float(softcap)
return torch.tanh(logits / softcap) * softcap- Call the helper at the end of the prefill/decode function.
# def prefill
# ..
logits = self.lm_head(
hidden_states[:, self.past_len - 1 : self.past_len, :]
)
logits = logits[:, -1, :]
return self._apply_final_logit_softcapping(logits)
# def decode_one
# ..
self.past_len += 1
logits = self.lm_head(hidden_states)[:, -1, :]
return self._apply_final_logit_softcapping(logits)- Remove the same logic from the verifier.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Related issue: #768
Related PR: #822
What
Implements the prefill verification step (Step 6) in the
StaticGemma4Runtimefor Gemma4 E2B. The runtime'sprefillmethod now correctly handles PLE (Per-Layer Embeddings), per-layer-type KV cache head dimensions, and shared-KV bookkeeping. A newverify_step_prefillfunction performs side-by-side validation of runtime prefill logits against the HuggingFace FP reference, and Step 6 is wired into therun_static_gemma4_runtimerunner.Why
The static runtime's
prefillpath had four correctness gaps that prevented it from producing valid logits:hidden_size_per_layer_input > 0, each decoder layer requires a per-layer input tensor. The runtime was not computing or passing these, causing a runtime error.head_dim, full_attention =global_head_dim). The cache allocator used a singlehead_dimfor all layers, causing a tensor shape mismatch.layer_type. The runtime did not maintainshared_kv_statesor passshared_key_valueto consumer layers, causing a runtime error.pixel_valueswas not cast to the model's dtype, causing a dtype mismatch in the vision tower.Without these fixes, the prefill path could not execute, blocking all downstream steps (decode, generation).
Key Design Decisions
_allocate_empty_cachenow inspectslayer_types[layer_idx]and selectshead_dimfromconfig.head_dim(sliding) orconfig.global_head_dim(full_attention). It also handles the general case wherenum_global_key_value_headsmay differ, though for E2B this defaults tonum_key_value_heads.get_per_layer_inputsandproject_per_layer_inputsare called once before the layer loop to produce a(B, S, L, P)tensor. Each layer receives its sliceper_layer_inputs[:, :, layer_idx, :].shared_kv_states: dict[str, tuple[Tensor, Tensor]]is maintained during the layer loop. Store layers (withstore_full_length_kv=True) write their(new_k, new_v). Consumer layers (withis_kv_shared_layer=True) read from the dict and receiveshared_key_value.copy_: For prefill,new_k/new_vcover the entiremax_seq, so the full tensor is copied into the pre-allocated fixed cache viacopy_()— no positional slicing is needed.final_logit_softcappingapplied in verification, not in the adapter: TheGemma4LMHeadExportAdapterdoes not applytanh(logits / sc) * sc. Theverify_step_prefillfunction applies it post-hoc to match HF's output, keeping the adapter clean for NPU export.compute_peir(ref_f, rt_f)— i.e.,base= FP reference,target= quantized runtime — so the interval denominator is independent of quantization error.build_static_inputscaches the raw (unpadded) processor output inbatch["_raw_inputs"]so thatverify_step_prefillcan reuse it without re-running the processor (which involves expensive image preprocessing). A fallback re-runs the processor if the cache is not available.Changes
tico/quantization/recipes/debug/static_gemma4_runtime.py(+236/-19):__init__: Storeself.text_modelreference (fromwrapped_model.language_model.wrapped) for PLE computation._allocate_empty_cache: Per-layer-typehead_dimandnum_kv_headsselection based onlayer_types[layer_idx].build_static_inputs: Cache raw processor output inbatch["_raw_inputs"]; return type updated todict[str, Any].prefill:pixel_valuestoself.model.dtype.get_per_layer_inputs+project_per_layer_inputswhenhidden_size_per_layer_input > 0.shared_kv_statesdict; passper_layer_inputandshared_key_valueto each layer.copy_(); store full-length K/V from store layers intoshared_kv_states.verify_step_prefill(new function): Runs runtimeprefill, appliesfinal_logit_softcapping, re-runs HFGemma4ForConditionalGeneration.forwardon unpadded inputs (using cached raw processor output), and compares logits via PEIR / mean|diff| / max|diff| / argmax match.run_static_gemma4_runtime: Wired Step 6 (verify_step_prefill) into the runner; updated docstring to reflect that prefill is now implemented.test/quantization/recipes/integration/test_static_gemma4_runtime_helpers.py(+165):New
TestAllocateEmptyCachetest class with 4 test cases:test_per_layer_type_head_dim: Mixed sliding/full layers — verifies correcthead_dimper layer type.test_all_sliding_layers: All sliding → all caches usehead_dim.test_all_full_attention_layers: All full_attention → all caches useglobal_head_dim.test_no_global_head_dim_falls_back:global_head_dim=None→ all layers usehead_dim.Tests use
SimpleNamespacemocks to test_allocate_empty_cachein isolation without requiring a full model.Tests
All 15 existing helper tests in
test/quantization/recipes/integration/test_static_gemma4_runtime_helpers.pycontinue to pass (11 original + 4 newTestAllocateEmptyCachetests).Example Script
$ python -m pdb ./tico/quantization/examples/inspector.py \ --mode static-gemma4-runtime \ --config ./tico/quantization/examples/configs/static_gemma4_runtime.yaml [run_static_gemma4_runtime] Loading model: google/gemma-4-e2b-it Warning: You are sending unauthenticated requests to the HF Hub. Please set a HF_TOKEN to enable higher rate limits and faster downloads. Loading weights: 100%|████████████████████████████████████████████████████████████████████████████████████████| 1951/1951 [00:00<00:00, 4241.73it/s] [run_static_gemma4_runtime] Creating StaticGemma4Runtime ... [run_static_gemma4_runtime] Step 1: verify build_static_inputs [verify_step_build_static_inputs] All checks passed. [run_static_gemma4_runtime] Step 2: verify token_embedding [verify_step_token_embedding] All checks passed. [run_static_gemma4_runtime] Step 3: verify vision_prefill [verify_step_vision_prefill] PEIR = 0.714285 % [verify_step_vision_prefill] All checks passed. [run_static_gemma4_runtime] Step 4: verify mm_fusion [verify_step_mm_fusion] All checks passed. [run_static_gemma4_runtime] Step 5: verify masks_and_rope [verify_step_masks_and_rope] RoPE exact match for 2 layer types. [verify_step_masks_and_rope] Mask patterns match for 2 layer types. [verify_step_masks_and_rope] All checks passed. [run_static_gemma4_runtime] Step 6: verify prefill [verify_step_prefill] Runtime logits shape: (1, 262144) [verify_step_prefill] Reference logits shape: (1, 262144) [verify_step_prefill] mean|diff| = 0.05616927 [verify_step_prefill] max|diff| = 0.50000000 [verify_step_prefill] PEIR = 2.285714 % [verify_step_prefill] Runtime argmax token: 106 [verify_step_prefill] Reference argmax token: 106 [verify_step_prefill] Argmax tokens MATCH. [verify_step_prefill] All checks passed. [run_static_gemma4_runtime] SKIP: decode (not implemented in this PR) [run_static_gemma4_runtime] SKIP: generation (not implemented in this PR) [run_static_gemma4_runtime] Done.