use onnx_runtime_ep_api::{EpError, Result};
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub(crate) enum KvDim {
KvHeads,
SeqCapacity,
HeadDim,
}
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub(crate) enum KvCachePath {
FusedDecodePrep,
FlashPrefillPrep,
FlashPrefillRead,
Fp16DecodeRead,
Bf16DecodeRead,
UnfusedDecodePrep,
F32DecodeRead,
ReferenceRead,
Phase2aRead,
}
impl KvCachePath {
fn name(self) -> &'static str {
match self {
Self::FusedDecodePrep => "fused fp16 decode prep",
Self::FlashPrefillPrep => "flash prefill prep",
Self::FlashPrefillRead => "flash prefill read",
Self::Fp16DecodeRead => "fp16 split-K decode read",
Self::Bf16DecodeRead => "bf16 split-K decode read",
Self::UnfusedDecodePrep => "unfused decode prep",
Self::F32DecodeRead => "f32 split-K decode read",
Self::ReferenceRead => "reference attention read",
Self::Phase2aRead => "phase2a attention read",
}
}
}
const CONVERTED_PATHS: &[KvCachePath] = &[
KvCachePath::FusedDecodePrep,
KvCachePath::FlashPrefillPrep,
KvCachePath::FlashPrefillRead,
KvCachePath::Fp16DecodeRead,
KvCachePath::Bf16DecodeRead,
];
#[derive(Clone, PartialEq, Eq, Debug)]
pub(crate) struct KvCacheStrides {
batch: Vec<KvDim>,
head: Vec<KvDim>,
seq: Vec<KvDim>,
head_dim: Vec<KvDim>,
offset_elements: u64,
reservation_override: bool,
named: &'static str,
}
impl Default for KvCacheStrides {
fn default() -> Self {
Self::head_major_bnsh()
}
}
impl KvCacheStrides {
pub(crate) fn head_major_bnsh() -> Self {
use KvDim::{HeadDim, KvHeads, SeqCapacity};
KvCacheStrides {
batch: vec![KvHeads, SeqCapacity, HeadDim],
head: vec![SeqCapacity, HeadDim],
seq: vec![HeadDim],
head_dim: vec![],
offset_elements: 0,
reservation_override: false,
named: "bnsh",
}
}
pub(crate) fn seq_major_bsnh() -> Self {
use KvDim::{HeadDim, KvHeads, SeqCapacity};
KvCacheStrides {
batch: vec![SeqCapacity, KvHeads, HeadDim],
head: vec![HeadDim],
seq: vec![KvHeads, HeadDim],
head_dim: vec![],
offset_elements: 0,
reservation_override: false,
named: "bsnh",
}
}
pub(crate) fn from_attribute(kv_layout: i64) -> Result<Self> {
match kv_layout {
0 => Ok(Self::head_major_bnsh()),
1 => Ok(Self::seq_major_bsnh()),
other => Err(EpError::KernelFailed(format!(
"cuda_ep GroupQueryAttention: kv_layout {other} must be 0 (BNSH) or 1 (BSNH)"
))),
}
}
pub(crate) fn is_head_major(&self) -> bool {
self.named == "bnsh"
}
pub(crate) fn require_converted_path_support(&self, path: KvCachePath) -> Result<()> {
if !self.head_dim.is_empty() {
return Err(EpError::KernelFailed(
"cuda_ep GroupQueryAttention: KV descriptor has a non-unit head_dim stride; the \
converted fp16 decode path requires a contiguous (half2-vectorizable) head_dim"
.into(),
));
}
if self.offset_elements != 0 || self.reservation_override {
return Err(EpError::KernelFailed(
"cuda_ep GroupQueryAttention: KV descriptor is a view into a larger reservation \
(non-zero offset or reservation-spanning seq extent); the converted path honors \
only whole-buffer bindings, not a token-major view"
.into(),
));
}
if self.named.is_empty() {
return Err(EpError::KernelFailed(
"cuda_ep GroupQueryAttention: KV descriptor does not match a layout with a cached \
specialized module; only head-major BNSH and seq-major BSNH are honored"
.into(),
));
}
if !self.is_head_major() && !CONVERTED_PATHS.contains(&path) {
return Err(EpError::KernelFailed(format!(
"cuda_ep GroupQueryAttention: seq-major (BSNH) KV cannot use the {} path; \
converted paths are fused fp16 decode prep, flash prefill prep/read, and fp16 \
split-K decode read",
path.name()
)));
}
Ok(())
}
pub(crate) fn decode_module_key(&self) -> Result<&'static str> {
match self.named {
"bnsh" => Ok("gqa_decode_attention_f16_v8_bnsh"),
"bsnh" => Ok("gqa_decode_attention_f16_v8_bsnh"),
_ => Err(self.no_cached_module_err()),
}
}
pub(crate) fn decode_module_key_bf16(&self) -> Result<&'static str> {
match self.named {
"bnsh" => Ok("gqa_decode_attention_bf16_v1_bnsh"),
"bsnh" => Ok("gqa_decode_attention_bf16_v1_bsnh"),
_ => Err(self.no_cached_module_err()),
}
}
pub(crate) fn prep_f32_module_key(&self) -> Result<&'static str> {
match self.named {
"bsnh" => Ok("group_query_attention_prep_v4_bsnh"),
_ => Err(self.no_cached_module_err()),
}
}
pub(crate) fn prep_half_module_key(&self) -> Result<&'static str> {
match self.named {
"bsnh" => Ok("group_query_attention_prep_half_v4_bsnh"),
_ => Err(self.no_cached_module_err()),
}
}
pub(crate) fn flash_f32_module_key(&self) -> Result<&'static str> {
match self.named {
"bnsh" => Ok("flash_attention_f32_v2_bnsh"),
"bsnh" => Ok("flash_attention_f32_v2_bsnh"),
_ => Err(self.no_cached_module_err()),
}
}
pub(crate) fn flash_half_module_key(&self) -> Result<&'static str> {
match self.named {
"bnsh" => Ok("flash_attention_half_v3_bnsh"),
"bsnh" => Ok("flash_attention_half_v3_bsnh"),
_ => Err(self.no_cached_module_err()),
}
}
fn no_cached_module_err(&self) -> EpError {
EpError::KernelFailed(
"cuda_ep GroupQueryAttention: no cached specialized module for this KV descriptor; \
only head-major BNSH and seq-major BSNH are honored"
.into(),
)
}
pub(crate) fn decode_prelude(&self) -> String {
let batch = product_expr(&self.batch, "kv_heads", "cache_capacity", "head_size");
let head = product_expr(&self.head, "kv_heads", "cache_capacity", "head_size");
let seq = product_expr(&self.seq, "kv_heads", "cache_capacity", "head_size");
format!(
"#define GQA_KV_BASE(b, h) ( (long)(b) * {batch} + (long)(h) * {head} )\n\
#define GQA_KV_STRIDE ( {seq} )\n"
)
}
pub(crate) fn flash_prelude(&self) -> String {
let batch = product_expr(&self.batch, "kv_heads", "kv_capacity", "dim");
let head = product_expr(&self.head, "kv_heads", "kv_capacity", "dim");
let seq = product_expr(&self.seq, "kv_heads", "kv_capacity", "dim");
format!(
"#define FLASH_KV_BASE(b, h) ( (long)(b) * {batch} + (long)(h) * {head} )\n\
#define FLASH_KV_STRIDE ( {seq} )\n"
)
}
pub(crate) fn prep_prelude(&self) -> String {
let batch = product_expr(&self.batch, "heads", "capacity", "dim");
let head = product_expr(&self.head, "heads", "capacity", "dim");
let seq = product_expr(&self.seq, "heads", "capacity", "dim");
format!(
"#define GQA_KV_INDEX(b, h, slot, heads, capacity, dim) ( (long)(b) * {batch} \
+ (long)(h) * {head} + (long)(slot) * {seq} )\n\
#define GQA_KV_DST(b, h, slot) \
GQA_KV_INDEX(b, h, slot, kv_heads, present_capacity, dim)\n"
)
}
}
fn product_expr(factors: &[KvDim], kv_heads: &str, capacity: &str, head_dim: &str) -> String {
if factors.is_empty() {
return "1".to_string();
}
let parts: Vec<String> = factors
.iter()
.map(|factor| match factor {
KvDim::KvHeads => format!("(long){kv_heads}"),
KvDim::SeqCapacity => format!("(long){capacity}"),
KvDim::HeadDim => format!("(long){head_dim}"),
})
.collect();
format!("({})", parts.join(" * "))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn attribute_maps_to_named_layouts() {
assert!(KvCacheStrides::from_attribute(0).unwrap().is_head_major());
assert!(!KvCacheStrides::from_attribute(1).unwrap().is_head_major());
assert!(KvCacheStrides::from_attribute(2).is_err());
}
#[test]
fn named_layouts_are_honored() {
assert!(
KvCacheStrides::head_major_bnsh()
.require_converted_path_support(KvCachePath::ReferenceRead)
.is_ok()
);
assert!(
KvCacheStrides::seq_major_bsnh()
.require_converted_path_support(KvCachePath::FlashPrefillRead)
.is_ok()
);
assert!(
KvCacheStrides::seq_major_bsnh()
.require_converted_path_support(KvCachePath::ReferenceRead)
.is_err()
);
}
#[test]
fn reservation_view_is_rejected() {
let mut view = KvCacheStrides::seq_major_bsnh();
view.offset_elements = 4096;
view.reservation_override = true;
let error = view
.require_converted_path_support(KvCachePath::Fp16DecodeRead)
.unwrap_err();
assert!(matches!(error, EpError::KernelFailed(_)));
assert!(view.decode_module_key().is_ok());
}
#[test]
fn non_unit_head_dim_is_rejected() {
let mut bad = KvCacheStrides::head_major_bnsh();
bad.head_dim = vec![KvDim::KvHeads];
assert!(
bad.require_converted_path_support(KvCachePath::Fp16DecodeRead)
.is_err()
);
}
#[test]
fn decode_prelude_matches_layout_formulae() {
let bnsh = KvCacheStrides::head_major_bnsh().decode_prelude();
assert!(bnsh.contains(
"#define GQA_KV_BASE(b, h) ( (long)(b) * \
((long)kv_heads * (long)cache_capacity * (long)head_size) \
+ (long)(h) * ((long)cache_capacity * (long)head_size) )"
));
assert!(bnsh.contains("#define GQA_KV_STRIDE ( ((long)head_size) )"));
let bsnh = KvCacheStrides::seq_major_bsnh().decode_prelude();
assert!(bsnh.contains("#define GQA_KV_STRIDE ( ((long)kv_heads * (long)head_size) )"));
}
#[test]
fn prep_prelude_matches_layout_formulae() {
let bnsh = KvCacheStrides::head_major_bnsh().prep_prelude();
assert!(bnsh.contains("(long)(slot) * ((long)dim)"));
let bsnh = KvCacheStrides::seq_major_bsnh().prep_prelude();
assert!(bsnh.contains("(long)(slot) * ((long)heads * (long)dim)"));
}
#[test]
fn flash_prelude_matches_layout_formulae() {
let bnsh = KvCacheStrides::head_major_bnsh().flash_prelude();
assert!(bnsh.contains("#define FLASH_KV_STRIDE ( ((long)dim) )"));
let bsnh = KvCacheStrides::seq_major_bsnh().flash_prelude();
assert!(bsnh.contains("#define FLASH_KV_STRIDE ( ((long)kv_heads * (long)dim) )"));
}
}