Skip to content

perf(rocm): remove per-call KV-cache copies from the MLA and paged-append paths - #292

Open
demandal25 wants to merge 6 commits into
amd-integrationfrom
mla-perf-cdna3
Open

perf(rocm): remove per-call KV-cache copies from the MLA and paged-append paths#292
demandal25 wants to merge 6 commits into
amd-integrationfrom
mla-perf-cdna3

Conversation

@demandal25

@demandal25 demandal25 commented Aug 22, 2026

Copy link
Copy Markdown
Collaborator

Summary

BatchMLAPagedAttentionWrapper.run() concatenated ckv_cache and kpe_cache on every call to build the single buffer AITER's MLA kernels read. torch.cat copies the whole tensor it is handed, so the cost tracked the allocated page pool rather than the live pages — at a 4.5 GB pool with a 9 MB working set, 2.799 ms per decode step, per layer. This makes that copy a view when the two halves are adjacent, which is how vLLM and SGLang already allocate MLA caches.

Also collapses plan()'s two device→host syncs into one, and moves the AITER paged-KV append off AITER's per-call Python dispatch into a compiled shim.

What changed

  • flashinfer/mla_rocm.py_combined_kv_view() detects ckv/kpe as adjacent halves of one allocation and returns a zero-copy as_strided view; run() falls back to the copy with a one-time warning otherwise, and rejects 4-D caches outright. _gather_plan_inputs_on_host() fetches the three index tensors in a single host round-trip.
  • flashinfer/csrc_rocm/page_aiter.cu — new shim calling aiter::reshape_and_cache_flash, with the FlashInfer→vLLM slot-index translation folded into one kernel (it was seven Python tensor ops). Validation matches csrc_rocm/page.cu: device, rank, head/dim, nnz, launch status.
  • flashinfer/csrc_rocm/page_aiter_jit_pybind.cu, flashinfer/jit/page.pyTORCH_LIBRARY_FRAGMENT registration and gen_page_aiter_module(), following the norm_aiter pattern.
  • flashinfer/page.py — routes the AITER append through the shim behind register_custom_op + register_fake_op; _try_get_page_aiter_module() keeps backend="auto" from raising when the shim cannot build; explicit backend="aiter" now enforces the same NHD/dtype gates auto applies. Adds the missing fake op for append_paged_mla_kv_cache.
  • flashinfer/__init__.py — export append_paged_mla_kv_cache on HIP; it was CUDA-only.
  • benchmarks/rocm_benchmarks/bench_mla_hip.py — new, four modes (attn/cat/plan/pool) so results stay attributable; --separate reproduces the pre-fix allocation layout.

Architecture / design notes

Detection rather than a new parameter, because production already has the layout. vLLM stores kv_c_and_k_pe_cache and SGLang stores K_Buffer, both sliced with torch.split — exactly the adjacency this detects — so those callers get the fast path with no code change and the signature stays identical to the CUDA wrapper. The rejected alternative, caching the concatenated buffer across calls, is silently wrong: the append op mutates the cache between decode steps, so a cached copy serves stale KV.

The old docstring told callers to pre-allocate a combined buffer and "pass it as both arguments (sliced)". That never worked — torch.cat copies regardless of how its inputs were carved — and the shape it named (4-D, with a size-1 axis) would then be unsqueezed to 5-D, which AITER rejects. Replaced with the 3-D form the code now honors; 4-D input raises instead of warning that a genuinely adjacent buffer is not adjacent.

plan() is bounded by round-trip count, not payload. A device→host sync costs ~70 µs on gfx942 regardless of size, and these tensors are batch+1 int32 elements, so batching the three into one copy is the whole win. The remaining ~70 µs is the H2D of the computed last_page_len; removing it means computing on-device while validating on host, which needs kv_len_arr on the device and turns circular when it is not.

The shim needs a fake op. Registering it through TORCH_LIBRARY_FRAGMENT makes it visible to Dynamo, which places it in the FX graph and fake-runs it during tracing — and the shim calls numel()/size(), which throw on symbolic shapes. The Python path it replaced was not a registered torch op, so Dynamo graph-broke on it instead. The norm/rope/activation AITER shims call their modules directly from the public entry points with no custom-op wrapper, so they carry the same exposure; not changed here.

backend="auto" must never raise. The shim links AITER's C++ symbols, so it can fail for reasons an import probe cannot see (no hipcc, unwritable cache dir, AITER signature drift). The None is cached deliberately — functools.cache does not memoize exceptions, so a raising getter would retry the whole AITER rebuild on every call. MissingJITCacheError is re-raised rather than caught, since conftest.py turns it into a skip and records the module for the prebuilt cache.

Benchmark results

MI300X (gfx942, 304 CU), bf16, page_size 1, torch 2.9.1+rocm7.2, amd-aiter 0.1.10.

MLA decode run() — active set fixed at b=8 / kv=1024 (9 MB), only the pool grows:

pool before after
0.01 GB 0.081 ms 0.071 ms
0.28 GB 0.281 ms 0.071 ms
1.12 GB 1.076 ms 0.072 ms
4.50 GB 2.799 ms 0.071 ms

Flat across a 450× range — cost now depends on the working set. Production pools fill HBM, well past the largest row here.

MLA decode, standard shapes:

batch kv_len heads=16 heads=128
8 32768 0.362 → 0.126 ms (2.9×) 0.573 → 0.313 ms (1.8×)
32 8192 0.360 → 0.130 ms (2.8×) 0.507 → 0.242 ms (2.1×)
32 32768 1.424 → 0.349 ms (4.1×) 1.806 → 0.756 ms (2.4×)

heads=16 is TP8, heads=128 is DP-attention + EP; AMD recommends the former below ~256 concurrent requests and the latter above ~512, so both are load-bearing.

plan(): 120–130 µs → 87–92 µs, flat in batch size (1..256).

AITER paged-KV append: 102.9 → 11.0 µs at nnz=32, 101.2 → 11.2 at 256, 101.8 → 12.1 at 4096 (~9×). Also removes the lazy build-on-first-call that otherwise stalls the first append in a server process.

Measured, but deliberately not changed

  • auto routes the KV append to AITER, which is slower than the in-tree kernel — 7.4 vs 11.6 µs at nnz=4096, 593 vs 751 µs at 262144, converging to ~0.79×. The gap is AITER's reshape_and_cache_flash itself (2.86 vs 3.62 TB/s on identical work), not the shim's slot-mapping kernel, which is free at scale. This predates the PR and only gfx942 was measured, so the routing is left alone.
  • The MLA decode kernel has no headroom at TP8 — against a measured bare-read floor for the same KV volume it runs at 1.01–1.09×, i.e. full attention for roughly the cost of reading the KV cache once. AITER's num_kv_splits heuristic is also already optimal (best 1.02× across ten shapes against fixed values).
  • Neither append kernel is tuned for CDNA3AppendPagedKVMlaCache hardcodes vec_size = 2 (4-byte accesses for bf16, where the MHA path computes 16-byte) and AppendPagedKVCache derives it partly from HEAD_DIM / 32, a CUDA warp-size constant. Coalescing recovers most of it (68% of peak at nnz=65536 vs 75% for a plain copy_), so the impact is moderate.
  • build_slot_mapping_kernel does unchecked page-table indexing where the torch.index_select it replaced was bounds-checked. This is parity with the native kernel, which is equally unchecked, so it should be fixed in both together rather than here.

CDNA3 impact

This is the CDNA3 path. Everything above was measured on gfx942. gfx950 is unmeasured and no behaviour is arch-gated, so CDNA4 gets the same code with unverified numbers.

Test plan

Verified on 8× MI300X (gfx942, ROCm 7.2.0, torch 2.9.1+rocm7.2.0, amd-aiter 0.1.10):

  • Full tests/rocm_tests suite, -n auto --reruns 2 -m "not slow": 27,535 passed, 3,584 skipped, 0 failed, 0 errors.
  • Combined-view and concatenating paths produce bitwise-identical output (torch.equal, not a tolerance) at three shapes including heads=128.
  • Adjacency detection unit-tested on CPU tensors: accepts torch.split views and layer-indexed slices; rejects separate allocations, reversed halves, padded row strides, mismatched dtypes — each of which would otherwise alias the wrong memory.
  • The pre-existing aiter-vs-native append tests assert bit-exactness across dtypes, page sizes and head configs, and cover the new kernel unchanged.
  • The AITER append honors torch's current stream. amd-aiter 0.1.10 reads at::hip::getCurrentHIPStream(), so this passes today; newer AITER moved these ops to a thread-local stream, which would silently put the append on the null stream — and since torch's default stream is stream 0, every other test would still pass. This is the one that would not.
  • test_torch_compile_without_custom_ops_fails un-skipped: its reason ("AITER routes append_paged_kv_cache away from the custom-op path") is obsolete now that the shim goes through register_custom_op, and it was being skipped on exactly the hardware that exercises it.
  • pre-commit run -a

demandal25 and others added 6 commits August 18, 2026 21:28
AITER's MLA kernels read one combined [pages, page_size, 1, ckv+kpe]
buffer, so BatchMLAPagedAttentionWrapper.run() concatenated ckv_cache and
kpe_cache on every call. torch.cat allocates unconditionally, and it
covers the *entire allocated page pool* rather than the live pages, so
decode cost tracked cache capacity instead of the working set.

Measured on MI300X (gfx942), active set pinned at b=8/kv=1024 (9 MB)
while only the pool grows:

    pool      before     after
    0.28 GB   0.281 ms   0.071 ms
    1.12 GB   1.076 ms   0.072 ms
    4.50 GB   2.799 ms   0.071 ms

Production pools fill HBM, so this scaled well past anything usable.

run() now detects when ckv_cache and kpe_cache are adjacent halves of a
single allocation and passes a view, copying nothing. That is already how
vLLM (kv_c_and_k_pe_cache) and SGLang (K_Buffer) store MLA caches -- both
slice with torch.split -- so those callers get the fast path with no code
change. Separate allocations still work, falling back to the copy with a
one-time warning.

At standard shapes this is 4.1x at heads=16 (b=32/kv=32K: 1.424 -> 0.349
ms) and 2.4x at heads=128 (1.806 -> 0.756 ms).

The previous docstring told callers to pre-allocate a combined buffer and
"pass it as both arguments (sliced)", which did nothing -- torch.cat
copies regardless. Replaced with the layout that is now actually honored.

Also adds benchmarks/rocm_benchmarks/bench_mla_hip.py (attn/cat/plan/pool
modes, --separate for the pre-fix layout), and ignores *_meta.json, which
the rocm_profiler ignore block intended to cover but missed.

Co-Authored-By: Claude <noreply@anthropic.com>
plan() reached device memory twice: once inside
_kv_lens_to_last_page_len_cpu (copying kv_indptr and kv_len_arr to derive
last-page lengths) and again for qo_lens.max().item() to derive
max_seqlen_q. On gfx942 a device->host sync costs ~70 us regardless of
payload, and these tensors are only batch+1 int32 elements, so the number
of round-trips was the entire cost.

Concatenating the three index tensors into a single copy makes it one
sync. Measured on MI300X: 120-130 us -> 87-92 us, flat in batch size
(1..256). Inputs already resident on the host now skip the sync
altogether rather than being copied per-tensor.

_kv_lens_to_last_page_len_cpu is unchanged -- it is simply handed tensors
that are already on the host, where its internal .to("cpu") is a no-op.
Every validation check and error message is preserved verbatim.

torch.cat rejects tensors on different devices, and mixed placement was
previously accepted (each tensor was moved independently), so a
per-tensor fallback keeps that working. Covered by a new parametrized
test over cuda / cpu / mixed placement.

Not addressed: the remaining ~70 us is the H2D copy of the computed
last_page_len, since non_blocking=True on pageable memory is still a
blocking copy. Removing it means computing on-device while validating on
host, which needs kv_len_arr on the device and turns circular when it is
not -- not worth a second code path for ~35 us.

Co-Authored-By: Claude <noreply@anthropic.com>
append_paged_kv_cache's AITER path went through aiter.ops.cache, which
meant AITER's per-call @compile_ops dispatch plus seven Python-level
tensor ops to translate FlashInfer's (batch_indices, positions) + page
table into the absolute slot indices AITER wants.

Move it to a csrc_rocm shim on the same pattern as norm/rope/activation:
link the symbol-visible AITER module and call reshape_and_cache_flash
directly, with the slot-mapping arithmetic folded into one kernel.

Measured on MI300X (gfx942), append into a 1024-page cache:

    nnz     python     shim
      32   102.9 us   11.0 us   9.3x
     256   101.2 us   11.2 us   9.0x
    4096   101.8 us   12.1 us   8.4x

The existing aiter-vs-native tests already assert bit-exact equality
across dtypes, page sizes and head configs, so they cover the new
kernel; 50 pass.

Targets amd-aiter 0.1.10, where the entry point takes torch::Tensor& in
namespace aiter and reads its stream from at::hip::getCurrentHIPStream().
Newer AITER (>=0.1.19) de-torched this to aiter_tensor_t and moved to a
thread-local stream set from Python -- under that version the shim would
silently run on the null stream, and since torch's default stream *is*
stream 0, every existing test would still pass. Added an explicit
side-stream test so that upgrade fails loudly instead.

Two smaller fixes while here: _auto_select_kv_append_backend now probes
via is_aiter_available rather than a bare try/except import, and an
explicit backend="aiter" goes through require_aiter so an unsupported
device raises ValueError instead of a loader ImportError -- both
matching what norm/rope/activation already do.

Note this does not remove `import aiter` from the process: the
backend-availability probe still imports the package. What it removes is
the per-call dispatch and the lazy build-on-first-call.

Co-Authored-By: Claude <noreply@anthropic.com>
A review of the three preceding commits turned up one real regression and
several validation gaps. Fixes, most severe first.

**backend="auto" could raise instead of falling back.** ab55de6 replaced
`try: _aiter_cache_module() except Exception: return "native"` with
`is_aiter_available()`, which only probes `import aiter`. But what can now
fail is the JIT shim build -- no hipcc, an unwritable cache dir, or an AITER
whose reshape_and_cache_flash signature no longer matches the forward
declaration -- and that happened later, unguarded. Every default-path caller
was exposed, including ones that never opted into AITER
(tests/attention/test_page.py, test_shared_prefix_kernels.py,
test_torch_compile_hip.py, bench_append_paged_kv_cache.py). Routed through a
new _try_get_page_aiter_module() that returns None and logs on failure. The
None is cached deliberately: functools.cache does not memoize exceptions, so
a raising getter retried the whole AITER rebuild on every call.

**Explicit backend="aiter" skipped the layout and dtype gates.** Those live
in _auto_select_kv_append_backend, which is bypassed when backend != "auto",
and require_aiter checks only arch and importability. The shim reads
page_size from paged_k_cache.size(1) -- num_kv_heads under HND -- so an HND
cache scattered every token to the wrong slot silently. Extracted the
constraints into _aiter_kv_append_supported() so both paths share them.

**The shim was less validated than its native sibling.** csrc_rocm/page.cu
does CHECK_EQ(device) on all seven tensors, checks batch_indices.size(0)
against nnz, and TORCH_CHECKs the launch status; page_aiter.cu did none of
these. It dereferences the index tensors as raw device pointers, so a
host-resident kv_indptr faulted instead of erroring, a short append_key was
read past its end, and a failed launch left slot_mapping uninitialized while
the scatter proceeded into garbage slots. All three now checked.

**4-D ckv/kpe produced a warning saying the opposite of the truth.**
_combined_kv_view rejects dim() != 3, but the docstring 8f14acb deleted told
callers to allocate [num_pages, page_size, 1, ckv+kpe] and pass 4-D slices.
Those callers were told their (genuinely adjacent) buffer was not adjacent,
then got a 5-D tensor AITER rejects. Now an explicit ValueError naming the
3-D form.

**Two docstrings asserted things the code did not do.**
_gather_plan_inputs_on_host claimed host-resident inputs were "a no-op, no
sync at all" -- but torch.cat on CPU is a real allocation, ~1.9 us, where the
old path copied nothing. Host inputs now pass through untouched. The bench
docstring described pre-fix behaviour in the present tense and cited line
numbers eb3ebda and 8f14acb had already invalidated.

Seven new tests, one per fix. Performance is unchanged: append 10.3-12.4 us,
MLA run 0.337 ms at b=32/kv=32K, plan 90 us. 74 tests pass.

Not addressed, noted for the PR: build_slot_mapping_kernel does unchecked
page-table indexing where the replaced torch index_select was bounds-checked
-- this is parity with the native kernel, which is equally unchecked, so it
is a regression only against the old AITER path.

Co-Authored-By: Claude <noreply@anthropic.com>
The full rocm_tests suite failed one test that the targeted runs did not
cover: test_torch_compile_hip.py::test_torch_compile_with_custom_ops, with

    Cannot call numel() on tensor with symbolic sizes/strides
      in append_paged_kv_cache_aiter(...)

Registering the shim through TORCH_LIBRARY_FRAGMENT made it visible to
Dynamo, which places it in the FX graph and fake-runs it during tracing --
and the shim calls numel()/size() on those symbolic-shaped FakeTensors. The
Python path it replaced was not a registered torch op, so Dynamo graph-broke
on it instead and the problem did not exist.

The native kernel already handles this: _append_paged_kv_cache_kernel pairs
@register_custom_op with a @register_fake_op no-op. The AITER path now does
the same, which keeps the C++ off the tracing path entirely.

Worth noting for the other AITER shims: norm/rope/activation call their
compiled modules directly from the public entry points without a custom-op
wrapper, so they have the same exposure if anyone puts them under
torch.compile. Not changed here -- out of scope, and untested.

Co-Authored-By: Claude <noreply@anthropic.com>
A second review pass over 8e6cf89..c60f45b (the fixes themselves were not
yet reviewed) found six issues.

**`except Exception` swallowed MissingJITCacheError.** That is the sentinel
for "JIT disabled and this module is absent from the prebuilt cache";
tests/conftest.py turns it into a skip and records the module so it gets
added to the cache. Downgrading it to a warning would have made
test_append_paged_kv_cache_aiter_auto_routes_on_nhd_fp16 hard-fail under
FLASHINFER_DISABLE_JIT=1, and page_aiter would never join the jit-cache.
Now re-raised.

**The 4-D error's remedy did not work.** It told callers to run
`cache.split([ckv, kpe], dim=-1)`, but on a 4-D buffer that returns 4-D
tensors and re-raises the identical error. Now names `cache.squeeze(2)`
first, so copy-pasting the message actually resolves it.

**The shim's parity claim was false.** csrc_rocm/page.cu enforces
append_key.dim()==3, size(1)==num_kv_heads and size(2)==head_dim; the shim
checked none of them while its comment said "page.cu checks the same". A GQA
caller passing num_q_heads would have had AITER copy the wrong element count
per slot, silently. Added, along with k_scale/v_scale to the device check --
they are dereferenced too, and the op is directly callable.

**hipGetLastError attribution.** It returns and clears the last error for
the thread, not just this launch, so an unrelated earlier failure would be
reported as ours. Drained before the launch, and the comment no longer
claims to catch in-kernel faults, which surface at the next sync.

**append_paged_mla_kv_cache had no fake op** -- and c60f45b exported it on
ROCm, newly exposing a torch.compile-unsafe op: exactly the failure class
that commit set out to fix. Added.

**Two weak tests.** The fallback test asserted isfinite() on a zero-filled
cache, which holds however badly the fallback misbehaves; it now compares
against a native reference and asserts something was written. The
layout/dtype test matched on "NHD|float16", which both parametrizations
satisfy, so the fp32 case did not distinguish itself; each case now matches
its own token.

Also un-skipped test_torch_compile_without_custom_ops_fails. Its skip reason
-- "AITER routes append_paged_kv_cache away from the custom-op path" -- was
made obsolete by c60f45b, and it was being skipped on exactly the hardware
that now needs it. Verified it passes.

Co-Authored-By: Claude <noreply@anthropic.com>
Copilot AI lite review requested due to automatic review settings August 22, 2026 12:24

Copilot AI left a comment

Copy link
Copy Markdown

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 improves ROCm MLA and paged KV-cache append performance by removing unnecessary copies and host synchronizations, while adding compiled AITER dispatch.

Changes:

  • Adds zero-copy MLA cache handling and batched planning transfers.
  • Adds validated, fallback-capable AITER append shims with torch.compile support.
  • Adds tests, benchmarks, ROCm export coverage, and profiler metadata ignores.

Reviewed changes

Copilot reviewed 10 out of 11 changed files in this pull request and generated 6 comments.

Show a summary per file
File Reviewed changes Final review comments
tests/rocm_tests/test_torch_compile_hip.py Updates compile-path coverage.
tests/rocm_tests/test_mla_aiter_hip.py Tests MLA views, planning, and validation.
tests/rocm_tests/test_append_paged_kv_cache_aiter_hip.py Tests append correctness, validation, fallback, and streams. Nit (3 votes): Replace device-wide synchronization with an inter-stream event/dependency check.
flashinfer/page.py Adds AITER dispatch, fallback, validation, and fake-op integration. Critical (1 vote): Isolate module caching by architecture/device or reject cross-architecture reuse. Moderate (2 votes): Normalize accepted int64 metadata or support both dtypes in the shim.
flashinfer/mla_rocm.py Adds zero-copy cache detection and batched planning. Nit (2 votes): Add an explicit one-time guard for the fallback warning.
flashinfer/jit/page.py Defines the AITER page JIT specification.
flashinfer/csrc_rocm/page_aiter.cu Implements slot mapping and AITER append dispatch. Critical (1 vote): Validate V-cache rank, shape, strides, and K/V stride compatibility. Critical (1 vote): Validate kv_indptr rank and length before slot mapping.
flashinfer/csrc_rocm/page_aiter_jit_pybind.cu Registers the compiled append shim.
flashinfer/__init__.py Exports the ROCm MLA append API.
benchmarks/rocm_benchmarks/bench_mla_hip.py Adds MLA performance benchmarks.
.gitignore Ignores profiler metadata outputs.
Suppressed comments (5)

benchmarks/rocm_benchmarks/bench_mla_hip.py:326

  • _alloc_kv() returns adjacent views by default, so run() takes the zero-copy path and performs no pool-sized torch.cat; that copy exists only with --separate. This KernelConfig nevertheless always reports 2 * pool_pages * ... bytes, so the default --mode pool roofline/arithmetic-intensity output attributes GBs of nonexistent traffic and incorrectly grows with pool capacity. Make the byte count conditional on _bench_args.separate and include the active attention traffic.
        # The pool-sized cat dominates: read ckv+kpe, write the combined buffer.
        theoretical_bytes=2 * pool_pages * _QK_HEAD_DIM * 2,

flashinfer/csrc_rocm/page_aiter.cu:72

  • The new shim rejects int64 index tensors, but the public API currently accepts them: the native path coerces batch_indices, positions, kv_indices, and kv_indptr with .int(), and the previous AITER path converted them with .long(). A caller using default torch.arange/int64 tensors therefore succeeds with backend='native' but now gets this error on the AITER auto/explicit path. Preserve the existing API by conditionally converting these inputs before the custom op or dispatching the shim for both integer widths.
  TORCH_CHECK(batch_indices.scalar_type() == at::kInt && positions.scalar_type() == at::kInt &&
                  kv_indices.scalar_type() == at::kInt && kv_indptr.scalar_type() == at::kInt,
              "batch_indices/positions/kv_indices/kv_indptr must be int32");
  TORCH_CHECK(batch_indices.numel() == positions.numel(),
              "batch_indices and positions must have the same length, got ", batch_indices.numel(),
              " vs ", positions.numel());

flashinfer/jit/page.py:34

  • aiter_jitspec_flags() eagerly calls ensure_aiter_lib() before returning, so this generator builds the external AITER module even when FLASHINFER_DISABLE_JIT=1. That bypasses the intended JitSpec.build() MissingJITCacheError path: an uncached or failed AITER dependency raises a generic build error (explicit AITER tests fail instead of being skipped/recorded, while auto fallback can hide the missing page_aiter cache). Defer resolving/building these flags until JIT is allowed, or handle the disabled-JIT path before calling aiter_jitspec_flags().
    extra_include_paths, extra_ldflags = aiter_jitspec_flags("module_cache")

flashinfer/jit/page.py:42

  • The page_aiter spec has a fixed name, while aiter_jitspec_flags() points at a version/architecture-specific cached AITER library. Once page_aiter/build.ninja exists, JitSpec.build() does not rewrite it, so upgrading AITER or changing the selected architecture leaves the shim linked/rpathed to the old library (or silently keeps using it). Include the AITER dependency identity in the spec/cache key or invalidate/rewrite the ninja file when these flags change.
    extra_include_paths, extra_ldflags = aiter_jitspec_flags("module_cache")
    return gen_jit_spec(
        "page_aiter",
        [
            jit_env.FLASHINFER_CSRC_DIR / "page_aiter.cu",
            jit_env.FLASHINFER_CSRC_DIR / "page_aiter_jit_pybind.cu",
        ],
        extra_include_paths=extra_include_paths,
        extra_ldflags=extra_ldflags,

flashinfer/page.py:247

  • This fake implementation is the fix for torch.compile on the newly exported MLA append op, but the compile test exercises only append_paged_kv_cache; no test invokes append_paged_mla_kv_cache with custom ops enabled under torch.compile. A registration or signature regression here would therefore pass the suite. Add a minimal compiled MLA append case.
@register_fake_op("flashinfer::append_paged_mla_kv_cache")
def _fake_append_paged_mla_kv_cache_kernel(
    append_ckv: torch.Tensor,
    append_kpe: torch.Tensor,
    batch_indices: torch.Tensor,
    positions: torch.Tensor,
    ckv_cache: Optional[torch.Tensor],
    kpe_cache: Optional[torch.Tensor],
    kv_indices: torch.Tensor,
    kv_indptr: torch.Tensor,
    kv_last_page_len: torch.Tensor,
) -> None:
    pass

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment on lines +64 to +66
TORCH_CHECK(paged_k_cache.dim() == 4,
"paged_k_cache must be [num_pages, page_size, num_kv_heads, head_dim] (NHD), got ",
paged_k_cache.dim(), " dims");
Comment on lines +67 to +72
TORCH_CHECK(batch_indices.scalar_type() == at::kInt && positions.scalar_type() == at::kInt &&
kv_indices.scalar_type() == at::kInt && kv_indptr.scalar_type() == at::kInt,
"batch_indices/positions/kv_indices/kv_indptr must be int32");
TORCH_CHECK(batch_indices.numel() == positions.numel(),
"batch_indices and positions must have the same length, got ", batch_indices.numel(),
" vs ", positions.numel());
Comment thread flashinfer/mla_rocm.py
Comment on lines +381 to +390
warnings.warn(
"MLA: ckv_cache and kpe_cache are not adjacent halves of a single "
"allocation, so run() must concatenate them on every call. That "
"copy covers the whole allocated page pool, not just the live "
"pages, so it scales with cache capacity. Allocate one "
"[num_pages, page_size, head_dim_ckv + head_dim_kpe] buffer and "
"pass torch.split(buf, [head_dim_ckv, head_dim_kpe], dim=-1) to "
"run() for the zero-copy path.",
UserWarning,
stacklevel=2,
Comment thread flashinfer/page.py
Comment on lines +134 to +139
batch_indices,
positions,
paged_k_cache,
paged_v_cache,
slot_mapping,
"auto",
kv_indices,
kv_indptr,
Comment thread flashinfer/page.py
Comment on lines 36 to +40
@functools.cache
def _aiter_cache_module():
from aiter.ops import cache as aiter_cache
def get_page_aiter_module():
from .jit.page import gen_page_aiter_module

return aiter_cache
return gen_page_aiter_module().build_and_load()
Comment on lines +206 to +208
if stream is not None:
torch.cuda.current_stream().wait_stream(stream)
torch.cuda.synchronize()
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.

2 participants