Skip to content

fix: eliminate non-constant Slice ends in Gemma4 sliding-window CB subfunction - #1170

Open
quic-amitraj wants to merge 1 commit into
quic:release/v1.22.0from
quic-amitraj:fix/gemma4-sliding-window-cb-subfunction-slice
Open

fix: eliminate non-constant Slice ends in Gemma4 sliding-window CB subfunction#1170
quic-amitraj wants to merge 1 commit into
quic:release/v1.22.0from
quic-amitraj:fix/gemma4-sliding-window-cb-subfunction-slice

Conversation

@quic-amitraj

@quic-amitraj quic-amitraj commented Jul 9, 2026

Copy link
Copy Markdown
Contributor

Problem

When compiling Gemma4ForConditionalGeneration with use_onnx_subfunctions=True and continuous_batching=True, the QAIC compiler fails with:

QAIC_ERROR:
Error message:  [Operator-'Slice_520'] : Slice: Non-constant ends tensor not supported.
Unable to AddNodesToGraphFromModel

This occurs during language-decoder QPC compilation when -sub-functions is passed. The vision encoder compiles successfully; only the decoder fails.

Root Cause

In QEffGemma4DynamicLayer.update() (cache_utils.py), the sliding-window rolling-index computation was:

all_indices     = torch.arange(layer_ctx_len) + kv_position_ids.max() + 1
rolling_indices = torch.where(all_indices > layer_ctx_len - 1, all_indices % layer_ctx_len, all_indices)
rolling_indices = rolling_indices[:ctx_len]   # ← generates Slice_520

The rolling_indices[:ctx_len] slice translates to an ONNX Slice node inside the QEffGemma4TextDecoderLayer subfunction. Its ends tensor is ctx_len = Shape(past_key.0)[2] (= sliding_window), which is pre-computed in the outer ONNX graph as Gather(Shape(past_key.0), 2) and injected into the subfunction as an opaque INT32 argument (onnx::Cast_1033). Inside the subfunction body, the QAIC compiler sees this as a dynamic value — not a shape-derived constant — and rejects it.

This is the same class of bug as the CtxGatherCB comp_ctx_len regression fixed in c89c4f7.

Fix

Replace torch.arange(layer_ctx_len)[:ctx_len] with torch.arange(ctx_len) directly.

# Before
all_indices     = torch.arange(layer_ctx_len) + kv_position_ids.max() + 1
rolling_indices = torch.where(all_indices > layer_ctx_len - 1, all_indices % layer_ctx_len, all_indices)
rolling_indices = rolling_indices[:ctx_len]

# After
all_indices     = torch.arange(ctx_len) + kv_position_ids.max() + 1
rolling_indices = torch.where(all_indices > layer_ctx_len - 1, all_indices % layer_ctx_len, all_indices)
# rolling_indices already has ctx_len elements — no Slice needed

arange(n)[:n] == arange(n) identically, so there is no behavioural change. The Range op with a dynamic upper bound is already permitted in QAIC subfunction lowering — the existing ctx_indices = torch.arange(ctx_len)[None, None, ...] line in the same method demonstrates this. Slice_520 is eliminated entirely from the exported subfunction body.

The fix is also correct for the CCL case: torch.arange(min(sliding_window, CCL)) naturally produces CCL elements without any slice.

Changes

File Change
QEfficient/transformers/cache_utils.py Replace arange(layer_ctx_len)[:ctx_len] with arange(ctx_len) in QEffGemma4DynamicLayer.update()
examples/image_text_to_text/models/gemma_vision/gemma4/gemma4_cb.py Update example to use_onnx_subfunctions=True to exercise the now-working path

Test Plan

  • gemma4_cb.py with use_onnx_subfunctions=True, continuous_batching=True, full_batch_size=4, 2-layer reduced config — compiles and runs end-to-end on AI100
  • gemma4_cb.py with use_onnx_subfunctions=False — continues to pass (no regression)
  • Verified Slice_520 is absent from the re-exported subfunction ONNX body

…bfunction

QEffGemma4DynamicLayer.update() computed rolling_indices as:

    all_indices   = torch.arange(layer_ctx_len) + kv_position_ids.max() + 1
    rolling_indices = torch.where(...)
    rolling_indices = rolling_indices[:ctx_len]          # <- Slice_520

When compiled with -sub-functions, QAIC subfunction lowering rejects
Slice operators whose `ends` input is a non-constant function argument.
`ctx_len` (= Shape(past_key.0)[2] = sliding_window) is pre-computed in
the outer ONNX graph as Gather(Shape(past_key), 2) and injected into the
subfunction as an opaque INT32 parameter (onnx::Cast_1033).  The compiler
therefore raises:

  [Operator-'Slice_520'] : Slice: Non-constant ends tensor not supported.

Fix: replace torch.arange(layer_ctx_len)[:ctx_len] with torch.arange(ctx_len)
directly.  arange(n)[:n] == arange(n) identically, so there is no
behavioural change.  The resulting Range node with a dynamic upper bound
is already supported in QAIC subfunction lowering -- the existing
  ctx_indices = torch.arange(ctx_len)[None, None, ...]
line in the same method demonstrates this.  Slice_520 is eliminated
entirely from the exported subfunction body.

This is the same class of bug as the CtxGatherCB comp_ctx_len regression
fixed in c89c4f7.

Also updates the gemma4_cb.py example to use_onnx_subfunctions=True to
exercise and demonstrate the now-working path.

Tested: gemma4_cb.py (FULL_BATCH_SIZE=4, 2-layer reduced config) with
use_onnx_subfunctions=True compiles and runs end-to-end on AI100.

Signed-off-by: Amit Raj <amitraj@qti.qualcomm.com>
@quic-amitraj quic-amitraj self-assigned this Jul 9, 2026
@quic-amitraj quic-amitraj added 1.22 Release 1.22 candidate bugfix labels Jul 9, 2026
@quic-hemagnih

Copy link
Copy Markdown
Contributor

@quic-amitraj - Can you please post the following, you can use Gemma4-E2B-IT model (quick for testing)

  1. Issue reproducer
  2. Fix validation
  3. PR on mainline.

Lets get it ready by Monday, so that we can merge your changes.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

1.22 Release 1.22 candidate bugfix

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants