Implements variable-length (varlen) FlashAttention that processes mixed-length batches without padding waste: - New CUDA kernel flash_attention_varlen_forward with BLOCK_Q=64 / BLOCK_K=64 tiling; grid=(ceil(max_seqlen_q/64), num_heads, 1). Each block uses a linear scan over cu_seqlens_q to identify its owning sequence and exits early when past sequence end. - New Rust module flash_varlen_forward: always-compiled CPU simulation (varlen_attention_cpu) for testing + #[cfg(cuda)] FlashVarlenKernel. - SdpaBackend::VarLen variant added to backend_selector. - 8 new CPU-only tests; total test count: 50. Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
47 KiB
Varlen Flash Attention Implementation Plan
For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (
- [ ]) syntax for tracking.
Goal: Add variable-length (varlen) packed-sequence support to rtx-flash-attention so mixed-length batches can be processed without padding waste, using a cu_seqlens prefix-sum descriptor.
Architecture: A new Rust file flash_varlen_forward.rs provides a pure-Rust CPU simulation (varlen_attention_cpu) that is always compiled, plus a #[cfg(feature = "cuda")]-gated FlashVarlenKernel struct that loads a new CUDA kernel via NVRTC. A new SdpaBackend::VarLen variant is added to backend_selector.rs. The CUDA kernel maps each grid block to a (q-tile, head) pair using a linear scan over cu_seqlens_q, then runs tiled online-softmax attention with BLOCK_Q=64 / BLOCK_K=64 tiles.
Tech Stack: Rust 2021, cudarc 0.16 (CUDA feature), half::bf16, existing FlashError/FlashResult error types.
Global Constraints
- Only modify files inside
crates/training/rtx-flash-attention/ cargo check -p rtx-flash-attentionmust produce zero errors/warnings after each taskcargo test -p rtx-flash-attention --libmust pass (42 existing + 8 new = 50 tests total after Task 3)- No new Cargo dependencies
- No GPU hardware required to run tests (all 8 new tests use
varlen_attention_cpu) - Clippy-clean: no
#[allow(clippy::...)]suppressions without a comment explaining why
File Map
| Status | Path | Role |
|---|---|---|
| CREATE | src/kernels/cuda/flash_attention_varlen.cu |
CUDA kernel — varlen forward pass |
| CREATE | src/kernels/flash_varlen_forward.rs |
Rust wrapper + CPU simulation |
| MODIFY | src/kernels/mod.rs |
Export the new module and re-exports |
| MODIFY | src/backend_selector.rs |
Add SdpaBackend::VarLen variant |
Task 1: CUDA Kernel Source — flash_attention_varlen.cu
Files:
- Create:
crates/training/rtx-flash-attention/src/kernels/cuda/flash_attention_varlen.cu
Interfaces:
-
Produces: CUDA
__global__functionflash_attention_varlen_forwardwith the signature shown in Step 1. -
Step 1: Write the CUDA kernel file
Create the file with the following content exactly. The key design decisions are:
BLOCK_Q = 64,BLOCK_K = 64— tile dimensions (match spec)- Grid:
(ceil(max_seqlen_q / BLOCK_Q), num_heads, 1)— blocks identified by(q_tile_idx, head_idx) - Sequence identification: linear scan over
cu_seqlens_qin the block prologue - Accumulation in
f32; output stored as__nv_bfloat16 - Online softmax (Dao FA2 algorithm): running
m(row max) andl(denominator)
// Variable-length FlashAttention forward kernel (packed sequences)
//
// Preconditions (caller-enforced):
// q, k, v are packed [total_tokens, num_heads, head_dim] in row-major, dtype __nv_bfloat16
// out is same shape, pre-allocated
// lse is [total_tokens, num_heads] in f32
// cu_seqlens_q[0] == 0, cu_seqlens_q[batch_size] == total_tokens_q
// cu_seqlens_k[0] == 0, cu_seqlens_k[batch_size] == total_tokens_k
// max_seqlen_q >= any individual sequence q length
// 0 < head_dim <= 256, head_dim % 8 == 0
//
// Grid/block assignment:
// blockDim = (128, 1, 1) — 4 warps
// gridDim = (ceil(max_seqlen_q / BLOCK_Q), num_heads, 1)
#include <cuda_runtime.h>
#include <cuda_bf16.h>
#include <stdint.h>
#include <float.h>
#define BLOCK_Q 64
#define BLOCK_K 64
// Per-token element offset in packed layout [total_tokens, num_heads, head_dim]
// token_idx is the absolute position in the packed buffer.
__device__ __forceinline__ int elem_offset(int token_idx, int head_idx, int d, int num_heads, int head_dim) {
return (token_idx * num_heads + head_idx) * head_dim + d;
}
// Find sequence index s such that cu_seqlens[s] <= pos < cu_seqlens[s+1].
// Linear scan is correct for all batch sizes; constant-time for batch <= 64.
__device__ __forceinline__ int find_sequence(const int* cu_seqlens, int batch_size, int pos) {
int s = 0;
while (s < batch_size - 1 && cu_seqlens[s + 1] <= pos) {
s++;
}
return s;
}
extern "C" __global__ void flash_attention_varlen_forward(
const __nv_bfloat16* __restrict__ q,
const __nv_bfloat16* __restrict__ k,
const __nv_bfloat16* __restrict__ v,
__nv_bfloat16* __restrict__ out,
float* __restrict__ lse,
const int* __restrict__ cu_seqlens_q,
const int* __restrict__ cu_seqlens_k,
int max_seqlen_q,
int batch_size,
int num_heads,
int head_dim,
float softmax_scale,
int causal
) {
// -------------------------------------------------------------------------
// Identify which q-tile and head this block handles
// -------------------------------------------------------------------------
const int q_tile_idx = blockIdx.x; // which BLOCK_Q tile within max_seqlen_q
const int head_idx = blockIdx.y;
const int tid = threadIdx.x; // 0..127
const int q_tile_start_global = q_tile_idx * BLOCK_Q;
// -------------------------------------------------------------------------
// Identify which sequence owns this q-tile (linear scan over cu_seqlens_q)
// -------------------------------------------------------------------------
int s = find_sequence(cu_seqlens_q, batch_size, q_tile_start_global);
const int seq_start_q = cu_seqlens_q[s];
const int seq_end_q = cu_seqlens_q[s + 1];
const int seq_len_q = seq_end_q - seq_start_q;
const int seq_start_k = cu_seqlens_k[s];
const int seq_end_k = cu_seqlens_k[s + 1];
const int seq_len_k = seq_end_k - seq_start_k;
// Local q-tile start within this sequence
const int local_q_start = q_tile_start_global - seq_start_q;
// Early exit: this block is completely past the end of sequence s
if (local_q_start >= seq_len_q) return;
const int q_rows_this_tile = min(BLOCK_Q, seq_len_q - local_q_start);
// -------------------------------------------------------------------------
// Shared memory layout:
// [0 .. BLOCK_Q * head_dim) : Q tile (bf16)
// [BLOCK_Q*D*2 .. BLOCK_Q*D*2 + BLOCK_K*D*2) : K tile (bf16)
// [next .. next + BLOCK_K*D*2) : V tile (bf16)
// We access shared mem via float-aligned pointers for vectorised loads.
// -------------------------------------------------------------------------
extern __shared__ char smem_raw[];
__nv_bfloat16* smem_q = (__nv_bfloat16*)smem_raw;
__nv_bfloat16* smem_k = smem_q + BLOCK_Q * head_dim;
__nv_bfloat16* smem_v = smem_k + BLOCK_K * head_dim;
// -------------------------------------------------------------------------
// Load Q tile into shared memory
// Each of 128 threads loads elements strided across [q_row, d].
// -------------------------------------------------------------------------
for (int i = tid; i < q_rows_this_tile * head_dim; i += blockDim.x) {
int row = i / head_dim;
int col = i % head_dim;
int global_token = seq_start_q + local_q_start + row;
smem_q[row * head_dim + col] = q[elem_offset(global_token, head_idx, col, num_heads, head_dim)];
}
// Zero-pad rows that don't exist in this tile
for (int i = q_rows_this_tile * head_dim + tid; i < BLOCK_Q * head_dim; i += blockDim.x) {
smem_q[i] = __float2bfloat16(0.0f);
}
__syncthreads();
// -------------------------------------------------------------------------
// Per-row accumulators in registers (one row per thread — split 64 rows
// across 128 threads, so each thread "owns" rows tid/2 with half the threads
// handling even/odd columns via the inner d-loop).
// For simplicity we assign each thread one q-row cyclically.
// -------------------------------------------------------------------------
// We unroll over q rows: thread tid handles q-row (tid) if tid < BLOCK_Q.
// For 128 threads and BLOCK_Q=64, threads 0..63 process one row each.
// Threads 64..127 do nothing except participate in K/V loads.
// Accumulators: acc[d] for output, running_m and running_l for online softmax
float acc[256]; // max supported head_dim
float running_m = -FLT_MAX;
float running_l = 0.0f;
for (int d = 0; d < head_dim; d++) acc[d] = 0.0f;
const int my_q_row = tid; // my row within the q tile (tid 0..63 active, 64..127 idle)
const int my_active = (my_q_row < q_rows_this_tile) ? 1 : 0;
// -------------------------------------------------------------------------
// Outer loop over K/V tiles
// -------------------------------------------------------------------------
const int num_k_tiles = (seq_len_k + BLOCK_K - 1) / BLOCK_K;
for (int k_tile = 0; k_tile < num_k_tiles; k_tile++) {
const int k_tile_start = k_tile * BLOCK_K;
const int k_rows_this_tile = min(BLOCK_K, seq_len_k - k_tile_start);
// ------------------------------------------------------------------
// Load K tile into shared memory
// ------------------------------------------------------------------
for (int i = tid; i < k_rows_this_tile * head_dim; i += blockDim.x) {
int row = i / head_dim;
int col = i % head_dim;
int global_token = seq_start_k + k_tile_start + row;
smem_k[row * head_dim + col] = k[elem_offset(global_token, head_idx, col, num_heads, head_dim)];
}
for (int i = k_rows_this_tile * head_dim + tid; i < BLOCK_K * head_dim; i += blockDim.x) {
smem_k[i] = __float2bfloat16(0.0f);
}
// ------------------------------------------------------------------
// Load V tile into shared memory
// ------------------------------------------------------------------
for (int i = tid; i < k_rows_this_tile * head_dim; i += blockDim.x) {
int row = i / head_dim;
int col = i % head_dim;
int global_token = seq_start_k + k_tile_start + row;
smem_v[row * head_dim + col] = v[elem_offset(global_token, head_idx, col, num_heads, head_dim)];
}
for (int i = k_rows_this_tile * head_dim + tid; i < BLOCK_K * head_dim; i += blockDim.x) {
smem_v[i] = __float2bfloat16(0.0f);
}
__syncthreads();
if (!my_active) { __syncthreads(); continue; }
// ------------------------------------------------------------------
// Compute attention scores for my_q_row x all k_rows: S[j] = Q[my_q_row] . K[j]
// ------------------------------------------------------------------
float S[BLOCK_K];
for (int j = 0; j < k_rows_this_tile; j++) {
float dot = 0.0f;
for (int d = 0; d < head_dim; d++) {
dot += __bfloat162float(smem_q[my_q_row * head_dim + d]) *
__bfloat162float(smem_k[j * head_dim + d]);
}
S[j] = dot * softmax_scale;
// Causal mask: token (seq_start_q + local_q_start + my_q_row) cannot
// attend to token (seq_start_k + k_tile_start + j) if j > my_q_row+local_q_start.
if (causal) {
int q_pos = local_q_start + my_q_row;
int k_pos = k_tile_start + j;
if (k_pos > q_pos) S[j] = -FLT_MAX;
}
// Pad out-of-sequence k positions
if (j >= k_rows_this_tile) S[j] = -FLT_MAX;
}
for (int j = k_rows_this_tile; j < BLOCK_K; j++) S[j] = -FLT_MAX;
// ------------------------------------------------------------------
// Online softmax update (FA2 algorithm 1)
// ------------------------------------------------------------------
float tile_m = -FLT_MAX;
for (int j = 0; j < k_rows_this_tile; j++) tile_m = fmaxf(tile_m, S[j]);
float m_new = fmaxf(running_m, tile_m);
// Rescale existing accumulator
float rescale = expf(running_m - m_new);
for (int d = 0; d < head_dim; d++) acc[d] *= rescale;
float l_rescale = running_l * rescale;
// Accumulate weighted V
float tile_l = 0.0f;
for (int j = 0; j < k_rows_this_tile; j++) {
float p = expf(S[j] - m_new);
tile_l += p;
for (int d = 0; d < head_dim; d++) {
acc[d] += p * __bfloat162float(smem_v[j * head_dim + d]);
}
}
running_m = m_new;
running_l = l_rescale + tile_l;
__syncthreads();
}
// -------------------------------------------------------------------------
// Write output and LSE
// -------------------------------------------------------------------------
if (!my_active) return;
float inv_l = (running_l > 0.0f) ? (1.0f / running_l) : 0.0f;
int global_token_out = seq_start_q + local_q_start + my_q_row;
for (int d = 0; d < head_dim; d++) {
out[elem_offset(global_token_out, head_idx, d, num_heads, head_dim)] =
__float2bfloat16(acc[d] * inv_l);
}
// lse[token, head] = log(l) + m (log-sum-exp in standard form)
float lse_val = (running_l > 0.0f) ? (logf(running_l) + running_m) : -FLT_MAX;
lse[global_token_out * num_heads + head_idx] = lse_val;
}
- Step 2: Verify file exists
ls -la /slab/projects/rustyverse/rustytorch/crates/training/rtx-flash-attention/src/kernels/cuda/flash_attention_varlen.cu
Expected: file listed with non-zero size.
Task 2: Rust Wrapper + CPU Simulation — flash_varlen_forward.rs
Files:
- Create:
crates/training/rtx-flash-attention/src/kernels/flash_varlen_forward.rs
Interfaces:
-
Consumes:
crate::error::{FlashError, FlashResult}(always);cudarc::driver::{CudaContext, CudaSlice, CudaStream, LaunchConfig, PushKernelArg},cudarc::nvrtc::compile_ptx,half::bf16(cuda feature only). -
Produces:
pub fn varlen_attention_cpu(q: &[f32], k: &[f32], v: &[f32], cu_seqlens: &[usize], num_heads: usize, head_dim: usize, softmax_scale: f32, causal: bool) -> Vec<f32>— always available.#[cfg(feature = "cuda")] pub struct FlashVarlenKernelwith:pub fn new(ctx: &Arc<CudaContext>) -> FlashResult<Self>pub fn forward(&self, q: &CudaSlice<bf16>, k: &CudaSlice<bf16>, v: &CudaSlice<bf16>, out: &mut CudaSlice<bf16>, lse: &mut CudaSlice<f32>, cu_seqlens_q: &CudaSlice<i32>, cu_seqlens_k: &CudaSlice<i32>, max_seqlen_q: usize, batch_size: usize, num_heads: usize, head_dim: usize, softmax_scale: f32, causal: bool, stream: &Arc<CudaStream>) -> FlashResult<()>
-
Step 1: Write the Rust file
Create /slab/projects/rustyverse/rustytorch/crates/training/rtx-flash-attention/src/kernels/flash_varlen_forward.rs with the following content:
//! Variable-length (varlen) Flash Attention forward pass.
//!
//! This module provides:
//! - [`varlen_attention_cpu`] — pure-Rust O(n²) reference implementation for
//! testing and CPU fallback. No GPU required.
//! - [`FlashVarlenKernel`] — CUDA kernel wrapper (cuda feature only).
//!
//! # Packed layout
//!
//! All tensors use **packed** (also called "varlen" or "jagged") layout:
//! instead of `[batch, heads, seq_len, head_dim]` with padding, sequences are
//! concatenated along the token axis: `[total_tokens, heads, head_dim]`.
//!
//! A `cu_seqlens` prefix-sum array of length `batch_size + 1` describes the
//! boundaries:
//! - `cu_seqlens[0] = 0`
//! - `cu_seqlens[b + 1] = cu_seqlens[b] + seqlen_b`
//! - `cu_seqlens[batch_size] = total_tokens`
//!
//! Sequence `b` occupies token indices `cu_seqlens[b] .. cu_seqlens[b+1]`.
use crate::error::FlashResult;
#[cfg(feature = "cuda")]
use crate::error::FlashError;
#[cfg(feature = "cuda")]
use cudarc::driver::{CudaContext, CudaSlice, CudaStream, LaunchConfig, PushKernelArg};
#[cfg(feature = "cuda")]
use cudarc::nvrtc::compile_ptx;
#[cfg(feature = "cuda")]
use half::bf16;
#[cfg(feature = "cuda")]
use std::sync::Arc;
#[cfg(feature = "cuda")]
const VARLEN_CUDA_SOURCE: &str =
include_str!("cuda/flash_attention_varlen.cu");
// =============================================================================
// CPU reference implementation
// =============================================================================
/// Variable-length attention on the CPU using naive O(n²) per-sequence dot-product.
///
/// # Layout
///
/// `q`, `k`, `v` are packed flat slices in row-major order:
/// `[total_tokens * num_heads * head_dim]` where the logical index for
/// token `t`, head `h`, dimension `d` is `(t * num_heads + h) * head_dim + d`.
///
/// `cu_seqlens` has length `batch_size + 1`; `cu_seqlens[b..b+1]` is a half-open
/// range of token indices for sequence `b`. Sequences with zero length
/// (i.e. `cu_seqlens[b] == cu_seqlens[b+1]`) are silently skipped.
///
/// # Returns
///
/// A `Vec<f32>` of length `total_tokens * num_heads * head_dim` — the attention
/// output in the same packed layout.
///
/// # Panics
///
/// Panics if `cu_seqlens` is empty (must have at least one entry: `[0]`).
///
/// # Example
///
/// ```
/// use rtx_flash_attention::kernels::varlen_attention_cpu;
///
/// let head_dim = 4;
/// let num_heads = 1;
/// // Two sequences: seq0 = 2 tokens, seq1 = 3 tokens → 5 total tokens
/// let cu_seqlens = vec![0usize, 2, 5];
/// let total = cu_seqlens[cu_seqlens.len() - 1];
/// let scale = 1.0 / (head_dim as f32).sqrt();
///
/// let q: Vec<f32> = (0..total * num_heads * head_dim).map(|i| i as f32 * 0.01).collect();
/// let k = q.clone();
/// let v = q.clone();
///
/// let out = varlen_attention_cpu(&q, &k, &v, &cu_seqlens, num_heads, head_dim, scale, false);
/// assert_eq!(out.len(), total * num_heads * head_dim);
/// ```
pub fn varlen_attention_cpu(
q: &[f32],
k: &[f32],
v: &[f32],
cu_seqlens: &[usize],
num_heads: usize,
head_dim: usize,
softmax_scale: f32,
causal: bool,
) -> Vec<f32> {
assert!(!cu_seqlens.is_empty(), "cu_seqlens must have at least one entry");
let batch_size = cu_seqlens.len() - 1;
let total_tokens = cu_seqlens[batch_size];
let mut output = vec![0.0f32; total_tokens * num_heads * head_dim];
// Helper: flat index for packed layout [total_tokens, num_heads, head_dim]
let idx = |token: usize, head: usize, d: usize| -> usize {
(token * num_heads + head) * head_dim + d
};
for b in 0..batch_size {
let seq_start = cu_seqlens[b];
let seq_end = cu_seqlens[b + 1];
let seq_len = seq_end - seq_start;
// Empty sequence — skip without touching output
if seq_len == 0 {
continue;
}
for h in 0..num_heads {
// Compute attention weights for every (query, key) pair in this sequence.
// scores[qi * seq_len + ki] = Q[qi] . K[ki] * scale
let mut scores = vec![0.0f32; seq_len * seq_len];
for qi in 0..seq_len {
for ki in 0..seq_len {
// Causal: query at position qi cannot attend to key at ki > qi
if causal && ki > qi {
scores[qi * seq_len + ki] = f32::NEG_INFINITY;
continue;
}
let mut dot = 0.0f32;
for d in 0..head_dim {
dot += q[idx(seq_start + qi, h, d)] * k[idx(seq_start + ki, h, d)];
}
scores[qi * seq_len + ki] = dot * softmax_scale;
}
}
// Softmax row-by-row (online — subtract row max for numerical stability)
let mut weights = vec![0.0f32; seq_len * seq_len];
for qi in 0..seq_len {
let row = &scores[qi * seq_len..(qi + 1) * seq_len];
let row_max = row
.iter()
.copied()
.fold(f32::NEG_INFINITY, f32::max);
let exps: Vec<f32> = row.iter().map(|&s| (s - row_max).exp()).collect();
let sum: f32 = exps.iter().sum();
let inv_sum = if sum > 0.0 { 1.0 / sum } else { 0.0 };
for ki in 0..seq_len {
weights[qi * seq_len + ki] = exps[ki] * inv_sum;
}
}
// Output: O[qi, d] = Σ_ki weights[qi, ki] * V[ki, d]
for qi in 0..seq_len {
for d in 0..head_dim {
let mut acc = 0.0f32;
for ki in 0..seq_len {
acc += weights[qi * seq_len + ki]
* v[idx(seq_start + ki, h, d)];
}
output[idx(seq_start + qi, h, d)] = acc;
}
}
}
}
output
}
// =============================================================================
// CUDA kernel wrapper
// =============================================================================
/// Compiled FlashAttention varlen forward kernel for a single CUDA device.
///
/// Create once per device; reuse across calls. The compiled PTX is cached
/// inside the struct.
///
/// # Feature gate
///
/// Only available when the `cuda` feature is enabled.
#[cfg(feature = "cuda")]
pub struct FlashVarlenKernel {
module: Arc<cudarc::driver::CudaModule>,
_ctx: Arc<CudaContext>,
}
#[cfg(feature = "cuda")]
impl FlashVarlenKernel {
/// Compile and load the varlen CUDA kernel into `ctx`.
///
/// # Errors
///
/// Returns [`FlashError::Cuda`] if NVRTC compilation fails or the PTX
/// cannot be loaded into the device context.
pub fn new(ctx: &Arc<CudaContext>) -> FlashResult<Self> {
let ptx = compile_ptx(VARLEN_CUDA_SOURCE)
.map_err(|e| FlashError::cuda(format!("varlen NVRTC compilation failed: {e:?}")))?;
let module = ctx
.load_module(ptx)
.map_err(|e| FlashError::cuda(format!("varlen PTX load failed: {e:?}")))?;
Ok(Self {
module,
_ctx: Arc::clone(ctx),
})
}
/// Launch the varlen forward kernel on `stream`.
///
/// # Arguments
///
/// - `q`, `k`, `v` — packed `[total_tokens, num_heads, head_dim]` in BF16.
/// - `out` — pre-allocated output buffer, same shape as `q`.
/// - `lse` — pre-allocated log-sum-exp buffer `[total_tokens, num_heads]` in f32.
/// - `cu_seqlens_q` / `cu_seqlens_k` — device buffers of length `batch_size + 1`.
/// - `max_seqlen_q` — maximum query sequence length across the batch.
/// - `batch_size` — number of sequences in the batch.
///
/// # Safety invariants (enforced by caller)
///
/// - All device buffers must be allocated on the same device as `ctx`.
/// - Buffer lengths: `q.len() >= total_tokens_q * num_heads * head_dim`.
/// - `cu_seqlens_q[0] == 0`, `cu_seqlens_q[batch_size] == total_tokens_q`.
/// - `head_dim <= 256` and `head_dim % 8 == 0`.
///
/// # Errors
///
/// Returns [`FlashError::Cuda`] if the kernel symbol is not found or the
/// driver rejects the launch configuration.
#[allow(clippy::too_many_arguments)] // kernel interface requires all parameters
pub fn forward(
&self,
q: &CudaSlice<bf16>,
k: &CudaSlice<bf16>,
v: &CudaSlice<bf16>,
out: &mut CudaSlice<bf16>,
lse: &mut CudaSlice<f32>,
cu_seqlens_q: &CudaSlice<i32>,
cu_seqlens_k: &CudaSlice<i32>,
max_seqlen_q: usize,
batch_size: usize,
num_heads: usize,
head_dim: usize,
softmax_scale: f32,
causal: bool,
stream: &Arc<CudaStream>,
) -> FlashResult<()> {
let kernel = self
.module
.load_function("flash_attention_varlen_forward")
.map_err(|e| {
FlashError::cuda(format!("varlen kernel symbol not found: {e:?}"))
})?;
// Grid: (ceil(max_seqlen_q / BLOCK_Q), num_heads, 1)
const BLOCK_Q: usize = 64;
let grid_x = max_seqlen_q.div_ceil(BLOCK_Q) as u32;
let grid_y = num_heads as u32;
// Shared memory: 3 tiles × BLOCK_K × head_dim × sizeof(bf16)
const BLOCK_K: usize = 64;
let smem_bytes = (3 * BLOCK_K * head_dim * 2) as u32; // bf16 = 2 bytes
let launch_cfg = LaunchConfig {
block_dim: (128, 1, 1),
grid_dim: (grid_x, grid_y, 1),
shared_mem_bytes: smem_bytes,
};
let causal_int = causal as i32;
let batch_size_i32 = batch_size as i32;
let num_heads_i32 = num_heads as i32;
let head_dim_i32 = head_dim as i32;
let max_seqlen_q_i32 = max_seqlen_q as i32;
// Safety:
// - All CudaSlice buffers are device-resident on the same device as `_ctx`.
// - `out` and `lse` are mutable unique references, so no aliasing.
// - The kernel reads cu_seqlens_q/k as read-only int arrays; they have
// length `batch_size + 1` ≥ 2 (enforced by caller).
// - head_dim ≤ 256 ensures the `acc[256]` register array is not overflowed.
// - The CUDA kernel performs a bounds check per q-row (early return when
// the tile is past the end of its sequence).
unsafe {
let mut builder = stream.launch_builder(&kernel);
builder.arg(q);
builder.arg(k);
builder.arg(v);
builder.arg(out);
builder.arg(lse);
builder.arg(cu_seqlens_q);
builder.arg(cu_seqlens_k);
builder.arg(&max_seqlen_q_i32);
builder.arg(&batch_size_i32);
builder.arg(&num_heads_i32);
builder.arg(&head_dim_i32);
builder.arg(&softmax_scale);
builder.arg(&causal_int);
builder.launch(launch_cfg)
.map_err(|e| FlashError::cuda(format!("varlen kernel launch failed: {e:?}")))?;
}
Ok(())
}
}
// =============================================================================
// Tests — all CPU, no GPU required
// =============================================================================
#[cfg(test)]
mod tests {
use super::varlen_attention_cpu;
// Helper: flat index for packed [total_tokens, num_heads, head_dim]
fn idx(token: usize, head: usize, d: usize, num_heads: usize, head_dim: usize) -> usize {
(token * num_heads + head) * head_dim + d
}
// Build a simple packed buffer filled with a constant value per token.
fn constant_qkv(
cu_seqlens: &[usize],
num_heads: usize,
head_dim: usize,
val: f32,
) -> Vec<f32> {
let total = *cu_seqlens.last().unwrap();
vec![val; total * num_heads * head_dim]
}
// -------------------------------------------------------------------------
// Test 1: Single sequence — must match standard (non-varlen) attention
// -------------------------------------------------------------------------
#[test]
fn test_varlen_cpu_single_sequence_matches_regular() {
let num_heads = 2;
let head_dim = 8;
let seq_len = 4;
let scale = 1.0 / (head_dim as f32).sqrt();
// cu_seqlens for a single sequence of length 4
let cu_seqlens = vec![0usize, seq_len];
let total = seq_len;
// Random-ish deterministic data
let q: Vec<f32> = (0..total * num_heads * head_dim)
.map(|i| (i as f32) * 0.1)
.collect();
let k = q.clone();
let v = q.clone();
let out = varlen_attention_cpu(&q, &k, &v, &cu_seqlens, num_heads, head_dim, scale, false);
// Output has the right length
assert_eq!(out.len(), total * num_heads * head_dim);
// For a single sequence, varlen output must be identical to the same
// computation run as a single entry — we verify by calling again with
// the same data (idempotency check + non-zero output sanity).
let out2 = varlen_attention_cpu(&q, &k, &v, &cu_seqlens, num_heads, head_dim, scale, false);
for i in 0..out.len() {
assert!(
(out[i] - out2[i]).abs() < 1e-6,
"output not deterministic at index {i}: {} vs {}",
out[i],
out2[i]
);
}
// All values must be finite (no NaN/Inf for well-conditioned inputs)
for (i, &v) in out.iter().enumerate() {
assert!(v.is_finite(), "non-finite output at index {i}: {v}");
}
}
// -------------------------------------------------------------------------
// Test 2: Two sequences are independent — tokens from seq0 do not appear
// in seq1's attention output
// -------------------------------------------------------------------------
#[test]
fn test_varlen_cpu_two_sequences_independent() {
let num_heads = 1;
let head_dim = 4;
let scale = 1.0 / (head_dim as f32).sqrt();
// Sequence 0: 3 tokens, all-ones
// Sequence 1: 4 tokens, all-twos
let cu_seqlens = vec![0usize, 3, 7];
let total = 7;
let mut q = vec![0.0f32; total * num_heads * head_dim];
let mut k = vec![0.0f32; total * num_heads * head_dim];
let mut v = vec![0.0f32; total * num_heads * head_dim];
// seq0 tokens (0..3) → value 1.0
for t in 0..3 {
for d in 0..head_dim {
q[idx(t, 0, d, num_heads, head_dim)] = 1.0;
k[idx(t, 0, d, num_heads, head_dim)] = 1.0;
v[idx(t, 0, d, num_heads, head_dim)] = 1.0;
}
}
// seq1 tokens (3..7) → value 100.0 (very different magnitude)
for t in 3..7 {
for d in 0..head_dim {
q[idx(t, 0, d, num_heads, head_dim)] = 100.0;
k[idx(t, 0, d, num_heads, head_dim)] = 100.0;
v[idx(t, 0, d, num_heads, head_dim)] = 100.0;
}
}
let out = varlen_attention_cpu(&q, &k, &v, &cu_seqlens, num_heads, head_dim, scale, false);
// seq0 output must be ~1.0 (attending only to seq0 tokens, all equal)
for t in 0..3 {
for d in 0..head_dim {
let val = out[idx(t, 0, d, num_heads, head_dim)];
assert!(
(val - 1.0).abs() < 1e-5,
"seq0 token {t} dim {d}: expected ~1.0, got {val}"
);
}
}
// seq1 output must be ~100.0
for t in 3..7 {
for d in 0..head_dim {
let val = out[idx(t, 0, d, num_heads, head_dim)];
assert!(
(val - 100.0).abs() < 1e-5,
"seq1 token {t} dim {d}: expected ~100.0, got {val}"
);
}
}
}
// -------------------------------------------------------------------------
// Test 3: Output shape is [total_tokens, heads, head_dim]
// -------------------------------------------------------------------------
#[test]
fn test_varlen_cpu_output_shape() {
let num_heads = 4;
let head_dim = 16;
let cu_seqlens = vec![0usize, 5, 12, 15]; // 3 seqs, lengths 5, 7, 3
let total = 15;
let scale = 1.0 / (head_dim as f32).sqrt();
let q = vec![1.0f32; total * num_heads * head_dim];
let k = q.clone();
let v = q.clone();
let out = varlen_attention_cpu(&q, &k, &v, &cu_seqlens, num_heads, head_dim, scale, false);
assert_eq!(
out.len(),
total * num_heads * head_dim,
"output length must equal total_tokens * num_heads * head_dim"
);
}
// -------------------------------------------------------------------------
// Test 4: cu_seqlens semantics — [0, 3, 7] means seq0=3 tokens, seq1=4 tokens
// -------------------------------------------------------------------------
#[test]
fn test_varlen_cu_seqlens_correct() {
let num_heads = 1;
let head_dim = 4;
let scale = 1.0 / (head_dim as f32).sqrt();
let cu_seqlens = vec![0usize, 3, 7];
// Verify: batch_size = cu_seqlens.len() - 1 = 2
let batch_size = cu_seqlens.len() - 1;
assert_eq!(batch_size, 2);
// seq0 length = cu_seqlens[1] - cu_seqlens[0] = 3
assert_eq!(cu_seqlens[1] - cu_seqlens[0], 3);
// seq1 length = cu_seqlens[2] - cu_seqlens[1] = 4
assert_eq!(cu_seqlens[2] - cu_seqlens[1], 4);
let total = cu_seqlens[batch_size];
let q = vec![0.5f32; total * num_heads * head_dim];
let k = q.clone();
let v = q.clone();
let out = varlen_attention_cpu(&q, &k, &v, &cu_seqlens, num_heads, head_dim, scale, false);
assert_eq!(out.len(), total * num_heads * head_dim);
}
// -------------------------------------------------------------------------
// Test 5: Causal mask — token i cannot attend to token j > i (within sequence)
// -------------------------------------------------------------------------
#[test]
fn test_varlen_causal_mask() {
let num_heads = 1;
let head_dim = 4;
let seq_len = 4;
let scale = 1.0 / (head_dim as f32).sqrt();
let cu_seqlens = vec![0usize, seq_len];
// Use distinct V values per token so any cross-contamination is detectable.
// V[token t, head 0, all dims] = (t+1) as f32
let total = seq_len;
let mut q = vec![1.0f32; total * num_heads * head_dim];
let mut k = vec![1.0f32; total * num_heads * head_dim];
let mut v = vec![0.0f32; total * num_heads * head_dim];
for t in 0..seq_len {
for d in 0..head_dim {
v[idx(t, 0, d, num_heads, head_dim)] = (t + 1) as f32;
}
}
let out_causal =
varlen_attention_cpu(&q, &k, &v, &cu_seqlens, num_heads, head_dim, scale, true);
let out_non_causal =
varlen_attention_cpu(&q, &k, &v, &cu_seqlens, num_heads, head_dim, scale, false);
// Token 0 in causal mode: can only attend to itself (k=0).
// V[0,*] = 1.0, so output for token 0 must be 1.0 in causal mode.
for d in 0..head_dim {
let val = out_causal[idx(0, 0, d, num_heads, head_dim)];
assert!(
(val - 1.0).abs() < 1e-5,
"causal: token 0 dim {d} expected 1.0, got {val}"
);
}
// In non-causal mode, token 0 attends to all tokens uniformly (Q=K=1.0),
// so output = mean(V) = (1+2+3+4)/4 = 2.5
for d in 0..head_dim {
let val = out_non_causal[idx(0, 0, d, num_heads, head_dim)];
assert!(
(val - 2.5).abs() < 1e-5,
"non-causal: token 0 dim {d} expected 2.5, got {val}"
);
}
// Last token in causal mode attends to all tokens (same as non-causal for last token)
for d in 0..head_dim {
let causal_last = out_causal[idx(seq_len - 1, 0, d, num_heads, head_dim)];
let noncausal_last = out_non_causal[idx(seq_len - 1, 0, d, num_heads, head_dim)];
assert!(
(causal_last - noncausal_last).abs() < 1e-5,
"causal last token should match non-causal: got {causal_last} vs {noncausal_last}"
);
}
}
// -------------------------------------------------------------------------
// Test 6: Attention weights sum to 1.0 per row within 1e-5
// -------------------------------------------------------------------------
#[test]
fn test_varlen_softmax_sums_to_one() {
let num_heads = 2;
let head_dim = 8;
let scale = 1.0 / (head_dim as f32).sqrt();
// Two sequences: 3 and 5 tokens
let cu_seqlens = vec![0usize, 3, 8];
let total = 8;
// V is the identity: each token t, dim d = (t == d) ? 1.0 : 0.0
// This lets us recover the attention weight for each (q, k) pair
// as the output value at dimension k. Works only when head_dim >= seq_len,
// but here head_dim=8 >= max(3,5)=5, so the trick works.
let q = vec![1.0f32; total * num_heads * head_dim];
let k = vec![1.0f32; total * num_heads * head_dim];
// V = identity: V[t, h, d] = if d == t % head_dim { 1.0 } else { 0.0 }
// (use global token index t, so different tokens map to different dims)
let mut v = vec![0.0f32; total * num_heads * head_dim];
for t in 0..total {
for h in 0..num_heads {
let d = t % head_dim; // unique dim per token (modulo to stay in range)
v[idx(t, h, d, num_heads, head_dim)] = 1.0;
}
}
let out = varlen_attention_cpu(&q, &k, &v, &cu_seqlens, num_heads, head_dim, scale, false);
// For each sequence, for each q-row, sum weights extracted via V=identity.
// Since q=k=const, the attention is uniform → weights = 1/seq_len.
// The output at dim d = weight for token whose unique dim is d.
// Sum of all output dims within a sequence = sum of all weights = 1.0.
for seq_idx in 0..2 {
let seq_start = cu_seqlens[seq_idx];
let seq_end = cu_seqlens[seq_idx + 1];
let seq_len = seq_end - seq_start;
for qi in 0..seq_len {
let global_token = seq_start + qi;
for h in 0..num_heads {
// Sum over dims that correspond to tokens in *this* sequence
let mut weight_sum = 0.0f32;
for ki in 0..seq_len {
let kt = seq_start + ki;
let d = kt % head_dim;
weight_sum += out[idx(global_token, h, d, num_heads, head_dim)];
}
assert!(
(weight_sum - 1.0).abs() < 1e-5,
"seq{seq_idx} q={qi} h={h}: weights sum to {weight_sum}, expected 1.0"
);
}
}
}
}
// -------------------------------------------------------------------------
// Test 7: Empty sequence — cu_seqlens = [0, 0, 4] → skip seq0, process seq1
// -------------------------------------------------------------------------
#[test]
fn test_varlen_empty_sequence_handled() {
let num_heads = 1;
let head_dim = 4;
let scale = 1.0 / (head_dim as f32).sqrt();
// seq0 is empty (0 tokens), seq1 has 4 tokens
let cu_seqlens = vec![0usize, 0, 4];
let total = 4; // only seq1 tokens exist
let q = vec![1.0f32; total * num_heads * head_dim];
let k = q.clone();
let v = q.clone();
// Must not panic — empty seq0 is silently skipped
let out = varlen_attention_cpu(&q, &k, &v, &cu_seqlens, num_heads, head_dim, scale, false);
assert_eq!(out.len(), total * num_heads * head_dim);
// seq1 tokens (indices 0..4 in the packed buffer) should have finite values
for i in 0..out.len() {
assert!(out[i].is_finite(), "non-finite at index {i}: {}", out[i]);
}
// For uniform q/k/v=1.0, output should be 1.0
for i in 0..out.len() {
assert!(
(out[i] - 1.0).abs() < 1e-5,
"expected 1.0 at index {i}, got {}",
out[i]
);
}
}
// -------------------------------------------------------------------------
// Test 8: Standard head_dim=64 (production default)
// -------------------------------------------------------------------------
#[test]
fn test_varlen_head_dim_64() {
let num_heads = 8;
let head_dim = 64;
let scale = 1.0 / (head_dim as f32).sqrt();
let cu_seqlens = vec![0usize, 7, 15]; // seq0=7, seq1=8
let total = 15;
let q: Vec<f32> = (0..total * num_heads * head_dim)
.map(|i| (i as f32) * 0.001)
.collect();
let k = q.clone();
let v: Vec<f32> = (0..total * num_heads * head_dim)
.map(|i| (i as f32) * 0.002)
.collect();
let out = varlen_attention_cpu(&q, &k, &v, &cu_seqlens, num_heads, head_dim, scale, false);
assert_eq!(out.len(), total * num_heads * head_dim);
for (i, &val) in out.iter().enumerate() {
assert!(val.is_finite(), "non-finite at index {i}: {val}");
}
}
}
- Step 2: Verify the file exists
ls -la /slab/projects/rustyverse/rustytorch/crates/training/rtx-flash-attention/src/kernels/flash_varlen_forward.rs
Expected: file with non-zero size.
Task 3: Wire Up Exports and Add SdpaBackend::VarLen
Files:
- Modify:
crates/training/rtx-flash-attention/src/kernels/mod.rs:1-57 - Modify:
crates/training/rtx-flash-attention/src/backend_selector.rs(lines 38–68 for the enum, lines 205–216 forsupports_backend, lines 55–68 forDisplay)
Interfaces:
-
Consumes:
flash_varlen_forward::varlen_attention_cpuandflash_varlen_forward::FlashVarlenKernelfrom Task 2. -
Produces:
pub use kernels::varlen_attention_cpuavailable at crate root (vialib.rsre-export chain);SdpaBackend::VarLenin the public enum. -
Step 1: Add module declaration and re-exports to
mod.rs
Edit src/kernels/mod.rs. Add after the existing #[cfg(feature = "cuda")] pub mod ptx; block (line 14) and before the #[cfg(feature = "metal")] pub mod metal; block (line 18):
// varlen module is always compiled (contains CPU simulation unconditionally)
pub mod flash_varlen_forward;
pub use flash_varlen_forward::varlen_attention_cpu;
#[cfg(feature = "cuda")]
pub use flash_varlen_forward::FlashVarlenKernel;
The resulting block after the edit looks like:
//! GPU kernels for Flash Attention implementation
#[cfg(feature = "cuda")]
pub mod flash_forward;
#[cfg(feature = "cuda")]
pub mod flash_backward;
#[cfg(feature = "cuda")]
pub mod flash_v3_forward;
#[cfg(feature = "cuda")]
pub mod utils;
#[cfg(feature = "cuda")]
pub mod simple;
#[cfg(feature = "cuda")]
pub mod ptx;
#[cfg(feature = "cuda")]
pub mod manager;
// varlen module is always compiled (contains CPU simulation unconditionally)
pub mod flash_varlen_forward;
pub use flash_varlen_forward::varlen_attention_cpu;
#[cfg(feature = "cuda")]
pub use flash_varlen_forward::FlashVarlenKernel;
#[cfg(feature = "metal")]
pub mod metal;
#[cfg(all(test, feature = "cuda"))]
mod manager_test;
#[cfg(all(test, feature = "metal"))]
mod metal_test;
#[cfg(all(test, feature = "cuda"))]
mod test_kernel_launch;
#[cfg(all(test, feature = "cuda"))]
mod cudarc_api_test;
// Re-export the implementation based on feature
#[cfg(feature = "cuda")]
pub use simple::{FlashCudaKernels, KernelResult};
#[cfg(feature = "cuda")]
pub use flash_forward::{
FlashForwardKernel,
KernelExecutionResult,
KernelPerformanceInfo,
KernelPerformanceCache,
FP8Format,
FP8KernelConfig,
};
#[cfg(feature = "cuda")]
pub use flash_v3_forward::FlashV3ForwardKernel;
#[cfg(feature = "metal")]
pub use metal::{FlashMetalKernels, MetalKernelResult};
// The complex CUDA kernel implementation has been replaced with a simplified version
// for compilation purposes. This allows the advanced crates to compile while maintaining
// the required API surface. In production, this would be replaced with optimized CUDA kernels.
- Step 2: Add
SdpaBackend::VarLentobackend_selector.rs
In src/backend_selector.rs, locate the SdpaBackend enum (lines 38–54) and add VarLen as a new variant. Add it after Cpu:
/// Available SDPA backends
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum SdpaBackend {
/// FlashAttention v2 - optimal for long sequences
FlashAttention,
/// FlashAttention v3 - WGMMA + TMA + warp specialization (Hopper/Blackwell)
FlashAttentionV3,
/// Standard mathematical attention - simple, debuggable
Math,
/// Memory-efficient chunked attention
MemoryEfficient,
/// NVIDIA cuDNN attention (Ampere+)
CuDnn,
/// Custom Metal implementation for Apple Silicon
Metal,
/// Fallback CPU implementation
Cpu,
/// Variable-length packed-sequence attention (no padding)
VarLen,
}
- Step 3: Add
Displayarm forVarLen
In the impl std::fmt::Display for SdpaBackend block (lines 56–68), add:
SdpaBackend::VarLen => write!(f, "VarLen"),
after the SdpaBackend::Cpu arm.
- Step 4: Add
supports_backendarm forVarLen
In HardwareCapabilities::supports_backend (lines 206–217), add:
SdpaBackend::VarLen => true, // CPU simulation always available; CUDA variant when feature is on
after the SdpaBackend::Cpu => true, arm.
- Step 5: Run
cargo checkand confirm clean
~/.cargo/bin/cargo check -p rtx-flash-attention 2>&1
Expected: zero errors, zero warnings (or only pre-existing warnings from other files).
- Step 6: Run
cargo test --liband confirm 50 tests pass
~/.cargo/bin/cargo test -p rtx-flash-attention --lib 2>&1 | tail -30
Expected output contains:
test result: ok. 50 passed; 0 failed; ...
(42 existing + 8 new varlen tests = 50)
- Step 7: Commit
git -C /slab/projects/rustyverse/rustytorch add \
crates/training/rtx-flash-attention/src/kernels/cuda/flash_attention_varlen.cu \
crates/training/rtx-flash-attention/src/kernels/flash_varlen_forward.rs \
crates/training/rtx-flash-attention/src/kernels/mod.rs \
crates/training/rtx-flash-attention/src/backend_selector.rs
git -C /slab/projects/rustyverse/rustytorch commit -m "$(cat <<'EOF'
feat(flash-attention): add varlen packed-sequence support
Implements variable-length (varlen) FlashAttention that processes
mixed-length batches without padding waste:
- New CUDA kernel flash_attention_varlen_forward with BLOCK_Q=64 /
BLOCK_K=64 tiling; grid=(ceil(max_seqlen_q/64), num_heads, 1).
Each block uses a linear scan over cu_seqlens_q to identify its
owning sequence and exits early when past sequence end.
- New Rust module flash_varlen_forward: always-compiled CPU simulation
(varlen_attention_cpu) for testing + #[cfg(cuda)] FlashVarlenKernel.
- SdpaBackend::VarLen variant added to backend_selector.
- 8 new CPU-only tests; total test count: 50.
Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
EOF
)"
Self-Review Checklist
Spec Coverage
| Spec requirement | Task covering it |
|---|---|
New CUDA kernel flash_attention_varlen.cu |
Task 1 |
Grid mapping: (ceil(max_seqlen_q/BLOCK_Q), num_heads, 1) |
Task 1 Step 1 |
| Sequence identification by linear scan | Task 1 Step 1 |
| Early exit when tile is past sequence end | Task 1 Step 1 |
| BLOCK_Q=64, BLOCK_K=64 | Task 1 Step 1 |
FlashVarlenKernel Rust struct with new + forward |
Task 2 Step 1 |
varlen_attention_cpu with correct signature |
Task 2 Step 1 |
Export from src/kernels/mod.rs |
Task 3 Step 1 |
SdpaBackend::VarLen |
Task 3 Steps 2–4 |
| 8 tests — all CPU | Task 2 Step 1 (tests block) |
cargo check clean |
Task 3 Step 5 |
cargo test --lib passes (50 total) |
Task 3 Step 6 |
| No new dependencies | Verified — only existing crate features used |
Placeholder Scan
No "TBD", "TODO", "implement later", or "similar to Task N" placeholders — all code blocks are complete and self-contained.
Type Consistency
varlen_attention_cpu(q: &[f32], k: &[f32], v: &[f32], cu_seqlens: &[usize], num_heads: usize, head_dim: usize, softmax_scale: f32, causal: bool) -> Vec<f32>— used consistently in both the implementation (Task 2) and all 8 test calls.FlashVarlenKernel::new(ctx: &Arc<CudaContext>) -> FlashResult<Self>— used in Task 2.FlashVarlenKernel::forward(...)— full 15-argument signature defined once in Task 2, re-exported in Task 3.SdpaBackend::VarLen— added in Task 3 Step 2, Display arm in Step 3,supports_backendarm in Step 4 — three places, all consistent spelling.