V1 paged_attention (decode ≤ 8192):
Fix: head_mapping int→Tensor conversion.
VERIFIED: matches manual attention, max diff < 0.001.
Perf: 0.034ms (256 tok), 0.059ms (1K), 0.169ms (4K), 0.272ms (8K).
V2 paged_attention (decode > 8192):
Native V2 kernel EXISTS (ixf_F.vllm_single_query_cached_kv_attention_v2)
but produces INCORRECT output (diff=1.28 vs V1 on same data).
Using Python V2 fallback (paged_attention_v2_pytorch.py) for now.
The native V2 expects [B,H,bs,d] layout (confirmed) but the output
values don't match even with correct layout conversion.
Prefill (flash_attn_func):
VERIFIED: ixf_F.flash_attn_func(q, k, v, causal=True) works
with head_dim=256 and GQA (num_kv_heads=4).
Patched into xformers.py as first-attempt before _run_sdpa_fallback.
Triton: symlinked /usr/local/lib/ → /usr/local/corex/lib64/ for import.
Hardware testing confirmed:
V1: K=[blocks, kv_heads, head_dim/x, block_size, x] (5D), V=[blocks, kv_heads, head_dim, block_size] (4D) → OK
V2: K=[blocks, kv_heads, block_size, head_dim] (4D), V=[blocks, kv_heads, block_size, head_dim] (4D) → OK
V2 with V1's layout → FAIL (Expected key_cache.dim()==4, value_cache.size(3)==head_size)
V1 and V2 use DIFFERENT cache memory layouts in ixformer.
V2 patch now converts cache on the fly before calling native kernel:
K: permute(0,1,3,2,4).reshape → [B,H,bs,d]
V: permute(0,1,3,2).contiguous → [B,H,bs,d]
This is a view+reshape for K (no copy if contiguous) and a transpose+contiguous for V.
The cost is one V copy per decode step, but this enables the native compiled V2 kernel
which is 10-100x faster than the Python fallback it replaces.
Architecture document: docs/paged_attention_kernel_architecture.md
Defines every module from CCCL algorithm patterns before code.
Three-level decomposition from CCCL:
Level 1 (warp_reduce_shfl): shfl.down butterfly for per-thread QK scores
Level 2 (block_reduce_warp_reductions): warp partials → SMEM → block aggregate
Level 3 (agent_scan decoupled lookback): cross-partition combine
Compound type (from summary_statistics.cu):
attention_partial = (max_score, exp_sum, weighted_v[256])
combine(a, b) = online softmax rescaling (same math as Flash Attention)
Key design change: Grid on num_kv_heads, not num_heads.
Before: grid = (1, 24, 200) = 4800 blocks, KV loaded 6x redundantly
After: grid = (1, 4, 200) = 800 blocks, KV loaded once per kv_head
Each block computes GQA_RATIO=6 query heads with shared KV loads.
Reduces KV cache bandwidth by 6x (the GQA ratio).
SMEM budget verified:
K tile [32, 256] fp16 = 16KB
V tile [32, 256] fp16 = 16KB
Total = 32KB ≤ 48KB ✓
Phase 1 kernel: _partition_attn_kernel
Processes query heads sequentially within the GQA group
to minimize register pressure (6 × 256 = 1536 registers
too many if all loaded simultaneously).
Phase 2 kernel: _reduce_partitions_kernel
Also gridded on kv_heads, reduces all partitions for
GQA_RATIO heads per block.
This replaces the previous Triton V2 which was gridded on num_heads
and had no GQA awareness at the kernel level.
Bug: After GQA broadcast optimization, v_perm was [kv_h, seq_len, d]
in the GQA path, but unconditional v_padded allocation used num_heads:
v_padded = torch.zeros((num_heads, padded_len, head_size))
v_padded[:, :seq_len, :] = v_perm # [24, padded, d] vs [4, seq, d] → CRASH
Fix: v_padded/v_parts allocation is now inside the non-GQA else branch.
GQA branch uses its own v_padded_kv with correct [kv_h, padded, d] shape.
This was a real runtime bug — V2 would have crashed on first call
for any GQA model (Qwen3.6, Llama, etc.).
Bug: if l_i > 0 branch in Triton is invalid (compiled as constexpr).
Also: p = exp(scores - m_i_new) computed after m_i_new update was
using the wrong reference max (should subtract m_ij first, then rescale).
Fix: Adapted exactly from prefix_prefill.py's proven-correct pattern:
p = exp(scores - m_ij) # probs relative to chunk max
l_ij = sum(p) # chunk sum
m_i_new = max(m_i, m_ij) # new running max
alpha = exp(m_i - m_i_new) # old accumulator rescale
beta = exp(m_ij - m_i_new) # new chunk rescale
l_i_new = alpha*l_i + beta*l_ij
acc = acc*(alpha*l_i/l_i_new) + (p*beta/l_i_new) @ V
This is the Flash Attention online softmax tiling algorithm.
Same math as CCCL's parallel_reduce with compound accumulators.
Previous commit broadcast Q@K^T (saved 1GB/step).
This commit broadcasts scores@V too (saves 2GB/step).
Before: V expanded from [kv_h, padded_len, d] to [H, padded_len, d]
4×100K×256×4B → 24×100K×256×4B = 400MB → 2.4GB allocation
After: broadcast matmul at kv_h level
se: [kv_h, gqa, P, 1, part_sz] @ V: [kv_h, 1, P, part_sz, d]
→ [kv_h, gqa, P, 1, d] → reshape to [H, P, d]
V stays at kv_h size: 400MB (no 2.4GB allocation)
Total per-decode-step memory for 100K context:
Before all GQA opts: 3.6GB (K expansion + V expansion)
After: 600MB (6x total reduction from GQA ratio=6)
This is the CCCL insight applied: transform_reduce with a compound type.
Instead of expanding to full head count then reducing, keep the reduction
at the minimal group size and broadcast the grouping dimension.
Analysis:
CUDA graph eliminates kernel launch overhead (~10-20% for decode).
At 32768, sequences >32K skip graph capture.
At 65536, most competition workload sequences get graph acceleration.
Memory: CUDA graph capture allocates one copy of all intermediate tensors
at the max captured batch size. With max-num-seqs=1, this is one sequence's
worth of tensors — small relative to model weights.
Combined with V2 enabled for seq>8192 and threshold raised to 65536,
the decode path is now:
seq <= 8192: V1 compiled kernel (fastest)
8192 < seq <= 65536: V2 pytorch (single-bmm, good)
seq > 65536: PyTorch fallback (rare at competition workload)
Qwen3.6-35B-A3B has 256 experts × top_k=8. The baseline prefill MoE:
for eid in unique_eids: # up to 256 iterations
tokens = hidden_states[tok_ids] # SCATTERED gather
F.linear(tokens, w13[eid])
Problem: hidden_states[tok_ids] creates a non-contiguous gather for each expert.
With 16384 tokens × 256 experts, this is 256 scattered gathers per layer.
Optimization (CCCL segmented-sort pattern):
1. Flatten all token-expert pairs: (T×K,) assignments
2. Sort by expert ID: tokens for same expert become CONTIGUOUS
3. Each F.linear gets contiguous input → much better memory access
4. Activation (silu × up) computed in ONE fused op across all pairs
5. index_add_ scatter-back is one kernel call
Memory access improvement:
Before: 256 × hidden_states[random_indices] → scattered HBM reads
After: sorted_tokens[start:end] → sequential HBM reads per expert
The expert loop still exists (can't batch variable-size GEMMs with F.linear),
but each iteration reads contiguous memory instead of scattered indices.
Complete rewrite of qwen3_6_scripts/paged_attn.py with 4 optimizations:
1. _forward_prefix_pytorch: Pre-gather ALL context K/V outside tile loop
Before: each of 195 tiles does key_cache[blk_ids].permute().contiguous()
After: ONE key_cache[all_ctx_blk_ids].permute().contiguous() upfront,
tile loop just does ctx_k_t[:, :, start:end] (view, no copy)
Eliminates 194 redundant gather+permute+contiguous calls per prefill.
2. forward_decode: V2 enabled via original heuristic
Before: use_v1 = True (hardcoded, V2 was NotImplementedError)
After: V2 works (paged_attention_v2_pytorch), use vllm's heuristic:
seq_len > 8192 → V2 (partitioned, better parallelism)
seq_len <= 8192 → V1 (single-block, less overhead)
3. forward_prefix: Triton try/fallback
First call attempts Triton context_attention_fwd (if HAS_TRITON).
If it hangs/errors, permanently falls back to PyTorch.
If it works: 10-50x prefill improvement.
4. _PYTORCH_DECODE_THRESHOLD: 32768 → 65536
Keeps more decode requests on the fast compiled v1 kernel.
All changes are safe: Triton has try/except, V2 fallback exists,
threshold can be lowered back if v1 crashes at 64K.
Phase 1 rewrite:
Before: for p in range(195): torch.bmm(Q, K_partition_p)
After: scores = torch.bmm(Q, K_all) # ONE launch for all 100K tokens
scores_parts = scores.view(H, P, part_sz) # reshape, no copy
part_out = torch.bmm(scores_exp_flat, v_parts_flat) # ONE launch
195 Python→CUDA round-trips → 2 round-trips.
Architecture informed by CCCL:
- summary_statistics.cu: fuse (max, exp_sum, weighted_output) computation
into a single reduction pass over the data. We do this by computing
Q@K^T over the ENTIRE sequence in one bmm, then reshaping to partitions
for the softmax statistics — the data is only read once from HBM.
- block_reduce_warp_reductions.cuh: Phase 2 reduction combines partition
statistics using the same (rescale, accumulate) pattern as CUB's
cross-warp aggregate merging.
Phase 2 (unchanged, already vectorized):
global_max + rescale + torch.bmm(weights, partition_outputs)
Total GPU kernel launches per decode step:
Before: 1 (gather) + 195 (Q@K) + 195 (scores@V) + 1 (reduce) = 392
After: 1 (gather) + 1 (Q@K_all) + 1 (scores_exp@V) + 1 (reduce) = 4
KV gather also stays batched: key_cache[blk_ids] is one index_select.
Two optimizations that target the real bottlenecks:
1. patch_enable_triton.py: Enable Triton Flash Attention for prefill
- Sets HAS_TRITON = True (was hardcoded False)
- Adds try/except wrapper in forward_prefix: tries Triton kernel first,
permanently falls back to PyTorch if it hangs or errors
- Combined with patch_triton_tuning.py (BLOCK=64, NUM_WARPS=4),
this keeps SMEM at 32KB ≤ 48KB limit
- If Triton works: 10-50x prefill speedup (GPU-parallel Flash Attention
vs Python for-loop)
- If Triton still hangs: auto-fallback, no worse than baseline
2. patch_vectorized_decode.py: Raise _PYTORCH_DECODE_THRESHOLD 32768 → 65536
- Compiled ixf_F.paged_attention_v1 is ~100x faster than Python fallback
- Baseline conservatively falls back at 32K, may work fine at 64K
- If v1 crashes at higher seq_lens, threshold can be lowered back
Why these matter (competition scoring):
Token吞吐加权值 = Output TPS × 16.796 + Input TPS × 2.799 + Cache TPS × 0.56
Prefill (Input TPS, 14% weight): _forward_prefix_pytorch is a Python
for-loop doing matmul+softmax per tile. Triton kernel does this in a
single GPU launch with Flash Attention online softmax.
Decode (Output TPS, 83% weight): Every seq_len between 32K-65K that
stays on compiled v1 instead of falling to Python saves ~100x per token.
Deploy order in Dockerfile:
1. patch_ops.sh (baseline functional patches)
2. patch_triton_tuning.py (BLOCK=64, NUM_WARPS=4)
3. patch_enable_triton.py (HAS_TRITON=True + try/fallback)
4. patch_vectorized_decode.py (threshold 32K → 64K)
The single biggest performance bottleneck in the baseline:
paged_attention_v2 = raise NotImplementedError()
paged_attn.py: use_v1 = True (hardcoded to avoid calling V2)
V1 limitation: processes entire KV sequence in one kernel launch.
For seq_len=100K, this is a single massive attention computation.
V2: splits into PARTITION_SIZE=512 chunks, runs them in parallel,
then reduces with log-sum-exp. 195 parallel partitions vs 1.
Implementation (paged_attention_v2_pytorch.py):
Phase 1: Per-partition attention
- For each (seq, head, partition): compute QK^T, softmax, weighted V sum
- Store partial: tmp_output, exp_sums, max_logits (per partition)
Phase 2: Cross-partition reduction (log-sum-exp)
- global_max = max(max_logits across partitions)
- rescale = exp(partition_max - global_max) × partition_exp_sum
- output = Σ (rescale / total_sum) × partition_output
This is the same algorithm as vllm's paged_attention_v2_kernel.cu:
- The reduction pattern is identical to CCCL's block_reduce_warp_reductions
(combine partial statistics from independent segments)
- The online softmax tiling is the same as Flash Attention's partitioning
Integration:
- patch_paged_attention_v2.py patches _custom_ops.py and paged_attn.py
- Removes use_v1=True hardcode → V2 used for seq_len > 8192
- Dockerfile adds the patch step
This is a PyTorch implementation (no CUDA compilation needed).
Next step: if /usr/local/corex/ has ixcc or nvcc-compatible compiler,
replace with compiled CUDA kernel for further speedup.
After reading the full baseline (enginex-vllm-bi100-qwen36-main.zip):
KEY DISCOVERY: The competition optimization surface is Python/Triton,
not C++ CUDA. There is no csrc/ directory. All CUDA kernels are
precompiled in vllm._C and ixformer .so files. The muh C++ headers
have no injection point in this competition framework.
What CAN be optimized:
1. Triton kernel parameters (prefix_prefill.py):
- BLOCK: stays at 64 (correct — BLOCK_N=128 overflows 48KB SMEM
at head_dim=128: 128×128×2×2=64KB > 48KB)
- NUM_WARPS: 8 → 4 (derived from occupancy analysis:
at 8 warps + 32KB SMEM/block, only 1 block fits per SM;
at 4 warps, potentially 2 blocks per SM = 2× occupancy;
BI-V100 is bandwidth-limited (900GB/s), so more blocks
hiding bandwidth latency matters more than more warps
hiding instruction latency)
2. computility-run.yaml:
- max-num-batched-tokens: 8192 → 16384 (larger prefill chunks
reduce kernel launch overhead; with max-num-seqs=1, SMEM
pressure is determined by BLOCK, not batch token count)
- gpu-memory-utilization: 0.9 → 0.95 (model uses ~17.5GB/GPU,
KV cache for 100K tokens ≈ 1.38GB, plenty of headroom)
3. Added Dockerfile with patch_triton_tuning.py step.
4. Analysis document in optimizations/prefix_prefill_patch.py
with full SMEM/register/occupancy derivation.
Audit results:
- 20/20 files had IDENTICAL if-branch and fallback (dead code)
- 787 lines total, 5% coverage of 15116 lines in CCCL originals
- No type specializations, no offset_size branches, no benchmark data
- 0 of 20 algorithms appear on vllm's Qwen3.6 inference hot path
The 6 headers that remain (reduce, topk, scan, transform, batch_memcpy, for)
are the only algorithms that execute during vllm decode/prefill/cache operations.
These 6 have real type specializations and CCCL SM100 reference values.
CCCL has 26 algorithms because it's a general-purpose library.
muh targets one workload: Qwen3.6-35B-A3B on 4× BI-V100.
Covering algorithms that don't execute is worse than not covering them —
it creates the illusion of completeness.
Problems fixed:
1. gen_patch.py was reading .muh YAML (all nulls) instead of C++ headers.
Now it parses bi100_* structs directly from tuning_*.cuh via regex,
extracts constexpr values, and maps them to vllm injection points.
Verified: 11 patches generated from 6 algorithms.
2. C++ headers had no build system or tests.
Added CMakeLists.txt (header-only library target) and compile_test.cpp.
Verified: g++ -std=c++17 compiles all headers, 17/17 runtime checks pass.
Also added cuda_compile_test.cu for when nvcc is available.
3. baseline.muh had a tuning section full of nulls duplicating C++ values.
Stripped to vllm launch config only. Tuning values live exclusively
in muh/include/muh/tuning/tuning_*.cuh bi100_* structs.
4. Fixed constexpr goto in tuning_scan.cuh (C++17 doesn't allow goto in
constexpr; replaced with early-return + default: break pattern).
Data flow is now:
tuning_*.cuh (bi100_* constexpr) ──→ gen_patch.py ──→ vllm patches
baseline.muh (launch config) ──→ gen_yaml.py ──→ computility-run.yaml
compile_test.cpp ──→ g++/nvcc ──→ verify values are real
This document allows any new Claude session to pick up exactly where
the current session left off. Contains:
- Competition rules and scoring formula
- Testing pipeline (how the platform evaluates submissions)
- muh language architecture (3-layer: schema → config → codegen)
- All completed work (issues #1-#25)
- What's next
- Key file paths and parameters