Skip to content

Enable static-cache + Flash-attention path (runtime-gated, ready for ORT #28958) - #364

Merged
titaiwangms merged 24 commits into
mainfrom
static-cache-flash-enablement
Jun 23, 2026
Merged

Enable static-cache + Flash-attention path (runtime-gated, ready for ORT #28958)#364
titaiwangms merged 24 commits into
mainfrom
static-cache-flash-enablement

Conversation

@titaiwangms

@titaiwangms titaiwangms commented Jun 19, 2026

Copy link
Copy Markdown
Contributor

What

mobius main already emits the correct maskless is_causal=1 + nonpad_kv_seqlen + TensorScatter static-cache decoder graph. This PR adds the runtime enablement, verification, and CI for that path on the CUDA Flash-attention kernel — it is not graph surgery. Nothing in the emitted graph is reverted or rewritten.

The path becomes runnable once an ONNX Runtime build containing microsoft/onnxruntime#28958 is installed (Flash eligibility widening for the bottom-right-causal errata onnx/onnx#8068).

Issues

Changes

  • (a) Conditional opset 24→23 lowering in _builder.py via _graph_requires_opset24 (a recursive subgraph scan) + _apply_opset_lowering, so graphs carrying TensorScatter or the Attention nonpad_kv_seqlen input correctly stay at opset 24. Flag-gated, default off. (_builder_test.py exercises the real branch.)
  • (b) Canonical capability probe src/mobius/_testing/ort_capabilities.pysupports_static_cache_flash() is a functional, fail-closed-but-loud runtime probe (not a version-string check). It builds a minimal TensorScatter + maskless Attention graph and runs it on CUDA. A known-answer value check closes a latent CPU-fallback fail-open: because ORT implicitly appends the CPU EP, a CUDA build that declines the node would silently run on CPU with wrong (top-left) values; the probe's deterministic reference (2.0) rejects that → NEEDS_FIXFalse. A structured _ProbeOutcome enum distinguishes the expected pre-#28958 reject from unexpected probe errors (logged with exc_info).
  • (c) Static-cache parity test (CI: add numerical parity coverage for the static-cache export path #329) tests/static_cache_parity_test.py — static vs dynamic vs HuggingFace, chunked-prefill zero-guard, V convex-hull valid-row invariant, exact-triangle nonpad == q_seq.
  • (d) e2e CUDA Flash-dispatch test tests/static_cache_flash_e2e_test.py — asserts the ONNX-domain Attention actually routes to Flash (via VERBOSE dispatch capture), gated on SM ≥ 8.0 (_flash_capable_gpu) and onnxruntime_QUICK_BUILD.
  • (e) Probe consolidation — deleted tests/_static_cache_support.py; one canonical probe module, no shim.

Gating

All new GPU tests skip automatically unless the installed ORT can actually run the path (probe-gated). CI stays green today and flips green automatically once an official ORT release containing microsoft/onnxruntime#28958 is installed — zero code change needed to enable.

Verified

  • 5/5 static-cache tests pass on an A100 (SM 8.0) with a post-#28958 ORT (full targeted suite: 14 passed including builder tests).
  • Fail-closed on pre-#28958 confirmed two ways: (1) source — the CUDA kernel guard causal_cross_no_past = is_causal && (q_seq != total_seq) && (past == 0) in Attention<T>::ComputeInternal (llm/attention.cc) raises NOT_IMPLEMENTED for the S_q=1 decode shape (no fast-path bypass); (2) empirically — a real onnxruntime-gpu==1.27.0 isolated venv raises NotImplementedsupports_static_cache_flash() == False.

Explicitly out of scope (intentionally held, separate follow-ups)

References


Co-authored-by: Copilot 223556219+Copilot@users.noreply.github.com

titaiwangms and others added 16 commits June 19, 2026 19:26
The build_from_module opset lowering downgraded the default-domain opset
from 24 to 23 for every non-default EP when MOBIUS_ORT_LOWER_OPSET_FOR_EP
is set. nonpad_kv_seqlen (Attention input #6) and TensorScatter are
opset-24-only, so declaring opset 23 on a static-cache graph is invalid
and silently strips the static-cache Flash-attention path.

Add _graph_requires_opset24() and skip lowering for any sub-model that
contains a TensorScatter node or an Attention consuming a non-empty
input #6. Standard-op-only sub-models still lower as before, so there is
no memcpy-storm regression for the common path. Defensive fix: the flag
defaults OFF.

Add CPU-authorable unit tests in src/mobius/_builder_test.py.

Agent-signed-off: Developer (71529949) [claude-opus-4.8 via copilot]
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The comment above the opset 24->23 lowering claimed the flag 'defaults
to True', but ort_lower_opset_for_ep defaults to False (_flags.py:112-113,
_env_bool MOBIUS_ORT_LOWER_OPSET_FOR_EP default False). Correct the
comment so the documented behavior matches reality.

Agent-signed-off: Developer (71529949) [claude-opus-4.8 via copilot]
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Re-adds the end-to-end static-cache (TensorScatter + ONNX-domain Attention)
CUDA coverage dropped with the aborted PR #340, adapted to the current
maskless graph on main (is_causal=1 + nonpad_kv_seqlen, no attn_mask/past_key).

tests/static_cache_flash_e2e_test.py builds a tiny fp16 static-cache model,
runs prefill (S_q>1) and decode (S_q=1) on the CUDA EP asserting finite outputs
and in-place cache advance, and asserts the Attention runs on the Flash kernel
(not MEA/unfused). Flash dispatch is asserted via the kernel's own VERBOSE log
("ONNX Attention: using Flash Attention" from cuda/llm/attention.cc), captured
by redirecting the native stderr fd and setting set_default_logger_severity(0)
-- this build exposes neither CUPTI Kernel-cat profiling events nor the per-op
AttentionKernelDebugInfo, so the VERBOSE log is the authoritative channel.

tests/_static_cache_support.py is a shared helper: a functional capability
probe (static_cache_cuda_supported) that skips when ORT lacks
microsoft/onnxruntime#28958, a quick_build_enabled gate that skips the Flash
assertion under onnxruntime_QUICK_BUILD (Flash compiled for head_dim 128 only
there), and the dispatch-log capture/parse helpers. Verified end-to-end on an
A100 (ORT 1.28.0 contains #28958): both tests pass, the Flash assertion
skips under onnxruntime_QUICK_BUILD, and the file is collectable without GPU.

Agent-signed-off: Developer (ed516084) [claude-opus-4.8 via copilot]
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Add tests/static_cache_parity_test.py guarding the maskless static-cache
attention combination (is_causal=1 + nonpad_kv_seqlen + TensorScatter) that
mobius emits and that onnx#8068 / onnxruntime#28958 enable on CUDA Flash.

Gate: reuses the shared functional probe static_cache_cuda_supported() from
tests/_static_cache_support.py (also used by the e2e Flash-dispatch test) so
both static-cache CUDA tests share one capability check. Pre-#28958 ORT raises
NOT_IMPLEMENTED -> skip; post-#28958 ORT runs. Also skips when no CUDA EP is
registered (TensorScatter + external-cache Attention are CUDA-only).

Tests (all @pytest.mark.integration):
- test_static_vs_dynamic_vs_hf_decode: builds Qwen2.5-0.5B twice from the same
  weights (static + dynamic cache); asserts token-id and last-token logit
  parity across static / dynamic / HuggingFace over N greedy decode steps on
  CUDA f32.
- test_chunked_prefill_structurally_empty_rows_are_zero: q_seq>1 chunked
  prefill with nonpad_kv_seqlen < q_seq; asserts structurally-empty
  bottom-right rows are EXACTLY 0 (LaunchZeroFullyMaskedRows guard), all
  finite, valid rows non-zero. Exact valid-row values are NOT asserted because
  upstream's bottom-right numpy parity reference is deferred (ort#28958 2E).
- test_prefill_chunk_causal_triangle_matches_reference: q_seq>1 prefill with
  nonpad==q_seq verifies the kernel reproduces the causal triangle against a
  NumPy SDPA reference (the well-defined bottom-right math).

Validated: 3 passed on A100 / ORT 1.28.0; skips cleanly on CPU-only.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Signed-off-by: titaiwang <titaiwang@microsoft.com>
…sting.ort_capabilities

The functional 'does this ORT run the maskless is_causal=1 + nonpad_kv_seqlen
+ TensorScatter static-cache graph on CUDA (needs onnxruntime#28958)' probe was
hand-rolled in two test modules. Move the single canonical implementation to
src/mobius/_testing/ort_capabilities.py as supports_static_cache_flash() — a
fail-closed (any exception -> False), lru_cached probe built on a minimal
self-contained TensorScatter + opset-24 Attention graph with no tests/ deps.

- New: src/mobius/_testing/ort_capabilities.py (CUDA_AVAILABLE,
  supports_static_cache_flash()).
- tests/_static_cache_support.py: delete the duplicate probe + its prefill
  feeds; keep only the e2e-specific model builder + Flash-dispatch capture
  helpers; re-export the canonical probe under the old name as a temporary
  migration bridge.
- tests/static_cache_flash_e2e_test.py: import the canonical probe.

Verified on A100 (ORT 1.28.0 source build w/ #28958): probe returns True and
all 5 static-cache tests pass (2 e2e Flash-dispatch + 3 parity).

Agent-signed-off: Developer (ed516084) [claude-opus-4.8 via copilot]
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…_capabilities

Swap the capability-probe import from the transitional tests/_static_cache_support
bridge to the canonical mobius._testing.ort_capabilities.supports_static_cache_flash.
Import-only change: no test logic altered. Docstring repointed to the canonical
module. All 3 tests still pass on A100/ORT 1.28.0.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Signed-off-by: titaiwang <titaiwang@microsoft.com>
Convenience wrapper over supports_static_cache_flash() that returns None when
the static-cache Flash path runs, else a skip message distinguishing the two
causes (no CUDA EP vs. an ORT build predating microsoft/onnxruntime#28958).
Lets tests skip via pytest.skip(reason) inside a fixture/helper instead of
CUDA_AVAILABLE-branching at the call site. Verified on A100: returns None
(path supported); isolated ruff check + format clean.

Agent-signed-off: Developer (ed516084) [claude-opus-4.8 via copilot]
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Adopt mobius._testing.ort_capabilities.static_cache_flash_skip_reason() in
_require_static_cache_attention(), replacing the local two-branch CUDA-EP /
onnxruntime#28958 skip text with the single canonical source. Drops the now-unused
CUDA_AVAILABLE import. Skip behavior is identical; all 3 tests pass on A100/ORT 1.28.0.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Signed-off-by: titaiwang <titaiwang@microsoft.com>
…o probe bridge

Per the tightened consolidation DoD, the e2e Flash-dispatch test no longer
depends on tests/_static_cache_support.py: its e2e-only helpers (tiny-model
builder, cache feeds, and the VERBOSE Flash-dispatch capture) are inlined, and
it imports the canonical probe from mobius._testing.ort_capabilities
(CUDA_AVAILABLE, supports_static_cache_flash).

tests/_static_cache_support.py is reduced to a minimal back-compat bridge that
only re-exports the canonical probe under its historical name, so the
not-yet-migrated parity test keeps collecting.  The bridge is deleted in a
follow-up once the parity test imports the canonical module directly.

Verified on A100 (ORT 1.28.0 w/ #28958): e2e 2/2 and parity 3/3 pass; isolated
ruff check + format clean.

Agent-signed-off: Developer (ed516084) [claude-opus-4.8 via copilot]
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Delete tests/_static_cache_support.py now that the capability probe lives solely in
mobius._testing.ort_capabilities and both the parity and e2e tests import it directly
(parity via static_cache_flash_skip_reason; e2e via supports_static_cache_flash with
its fixture helpers inlined). No file imports the shim anymore. Parity 3/3 pass and
e2e collects clean on A100/ORT 1.28.0. Single canonical import path.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Signed-off-by: titaiwang <titaiwang@microsoft.com>
Strengthen the nonpad<q_seq chunked-prefill test: replace reliance on the
near-vacuous non-zero check with an authoritative convex-combination invariant.
Softmax attention output is a convex combination of the attended V rows, so each
surviving output element must lie within the [min, max] hull of V over the valid
key window [0, nonpad), per head and per channel. This catches a wrong
attention-frontier bug (attending padded keys / mean-of-all-V) without depending
on the upstream-deferred exact valid-row reference (ort#28958 §2E). The exact-math
causal-triangle test (nonpad==q_seq) is unchanged.

Also move the mobius._testing.ort_capabilities import into the first-party isort
block. All 3 parity tests pass on A100/ORT 1.28.0.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Signed-off-by: titaiwang <titaiwang@microsoft.com>
Two robustness improvements to the conditional opset 24->23 lowering
(decision semantics unchanged):

1. _graph_requires_opset24 now scans with ir.traversal.RecursiveGraphIterator
   instead of only top-level nodes, so a TensorScatter or Attention input #6
   (nonpad_kv_seqlen) nested inside a future If/Loop/Scan subgraph is still
   detected. Docstring updated to drop the shallow-scan assumption.

2. The lowering loop is extracted into _apply_opset_lowering(pkg, ep), which
   owns the flag/EP/opset call-site gates and is now driven directly by the
   tests instead of a re-implemented decision. Added a nested-subgraph test
   and a mixed-package test (one static-cache sub-model that must keep opset
   24 + one standard sub-model that must lower to 23), plus default-EP and
   flag-disabled gate tests.

Agent-signed-off: Developer (71529949) [claude-opus-4.8 via copilot]
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
M1 (ort_capabilities.py): make supports_static_cache_flash() fail-closed but
not fail-silent. Introduce a cached _probe_static_cache_flash() returning a
_ProbeOutcome enum (SUPPORTED/NO_CUDA/NEEDS_FIX/PROBE_ERROR). Expected ORT
kernel rejects (NotImplemented/Fail from session.run, pre-#28958) are logged at
debug -> NEEDS_FIX; unexpected build/serialize/session errors and a vanished
CUDA EP are logged at warning with exc_info -> PROBE_ERROR. static_cache_flash_
skip_reason() now reports the true cause and no longer misattributes a broken
probe to needs-#28958.

M2 (static_cache_flash_e2e_test.py): add _flash_capable_gpu() (torch compute
capability >= (8,0)) and skipif-guard the Flash-dispatch test so pre-SM80 CUDA
(T4/Volta) skips cleanly instead of hard-failing the =={'flash'} assertion;
update the docstring accordingly.

nits: hoist prefill_len=4 to module constant _PREFILL_LEN; annotate config:
object params as ArchitectureConfig.

Validated on A100 (SM 8.0): 5/5 static-cache tests pass (43s), ruff clean.

Agent-signed-off: Developer (ed516084) [claude-opus-4.8 via copilot]
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…e check

Part (b) of the expanded scope: the structured-outcome enum (part a) cannot
catch a SILENT CPU fallback because no exception is raised. ORT always appends
the CPU EP as implicit fallback, and the CPU opset-24 Attention kernel runs the
maskless is_causal=1 + nonpad + TensorScatter graph WITHOUT erroring (with the
historical wrong top-left-causal values on pre-fix builds). So if a CUDA build
declines the node at GetCapability, session.run() succeeds on CPU and the probe
would fail-OPEN -> report SUPPORTED with zero CUDA coverage.

Close it with a deterministic known-answer (value-based, not log parsing):
identical keys/query -> uniform softmax over the two valid KV positions; distinct
per-position value tags (1.0, 3.0) -> the correct bottom-right output is their
mean (2.0). A wrong top-left kernel lets the single decode query attend only KV
slot 0 -> 1.0, which _probe_output_is_correct() rejects, classifying the run as
NEEDS_FIX rather than SUPPORTED. Probe geometry switched to the decode regime
(S_q=1, two valid KV slots) so bottom-right vs top-left genuinely differ.

Validated on A100 (SM 8.0): correct ref (2.0) accepted, top-left (1.0) rejected,
A100 still SUPPORTED; 5/5 static-cache tests pass (46s), ruff clean.

Agent-signed-off: Developer (ed516084) [claude-opus-4.8 via copilot]
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…guard

Confirm the core contract for the new S_q=1 probe geometry: a pre-#28958 ORT
still returns False. Source-grounded in CUDA llm/attention.cc Attention<T>::
ComputeInternal, guard `causal_cross_no_past = is_causal && (q_seq != total_seq)
&& (past == 0)` then `if (causal_cross_no_past && nonpad_kv_seqlen != nullptr)
return NOT_IMPLEMENTED`. For S_q=1 over total_kv>=2, q_seq != total_seq holds and
there is no S_q==1 decode fast-path that bypasses it (S_q=1 is the case the
reject message explicitly names), so a pre-fix build RAISES at run from
ComputeInternal -> expected reject -> NEEDS_FIX -> False. The known-answer value
check remains the second defense for non-raising wrong-value fallbacks.

Comment-only; no behavior change. 5/5 static-cache tests pass on A100 (45s),
ruff clean.

Agent-signed-off: Developer (ed516084) [claude-opus-4.8 via copilot]
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Comment/docstring only, zero logic change:
1. ort_capabilities.py _probe_static_cache_flash docstring: record the one
   residual assumption — the probe validates a CUDA EP + output correctness but
   does NOT explicitly assert the Attention node was placed on CUDA; safe today
   because the pre-#28958 CPU fallback yields the wrong top-left value the
   value-check rejects, would only mis-pass if a future CPU kernel became
   bottom-right-correct AND CUDA declined the node.
2. e2e test module docstring: note the new SM>=8.0 (_flash_capable_gpu) skip on
   the Flash-dispatch test alongside the QUICK_BUILD skip, so the doc matches
   behavior.
3. ort_capabilities.py: note _PROBE_QUERY_FILL is intentionally equal to
   _PROBE_KEY_FILL (identical K/Q -> uniform softmax the known-answer relies on).

5/5 static-cache tests pass on A100 (44s), ruff clean.

Agent-signed-off: Developer (ed516084) [claude-opus-4.8 via copilot]
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@github-actions

github-actions Bot commented Jun 19, 2026

Copy link
Copy Markdown

🏗️ Architecture Diff

Comparing bab4068d95460c

Model Sub-model Changes Status

No architecture changes detected.


Legend: ⚪ No change · 🔵 Minor (attrs/inits) · 🟡 Moderate (nodes added/removed) · 🔴 Major (interface changed)

@github-actions

github-actions Bot commented Jun 19, 2026

Copy link
Copy Markdown

Performance Comparison

Comparing bab4068d95460c

Model Metric Baseline Current Delta
bert (feature-extraction) model_size_bytes 359 KB 359 KB +0.0%
bert (feature-extraction) num_nodes 60 60 +0.0%
falcon model_size_bytes 364 KB 364 KB +0.0%
falcon num_nodes 68 68 +0.0%
gemma2 model_size_bytes 428 KB 428 KB +0.0%
gemma2 num_nodes 107 107 +0.0%
gpt2 model_size_bytes 388 KB 388 KB +0.0%
gpt2 num_nodes 54 54 +0.0%
llama model_size_bytes 425 KB 425 KB +0.0%
llama num_nodes 62 62 +0.0%
llama (static-cache) model_size_bytes 425 KB 425 KB +0.0%
llama (static-cache) num_nodes 58 58 +0.0%
mamba (ssm-text-generation) model_size_bytes 296 KB 296 KB +0.0%
mamba (ssm-text-generation) num_nodes 98 98 +0.0%
phi3 model_size_bytes 421 KB 421 KB +0.0%
phi3 num_nodes 60 60 +0.0%
phi3 (static-cache) model_size_bytes 421 KB 421 KB +0.0%
phi3 (static-cache) num_nodes 56 56 +0.0%
qwen2 model_size_bytes 425 KB 425 KB +0.0%
qwen2 num_nodes 62 62 +0.0%
qwen2 (static-cache) model_size_bytes 425 KB 425 KB +0.0%
qwen2 (static-cache) num_nodes 58 58 +0.0%
qwen3_5_moe (hybrid-text-generation) model_size_bytes 506 KB 506 KB +0.0%
qwen3_5_moe (hybrid-text-generation) num_nodes 275 275 +0.0%
qwen3_5_text (hybrid-text-generation) model_size_bytes 458 KB 458 KB +0.0%
qwen3_5_text (hybrid-text-generation) num_nodes 129 129 +0.0%
qwen3_5_vl (hybrid-qwen-vl) model_size_bytes 977 KB 977 KB +0.0%
qwen3_5_vl (hybrid-qwen-vl) num_nodes 413 413 +0.0%
t5 (seq2seq) model_size_bytes 836 KB 836 KB +0.0%
t5 (seq2seq) num_nodes 166 166 +0.0%
whisper (speech-to-text) model_size_bytes 1008 KB 1008 KB +0.0%
whisper (speech-to-text) num_nodes 128 128 +0.0%

No performance regressions.

@codecov

codecov Bot commented Jun 19, 2026

Copy link
Copy Markdown

The author of this PR, titaiwangms, is not an activated member of this organization on Codecov.
Please activate this user on Codecov to display this PR comment.
Coverage data is still being uploaded to Codecov.io for purposes of overall coverage calculations.
Please don't hesitate to email us at support@codecov.io with any questions.

Copilot AI left a comment

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.

Pull request overview

This PR adds ORT capability gating plus CUDA integration tests to validate mobius’s static-cache (TensorScatter KV) attention path, and updates opset-lowering logic so the static-cache Flash-attention form remains valid when lowering opset declarations for older EPs.

Changes:

  • Add CUDA integration coverage for static-cache correctness (parity vs dynamic-cache ONNX + HF) and for Flash-kernel dispatch.
  • Introduce a cached, functional ORT capability probe (supports_static_cache_flash / static_cache_flash_skip_reason) to skip tests until ORT includes microsoft/onnxruntime#28958 behavior.
  • Refactor opset 24→23 lowering so sub-models that require opset-24-only semantics (TensorScatter or Attention input #6 nonpad_kv_seqlen) are not lowered; add unit tests for this scan/lowering behavior.

Reviewed changes

Copilot reviewed 5 out of 5 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
tests/static_cache_parity_test.py Integration parity + edge-case tests for static-cache Attention behavior (incl. bottom-right causal corner cases).
tests/static_cache_flash_e2e_test.py CUDA e2e regression + Flash-dispatch verification via ORT VERBOSE logs.
src/mobius/_testing/ort_capabilities.py Adds functional CUDA capability probe to gate static-cache Flash tests until ORT supports the kernel combo.
src/mobius/_builder.py Moves opset-lowering into helpers and prevents lowering for graphs requiring opset-24-only semantics.
src/mobius/_builder_test.py Unit tests for _graph_requires_opset24 and per-submodel opset-lowering behavior.

Comment thread src/mobius/_builder.py Outdated
Comment thread src/mobius/_testing/ort_capabilities.py Outdated
Comment thread tests/static_cache_flash_e2e_test.py Outdated
Comment thread src/mobius/_builder.py Outdated
@titaiwangms

Copy link
Copy Markdown
Contributor Author

Review summary

Verdict: sound, mergeable. No Critical issues across a five-reviewer pass (readability / correctness / adversarial / spec-deep / cross-module). The math and spec are independently verified: opset-24 Attention input #6 = nonpad_kv_seqlen ✓, the MOBIUS_ORT_LOWER_OPSET_FOR_EP default is False ✓, and the CPU _builder_test.py passes (9/9) ✓. 2 Major, several Minor/Nit, 2 open questions below.

🟠 Major

M1 — The probe's NEEDS_FIX classification is too broad, which contradicts its own "fail-closed but not fail-silent" thesis. (src/mobius/_testing/ort_capabilities.py)

  • Generic onnxruntime.Fail is in _EXPECTED_REJECT_ERRORS (~line 350), so a real runtime failure on a fixed build (CUDA OOM, illegal memory access, TensorScatter crash) is mislabeled "needs #28958" and silently skips the suite.
  • Any value mismatch maps to NEEDS_FIX logged at DEBUG (~line 614), not just the known top-left 1.0 signature — so a genuine numerical regression is masked as "needs fix."
  • Suggestion: only treat Fail as expected when the message carries the known reject signature; classify other mismatches/failures as PROBE_ERROR at WARNING.

M2 — Duplicated opset-lowering logic has drifted. src/mobius/_testing/ort_inference.py _should_lower_opset (lines 124-127) only scans top-level nodes for TensorScatter; it lacks both the recursion and the Attention input-#6 check that the new canonical _graph_requires_opset24 (src/mobius/_builder.py) has. Not corrupting today (the static-cache path co-emits a top-level TensorScatter, so it is caught), but it is a latent hazard: a maskless Attention without TensorScatter, or either op nested inside If/Loop/Scan, would be silently lowered to opset 23 → invalid model. Suggestion: have _should_lower_opset reuse _graph_requires_opset24 (extract to a shared util if import cycles bite).

🟡 Minor

  • Collection-time CUDA probe: tests/static_cache_flash_e2e_test.py evaluates supports_static_cache_flash() in a module-level pytestmark, so the functional CUDA probe (build graph + create CUDA session) runs at import/collection, before fixtures — an xdist / lazy-CUDA-init hazard. The parity test's setup-time helper pattern is cleaner; consider matching it.
  • Global-state mutation in captured_attention_dispatch: restores the ORT logger severity to a hardcoded 2 rather than the prior value, and redirects process fd 2 (blast radius is test-only, but it is not concurrency-safe).
  • Probe does not assert CUDA node placement (acknowledged residual in the docstring; the known-answer value check mitigates the realistic case).
  • _apply_opset_lowering lacks the original <= 23 guard that ort_inference.py has, so a sub-model already at ≤23 would be "upgraded" to 23 with a misleading Lowered 21→23 log (latent; all packages are 24 today).
  • Unenforced load-bearing invariant _PROBE_KEY_FILL == _PROBE_QUERY_FILL — collapse to one constant or add an assert.

🔵 Nits

_require_static_cache_attention skips (not fails) — the require_* name implies otherwise; stale name="static_cache_attention_probe" in the parametric builder in the parity test; FLASH_CACHE_DTYPE governs all tensors, not just caches; double-negation guard in _builder.py; no ai.onnx-domain test for the Attention input-#6 branch; _flash_capable_gpu keys on torch's device view rather than ORT's active device.

❓ Open questions (grounded)

  1. Fix Attention is_causal causal-mask alignment + composed is_causal/attn_mask NaN robustness for external (static) KV cache (#8054) onnx/onnx#8068 is still an open/unmerged errata — the published opset-24 reference impl and spec prose still describe top-left alignment; Fix Attention is_causal bottom-right alignment for external KV cache (onnx#8068, #28904) microsoft/onnxruntime#28958 (the actual authority for these tests) merged very recently. The module docstrings cite #8068 as if it were settled spec. A one-line note ("unmerged errata; the authority here is the merged ORT kernel") would prevent confusion.
  2. test_chunked_prefill_structurally_empty_rows_are_zero pins behavior in the nonpad < q_seq (negative-offset) regime that #8068 itself labels out-of-contract. It is a deliberate mobius guard (and never arises in production), but a future ORT could legitimately change it without violating #8068. Confirm you want it pinned.

✅ Praise

The known-answer probe design (identical Q/K → uniform softmax → mean-of-value-tags, which cleanly separates top-left 1.0 from bottom-right 2.0, fp16-exact with atol=0.1), the convex-combination hull invariant in the chunked-prefill test, the _ProbeOutcome four-state enum, and the recursive If/Loop/Scan subgraph scan were all called out as genuinely well-reasoned.

Synthesized from a five-agent review (readability, correctness, adversarial, spec-deep, cross-module). GPU-gated tests were not executed here; M1/M2 and the open questions are the load-bearing items.

titaiwangms added a commit that referenced this pull request Jun 19, 2026
…er -> after-scatter) (#365)

## What

`examples/static_cache_generation.py` fed `nonpad_kv_seqlen` as the
**before-scatter** KV count. A separate `nonpad_kv_seqlen` accumulator
stayed always-equal to `write_indices` (both initialized to 0, both
incremented only *after* `session.run`), so the example passed `nonpad
== write_indices`. The spec-correct value is `write_indices +
cur_seq_len` — the valid KV count **after** the current chunk is
scattered into the cache, per the bottom-right `is_causal` contract
(onnx/onnx#8068).

## Impact

Wrong in **every** phase:
- **Prefill**: passed `0` instead of `prompt_len`, so the kernel saw ~0
valid keys and produced wrong logits.
- **Decode**: off-by-one on every step.

On the CPU EP the maskless graph **runs-but-wrong silently** (no error
raised). This is **example-only** — it is the canonical usage reference
and does **not** affect library/graph code or PR #364.

## Fix

- Feed `nonpad_kv_seqlen = write_indices + cur_seq_len` at the run
inputs.
- Remove the now-dead redundant `nonpad_kv_seqlen` accumulator (it
merely duplicated `write_indices`).
- Add an explanatory inline comment + docstring note so the canonical
example teaches the correct **after-scatter** pattern.

## Verified

- Matches the authoritative reference in
`tests/static_cache_parity_test.py` (prefill nonpad = prompt_len =
write_indices(0) + query_len, lines 322-327; decode nonpad = valid_len +
1 = write_indices(valid_len) + 1, lines 361-362) and the consumer
`src/mobius/components/_attention.py:169-183` (the scatter precedes the
maskless `is_causal` Attention's `nonpad` read).
- Ran on CPU with `Qwen/Qwen2.5-0.5B` -> coherent output: `The capital
of France is` -> ` Paris. It is the largest city in`. Pre-fix the
all-zero prefill nonpad silently produced wrong logits.
- Triple-reviewed (code / critical / readability all ship).

## Reference

- onnx/onnx#8068 (causal bottom-right errata).
- Relates to the static-cache + Flash enablement in PR #364 — this is a
separate, example-only follow-up.

---------

Signed-off-by: titaiwang <titaiwang@microsoft.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
titaiwangms and others added 3 commits June 19, 2026 22:48
Three grounded review fixes on the static-cache probe + opset lowering:

(2) ort_capabilities.py: the inner reject handler matched (NotImplemented,
    Fail) by type alone, so a genuine ORT Fail (CUDA OOM, kernel bug, install
    drift) was silently misclassified as 'needs onnxruntime#28958' -> whole
    suite skips, masking a regression. Add _is_expected_pre28958_reject():
    NotImplemented is always the confirmed pre-#28958 path; a Fail (or the
    defensive RuntimeError fallback) is accepted only when its message carries
    the reject signature (nonpad_kv_seqlen / tensorscatter), else it returns
    PROBE_ERROR and logs at WARNING so real failures stay loud. Value-based
    known-answer check preserved. Adds CPU-only unit tests for the classifier
    and the output check.

(1) _builder.py _apply_opset_lowering: the gate guarded only EP != 'default',
    so it also fired for EP == 'cpu', diverging from ort_inference. Also skip
    'cpu' (CPU EP already has opset-24 kernels). Adds a cpu-skip unit test.

(M2) ort_inference.py _should_lower_opset: only scanned top-level nodes and
    lacked the Attention input-#6 (nonpad_kv_seqlen) check. Delegate to
    _builder._graph_requires_opset24 (recursive + input-#6) so both
    opset-24-detection paths agree.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Signed-off-by: titaiwang <titaiwang@microsoft.com>
…ew fixes

Extend coverage for each behavior added in d0e96f5, per review:

(2) ort_capabilities.py: extract _classify_run_error(exc) -> _ProbeOutcome
    (the caught session.run error -> NEEDS_FIX vs PROBE_ERROR mapping, with
    the debug/warning logging colocated) so the outcome is unit-testable
    without CUDA. New tests assert NotImplemented -> NEEDS_FIX, a
    signature-matching Fail -> NEEDS_FIX, a genuine Fail (CUDA OOM, no
    signature) -> PROBE_ERROR (not a silent 'needs #28958' skip), and the
    RuntimeError fallback -> PROBE_ERROR.

(M2) add ort_inference_test.py: _should_lower_opset with a nested If-subgraph
    Attention(nonpad_kv_seqlen input #6) -> False, proving the recursion +
    input-#6 alignment now fires where the old top-level-only scan missed it;
    plus top-level TensorScatter, standard-graph-lowers, cpu-skip,
    flag-disabled, and opset<=max guards.

(1) cpu-skip already covered by _builder_test.test_apply_opset_lowering_skipped_for_cpu_ep.

Behavior unchanged; 28 unit tests pass, static-cache GPU tests remain
fail-closed-skip on pre-#28958 ORT.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Signed-off-by: titaiwang <titaiwang@microsoft.com>
…t.py

Every sibling file under src/mobius/_testing/ carries the standard 2-line
MIT license header; this newly-added test file was missing it. Header-only
change, no logic touched.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Signed-off-by: titaiwang <titaiwang@microsoft.com>
titaiwangms and others added 2 commits June 19, 2026 23:18
CI lint (ruff 0.15.14 with --config=pyproject.toml) flagged three errors in
the static-cache files that the isolated local ruff run did not surface:

- LOG014 (ort_capabilities.py): `exc_info=True` sits outside an except handler
  after _classify_run_error was extracted from the except arm. Pass the
  explicitly-captured exception (`exc_info=exc`) instead — semantically correct
  regardless of handler context and preserves the traceback in the warning log.
- SIM112 (static_cache_flash_e2e_test.py): capitalize the CI-exported env var
  to ONNXRUNTIME_QUICK_BUILD. The lowercase onnxruntime_QUICK_BUILD references
  that name the actual ORT cmake build flag are left unchanged.
- SIM115 (static_cache_flash_e2e_test.py): open the stderr-capture
  tempfile.TemporaryFile via a `with` context manager.

Behavior-preserving: 28 unit tests pass, static-cache GPU trio still
fail-closed-skips on pre-#28958 ORT.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Signed-off-by: titaiwang <titaiwang@microsoft.com>
The ② narrowing (a signature-less ORT Fail must surface as PROBE_ERROR, not
be misclassified as the pre-#28958 NEEDS_FIX reject) was only tested at the
pure _classify_run_error classifier level. Add a small end-to-end test that
mocks ort.InferenceSession so session.run raises a signature-less Fail
(CUDA OOM) and asserts _probe_static_cache_flash returns PROBE_ERROR AND
logs a WARNING - i.e. a genuine failure stays loud instead of silently
skipping the whole suite as "needs #28958". A contrasting case asserts the
real NotImplemented reject still maps to NEEDS_FIX end-to-end.

Quality improvement only: this is NOT the codecov/patch fix. codecov/patch is
red due to a Codecov org account-activation gap (the PR author is not an
activated Codecov member), which only a maintainer can resolve at
app.codecov.io/members/gh/onnxruntime - adding tests cannot turn it green.

Production code untouched; CPU-only (no CUDA needed). 14 ort_capabilities
unit tests pass.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Signed-off-by: titaiwang <titaiwang@microsoft.com>
@titaiwangms
titaiwangms force-pushed the static-cache-flash-enablement branch from c39a5b8 to 6e19723 Compare June 19, 2026 23:52
Comment thread src/mobius/_builder.py Outdated

@justinchuby justinchuby left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Review verdict: 🟡 minor-issues (non-blocking). Thorough, correctness-first PR — the opset-24 retention logic, the fail-closed/fail-open probe design, and the parity tests are all rigorous and well-reasoned. I traced the probe + tests against the production components/_attention.py static branch (Attention input #6 = nonpad_kv_seqlen, is_causal=1, no attn_mask/past_key) and they faithfully replicate it. A few non-blocking observations:

1. Description ↔ code mismatch (no fix needed in code). Change-summary item (e) says "deleted tests/_static_cache_support.py", but the diff contains no deletion and that file exists on neither main nor the head ref (gh api .../contents/tests/_static_cache_support.py → 404 on both). The end state is correct (no shim), but the "deleted" claim is misleading — worth correcting the PR body so reviewers aren't hunting for a phantom removal.

2. Nice latent-bug fix. Routing the build-side _apply_opset_lowering through the recursive _graph_requires_opset24 (and adding the per-submodel retention) closes a real hole: the previous build_from_module lowering had no TensorScatter/nonpad guard at all, so with MOBIUS_ORT_LOWER_OPSET_FOR_EP=1 on a non-default EP it would have emitted an invalid opset-23 graph carrying TensorScatter. Latent only (flag default is False), but a genuine correctness improvement, and the build/inference gates now agree.

3. Probe value-mismatch classification (inline) — a wrong-but-not-top-left output is silently NEEDS_FIX; see inline note.

4. Logger-severity restore nit (inline).

Nothing here blocks merge; the gating story (probe-gated skips, self-enabling once ORT #28958 is pinned) is sound and CI-safe. 👍

Comment thread src/mobius/_testing/ort_capabilities.py
Comment thread tests/static_cache_flash_e2e_test.py Outdated
titaiwangms and others added 2 commits June 22, 2026 21:16
Three non-blocking review nits from justinchuby:

1. Rename `_apply_opset_lowering` -> `_maybe_apply_opset_lowering` (it gates on
   the flag/EP and may not lower), updating the build_from_module call site and
   the _builder_test import, calls, and test names.

2. ort_capabilities: in the silent-wrong-output branch, distinguish the
   *expected* pre-#28958 top-left fallback (mean ~= first value tag, 1.0) from an
   *unexpected* wrong value. The known fallback stays at debug; any other wrong
   value is now logged at WARNING so a genuine post-#28958 regression (or ORT
   drift) is not silently bucketed as the known fallback. Adds the
   `_PROBE_TOPLEFT_OUTPUT` constant naming that signature.

3. captured_attention_dispatch: restore the ORT default logger severity to the
   value that was set before the context (captured via a module-level shadow,
   since ORT exposes a setter but no getter) instead of a hardcoded 2.

Lint clean (real pyproject config); _builder_test + ort_capabilities_test pass
(24 passed, 2 CUDA-only e2e skipped on CPU).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Signed-off-by: titaiwang <titaiwang@microsoft.com>
Address Copilot review comments on PR #364:

- tests/static_cache_flash_e2e_test.py: the module-level `skipif` evaluated
  `supports_static_cache_flash()` eagerly at import/collection time, which runs
  a CUDA build + serialize + session.run. Replace it with an autouse fixture
  that calls `static_cache_flash_skip_reason()` at test setup, so importing the
  module and `pytest --collect-only` are side-effect-free. The probe is
  lru-cached (runs at most once) and the helper reports the true skip cause
  (no CUDA EP, missing microsoft/onnxruntime#28958, or an unexpected probe
  failure).

The remaining Copilot comments are already satisfied at HEAD: the opset-lowering
gate already excludes the CPU and default EPs (_builder.py), and the probe only
treats an ORT `Fail` as the expected pre-#28958 reject when its message carries
the reject signature, routing generic failures (OOM, drift) to PROBE_ERROR
(ort_capabilities.py). The _builder.py:216 comment was a PR-description note,
not a code defect.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Signed-off-by: titaiwang <titaiwang@microsoft.com>
Comment thread tests/static_cache_flash_e2e_test.py

@titaiwangms titaiwangms left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Review summary — static-cache Flash enablement / probe / CI

Reviewed at d95460c by a multi-model review team plus independent verification. Clean, well-gated infrastructure; no Critical/Major findings specific to this PR. Mergeable.

Verified correct ✅

  • Recursive opset-24 guard (_graph_requires_opset24 + _maybe_apply_opset_lowering): correctly detects TensorScatter and Attention with a non-empty input #6 (nonpad_kv_seqlen) inside If/Loop/Scan bodies, decides per sub-model, and errs conservatively — an empty-name optional placeholder only keeps opset 24, never wrongly lowers to 23. The default-off flag means no shipped export changes.
  • Capability probe (supports_static_cache_flash): genuinely fail-closed. The known-answer value check (2.0) closes the latent CPU-fallback fail-open where a CUDA build that declines the node would silently run on CPU with wrong (top-left) values; the _ProbeOutcome enum cleanly separates the expected pre-#28958 reject from unexpected probe errors (logged with exc_info). Strong unit-test coverage of the classifier.
  • New GPU tests skip automatically unless the installed ORT can actually run the path, so CI stays green today and flips green automatically once an ORT release with microsoft/onnxruntime#28958 is installed — no code change needed.

Notes (no action required for this PR)

  • This PR enables the maskless is_causal=1 path; the bias-aware sibling (#367) and the genai consumer (microsoft/onnxruntime-genai#2235) edit parallel, non-conflicting structures — confirmed compatible across branches.
  • The recursive guard is future-proof but currently only matches TensorScatter / Attention-input#6; if other opset-24-only default-domain ops are introduced into these graphs later, the predicate set will need extending (a lowered-to-23 graph would otherwise be silently invalid).

Praise: the probe design (functional runtime check + known-answer value gate, not a version-string check) is exactly the right pattern for a capability that depends on an unreleased ORT kernel, and the fail-closed-but-loud behavior is well thought through.

@titaiwangms
titaiwangms enabled auto-merge (squash) June 23, 2026 00:00
@titaiwangms
titaiwangms merged commit 1ad4160 into main Jun 23, 2026
21 of 23 checks passed
@titaiwangms
titaiwangms deleted the static-cache-flash-enablement branch June 23, 2026 00:00
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.

CI: add numerical parity coverage for the static-cache export path

3 participants