Skip to content

[quantization] Implement build_static_inputs for Gemma4 static runtime#828

Merged
dvsav merged 1 commit into
Samsung:mainfrom
dvsav:build_static_inp
Jul 20, 2026
Merged

[quantization] Implement build_static_inputs for Gemma4 static runtime#828
dvsav merged 1 commit into
Samsung:mainfrom
dvsav:build_static_inp

Conversation

@dvsav

@dvsav dvsav commented Jul 16, 2026

Copy link
Copy Markdown
Contributor

Related issue: #768
Related PR: #822

What

This PR implements StaticGemma4Runtime.build_static_inputs — the first concrete piece of the Gemma4 E2B static-shape NPU inference runtime. It processes a prompt+image through the HF processor, pads to a fixed max_seq, and replaces image placeholder tokens with pad_token_id to produce llm_input_ids suitable for static NPU export. A side-by-side validation function (verify_step_build_static_inputs) re-derives each sub-step from raw HF processor output and compares against the runtime's output. The runtime entry point (run_static_gemma4_runtime) is wired to run only this validation step, with clear SKIP messages for prefill/decode/generation which are not yet implemented.

Why

The Gemma4 E2B static runtime follows the Llama static runtime pattern: CPU owns dynamic/control-heavy orchestration (tokenization, layout validation, padding, mask generation, cache management), while NPU executes fixed-shape quantized tensor-compute subgraphs. build_static_inputs is the CPU-side entry point that transforms variable-length processor output into the fixed-shape tensor contract the NPU subgraphs expect. Without correct llm_input_ids construction (especially the image placeholder replacement), downstream NPU prefill would receive image-token IDs in the embedding lookup, producing silently wrong results. This PR establishes the input-building foundation and its validation harness so that subsequent prefill/decode PRs can build on a verified input contract.

Key Design Decisions

  1. image_token_id read from self.config, not self.text_config: The image_token_id attribute lives on the top-level Gemma4Config, not on Gemma4TextConfig. Reading it from self.text_config returns None (the attribute doesn't exist there), which silently skips the image placeholder replacement — a critical bug that was caught and fixed during development.

  2. Side-by-side validation rather than golden-file comparison: verify_step_build_static_inputs re-derives the expected output from raw HF processor output at validation time, rather than comparing against pre-recorded golden tensors. This makes the validation self-contained and robust to processor version changes.

  3. Helper functions as pure module-level functions: _normalize_valid_token_mask and _validate_padding_layout are extracted as standalone functions (not methods) so they can be unit-tested without instantiating a full StaticGemma4Runtime (which requires a real model and processor).

  4. padding_side accessed defensively via hasattr: StaticGemma4Layout does not currently have a padding_side field. The code uses hasattr(self.layout, "padding_side") with a "right" fallback, so the layout dataclass doesn't need modification in this PR.

  5. Validation-only entry point: run_static_gemma4_runtime runs only build_static_inputs validation and prints explicit SKIP messages for prefill, decode, and generation. This keeps the PR focused and reviewable while establishing the validation harness for future PRs.

Changes

  • tico/quantization/recipes/debug/static_gemma4_runtime.py — Replaced build_static_inputs stub with full implementation (processor call, padding, image token replacement, valid_token_mask normalization, padding layout validation). Added _normalize_valid_token_mask and _validate_padding_layout module-level helper functions. Added verify_step_build_static_inputs side-by-side validation function (6 sub-step checks). Replaced run_static_gemma4_runtime stub with validation-only entry point that loads model/processor, creates runtime, runs validation, and prints SKIP messages for prefill/decode/generation.
  • tico/quantization/examples/inspector.py — Added static-gemma4-runtime mode to the inspector CLI, importing and dispatching to run_static_gemma4_runtime / StaticGemma4RuntimeConfig.
  • tico/quantization/examples/configs/static_gemma4_runtime.yaml — New example config for the static Gemma4 runtime inspector mode.
  • test/quantization/recipes/integration/test_static_gemma4_runtime_helpers.py — New test file with 12 unit tests for _normalize_valid_token_mask (5 tests: with attention mask, without mask using pad_token_id, without mask and no pad_token_id, shape mismatch raises, batched input) and _validate_padding_layout (7 tests: right padding valid, right padding no padding, right padding invalid raises, left padding valid, left padding no padding, batched right padding valid, batched right padding invalid raises).

Tests

The 12 unit tests in test/quantization/recipes/integration/test_static_gemma4_runtime_helpers.py exercise the pure-Python helper functions without requiring a real Gemma4 model or processor:

  • _normalize_valid_token_mask (5 tests): attention mask conversion to bool, pad_token_id derivation when mask is absent, all-valid fallback when both mask and pad_token_id are None, shape mismatch error, batched 2D input.
  • _validate_padding_layout (7 tests): valid right padding, no-padding case, invalid right padding raises, valid left padding, left no-padding case, batched valid, batched invalid raises.

All 12 tests pass:

$ python -m pytest test/quantization/recipes/integration/test_static_gemma4_runtime_helpers.py -v

================================================================= test session starts =================================================================
platform linux -- Python 3.10.12, pytest-9.0.3, pluggy-1.6.0 -- /home/d.savchenkov/myenv/bin/python
cachedir: .pytest_cache
rootdir: /home/d.savchenkov/TICO
configfile: pyproject.toml
plugins: anyio-4.13.0
collected 12 items                                                                                                                                    

test/quantization/recipes/integration/test_static_gemma4_runtime_helpers.py::TestNormalizeValidTokenMask::test_with_attention_mask PASSED       [  8%]
test/quantization/recipes/integration/test_static_gemma4_runtime_helpers.py::TestNormalizeValidTokenMask::test_without_attention_mask_uses_pad_token_id PASSED [ 16%]
test/quantization/recipes/integration/test_static_gemma4_runtime_helpers.py::TestNormalizeValidTokenMask::test_without_attention_mask_no_pad_token_id PASSED [ 25%]
test/quantization/recipes/integration/test_static_gemma4_runtime_helpers.py::TestNormalizeValidTokenMask::test_shape_mismatch_raises PASSED     [ 33%]
test/quantization/recipes/integration/test_static_gemma4_runtime_helpers.py::TestNormalizeValidTokenMask::test_batched_input PASSED             [ 41%]
test/quantization/recipes/integration/test_static_gemma4_runtime_helpers.py::TestValidatePaddingLayout::test_right_padding_valid PASSED         [ 50%]
test/quantization/recipes/integration/test_static_gemma4_runtime_helpers.py::TestValidatePaddingLayout::test_right_padding_no_padding PASSED    [ 58%]
test/quantization/recipes/integration/test_static_gemma4_runtime_helpers.py::TestValidatePaddingLayout::test_right_padding_invalid PASSED       [ 66%]
test/quantization/recipes/integration/test_static_gemma4_runtime_helpers.py::TestValidatePaddingLayout::test_left_padding_valid PASSED          [ 75%]
test/quantization/recipes/integration/test_static_gemma4_runtime_helpers.py::TestValidatePaddingLayout::test_left_padding_no_padding PASSED     [ 83%]
test/quantization/recipes/integration/test_static_gemma4_runtime_helpers.py::TestValidatePaddingLayout::test_batched_right_padding_valid PASSED [ 91%]
test/quantization/recipes/integration/test_static_gemma4_runtime_helpers.py::TestValidatePaddingLayout::test_batched_right_padding_invalid PASSED [100%]

=========================================================== 12 passed, 2 warnings in 7.17s ============================================================

Example Scripts

The static runtime can be invoked via the inspector CLI:

$ python ./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
Loading weights: 100%|███████████████████████████████████████████████████████████████████████████████████████████| 1951/1951 [00:00<00:00, 4720.12it/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] SKIP: prefill (not implemented in this PR)
[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.

This loads the model, creates the runtime, runs build_static_inputs validation, and prints SKIP messages for the not-yet-implemented prefill/decode/generation steps.

@dvsav
dvsav force-pushed the build_static_inp branch from 4c3a6da to bfbd37e Compare July 16, 2026 09:35
@dvsav
dvsav requested review from Torrero and mhs4670go July 16, 2026 09:42
Comment on lines +101 to +103
first_true = int(true_indices[0].item())
if first_true > 0 and torch.any(row[:first_true]):
raise ValueError("Left padding expected but not found")

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The condition torch.any(row[:first_true]) is always False because first_true is defined as the index of the first True in the row. By definition, everything before first_true is False.
If I understand correctly, for 'left' padding we need to check that rest values after first_true are valid.
We can do it with using following condition:

if not torch.all(row[first_true:]):
    raise ValueError("Left padding expected but not found")

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

cfg.padding_side is currently dead: StaticGemma4Layout has no such field, the runtime falls back to "right", and manual padding always writes to [:seq_len]. #822 keeps the same behavior. You can support right-padding-only in this step and reject/remove the left option.

if cfg.padding_side != "right":
    raise ValueError(
        "StaticGemma4Runtime currently supports right padding only"
    )

device: cpu
prompt: "<|image|>Describe the image."
verify_steps: 4
gen_steps: 16 No newline at end of file

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
gen_steps: 16
gen_steps: 16

@mhs4670go

Copy link
Copy Markdown
Contributor

Since this script validates a single fixed prompt/image against a fixed static export profile, keeping visual_start_idx and num_visual_tokens in the configuration makes sense. However, we should verify that the processor output actually matches that profile before replacing the image placeholder IDs.

Without this check, a stale or incorrect visual_start_idx can cause mm_fusion to insert the image embeddings into the wrong token range. The later reference comparison may show a divergence, but it does not clearly identify the profile mismatch and may not fail immediately.

Could we add a validation like the following before replacing the placeholder tokens?

raw_input_ids = inputs["input_ids"]  # (1, seq_len)

image_mask, video_mask, audio_mask = self.model.model.get_placeholder_mask(
    input_ids=raw_input_ids
)

image_positions = torch.nonzero(image_mask[0], as_tuple=True)[0]
if image_positions.numel() == 0:
    raise ValueError("No image placeholder tokens were found in processor output")

actual_start = int(image_positions[0].item())
actual_count = int(image_positions.numel())

expected_positions = torch.arange(
    actual_start,
    actual_start + actual_count,
    device=image_positions.device,
)

if not torch.equal(image_positions, expected_positions):
    raise ValueError(
        "Image placeholder tokens must form one contiguous span. "
        f"positions={image_positions.tolist()}"
    )

if actual_start != self.layout.visual_start_idx:
    raise ValueError(
        "Processor output does not match the static export profile: "
        f"expected visual_start_idx={self.layout.visual_start_idx}, "
        f"actual={actual_start}"
    )

if actual_count != self.layout.num_visual_tokens:
    raise ValueError(
        "Processor output does not match the static export profile: "
        f"expected num_visual_tokens={self.layout.num_visual_tokens}, "
        f"actual={actual_count}"
    )

input_ids = raw_input_ids.squeeze(0)

@mhs4670go

Copy link
Copy Markdown
Contributor

LGTM

mhs4670go
mhs4670go previously approved these changes Jul 20, 2026
Adds static input building, side-by-side validation, and helper tests.

Co-authored-by: Cline

TICO-DCO-1.0-Signed-off-by: d.savchenkov <d.savchenkov@partner.samsung.com>
@dvsav
dvsav force-pushed the build_static_inp branch from d97dda3 to 934991d Compare July 20, 2026 09:10
@dvsav
dvsav requested a review from Torrero July 20, 2026 09:19
@dvsav
dvsav merged commit 80ff2f3 into Samsung:main Jul 20, 2026
7 checks passed
@dvsav
dvsav deleted the build_static_inp branch July 20, 2026 11:07
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