diff --git a/Cargo.lock b/Cargo.lock
index ec7492f70..6555182ce 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -3773,6 +3773,8 @@ dependencies = [
"log",
"pegainfer-build",
"serde",
+ "serde_json",
+ "sha2 0.11.0",
"tvm-ffi",
]
diff --git a/README.md b/README.md
index f30b2b665..cf3be38de 100644
--- a/README.md
+++ b/README.md
@@ -71,7 +71,7 @@ pegainfer --model-path models/Qwen3-4B
- Rust (2024 edition), CUDA Toolkit (nvcc, cuBLAS), CUDA-capable GPU
- NVIDIA driver R545 (CUDA 12.3) or newer; `cuFuncGetName` sets this floor, while per-symbol lazy loading keeps the `cuda-12090` cudarc binding from requiring a CUDA 12.9 driver
- The default build (Qwen3-4B / 8B) is pure Rust + CUDA — no Python at all
-- Python 3 + Triton for `qwen35` feature builds (build-time only — no Python at runtime)
+- Python 3 + Triton for `qwen35` feature builds (build-time only — no Python at runtime). The supported Qwen3.5-4B SM120/Hv32 path can additionally link a pre-generated FlashInfer/CuTe GDN AOT candidate; generation is separate from serving
- The `kimi-k2` EP path additionally needs NCCL ≥ 2.27 at runtime (`ncclAlltoAll`)
### Build & Run
@@ -105,7 +105,8 @@ curl -N http://localhost:8000/v1/completions \
More options
```bash
-# Qwen3.5 requires the feature-gated Triton AOT kernels (Python + Triton at build time)
+# Qwen3.5 uses build-time Triton AOT; a validated SM120/Hv32 FlashInfer GDN
+# candidate can additionally be selected through PEGAINFER_QWEN35_GDN_AOT_BUNDLE
uv venv && uv pip install triton
export PEGAINFER_TRITON_PYTHON=.venv/bin/python
cargo run --release --features qwen35 -- --model-path models/Qwen3.5-4B
@@ -120,6 +121,7 @@ cargo run --release -- --cuda-graph=false
|----------|-------------|
| `CUDA_HOME` | CUDA Toolkit path (default: `/usr/local/cuda`) |
| `PEGAINFER_TRITON_PYTHON` | Python with Triton for `qwen35` build-time AOT compilation |
+| `PEGAINFER_QWEN35_GDN_AOT_BUNDLE` | Validated Qwen3.5-4B SM120/Hv32 FlashInfer GDN candidate directory linked by `pegainfer-kernels/build.rs` |
| `PEGAINFER_TILELANG_PYTHON` | Python with TileLang for `k3` build-time kernel generation |
| `PEGAINFER_CUDA_SM` | GPU SM target override when `nvidia-smi` unavailable (e.g. `120`) |
@@ -303,14 +305,14 @@ flowchart TB
**Key design decisions:**
- **GPU-first runtime** — model execution stays in native Rust/CUDA paths
-- **Custom GPU kernels** — CUDA for decode-critical paths, Triton AOT for Qwen3.5 compatibility kernels, FlashInfer for paged attention/sampling, NCCL for multi-GPU reductions, and cuBLAS for matrix multiplication
+- **Custom GPU kernels** — CUDA for decode-critical paths; Triton AOT for the general Qwen3.5 GDN path; a statically linked FlashInfer/CuTe AOT specialization for Qwen3.5-4B SM120/Hv32 when a validated candidate is supplied; FlashInfer for paged attention/sampling; NCCL for multi-GPU reductions; and cuBLAS for matrix multiplication
- **CUDA Graph** on Qwen decode paths — eliminates kernel launch overhead where enabled
- **Per-model crate boundary** — Qwen3-4B owns its config, weights, scheduler/executor, tests, benches, and kernel plan in `pegainfer-qwen3`
**Model details:**
- **Qwen3**: 32 Q heads, 8 KV heads (GQA 4:1), head_dim=128
-- **Qwen3.5**: hybrid — 24 linear attention layers (Gated Delta Rule) + 8 full attention layers, head_dim=256
+- **Qwen3.5**: hybrid — 24 linear attention layers (Gated Delta Rule) + 8 full attention layers, head_dim=256. Qwen3.5-4B on single-GPU SM120 uses the validated FlashInfer GDN AOT specialization when linked; unsupported geometry/SM/TP configurations retain the explicit Triton path
- **DeepSeek V2-Lite**: feature-gated 2-GPU EP2 correctness/attribution path for the HF/host-staged/NCCL narrow greedy gate
### What's not (yet) implemented
@@ -350,6 +352,8 @@ PEGAINFER_TEST_MODEL_PATH=models/Qwen3.5-4B cargo test --release -p pegainfer-qw
PEGAINFER_TEST_MODEL_PATH=models/DeepSeek-V2-Lite cargo test --release -p pegainfer-deepseek-v2-lite --features deepseek-v2-lite --test e2e_ep2 -- --nocapture
```
+The SM120 FlashInfer GDN production boundary has a separate fail-closed five-gate runner because it requires a real generated candidate, the pinned Qwen3.5-4B snapshot, and an SM120 GPU. See [`pegainfer-kernels/tools/flashinfer_gdn/README.md`](pegainfer-kernels/tools/flashinfer_gdn/README.md); the non-default `gdn-validation` feature exists only for that runner and adds no counters or validation API to a default serving build.
+
The DeepSeek-V2-Lite E2E is a correctness/integration gate. Direct diagnostics and HTTP SLO report commands live in [`benchmarking.md`](docs/models/deepseek-v2-lite/benchmarking.md).
## License
diff --git a/pegainfer-kernels/Cargo.toml b/pegainfer-kernels/Cargo.toml
index 4849b68ae..bbf3ab350 100644
--- a/pegainfer-kernels/Cargo.toml
+++ b/pegainfer-kernels/Cargo.toml
@@ -15,6 +15,8 @@ tvm-ffi = { version = "0.1.0-alpha.0", optional = true }
[build-dependencies]
cc = { workspace = true }
pegainfer-build = { workspace = true }
+serde_json = { workspace = true, optional = true }
+sha2 = { workspace = true, optional = true }
[features]
default = []
@@ -24,7 +26,7 @@ tvm-ffi-triton-cubin = ["dep:tvm-ffi", "qwen35"]
deepseek-v2-lite = []
# Gemma 4: NVFP4 dequantization, whose conversion intrinsics need CUDA >= 12.8.
gemma4 = []
-qwen35 = []
+qwen35 = ["dep:serde_json", "dep:sha2"]
# Shared MoE/MLA third-party substrate: DeepEP, DeepGEMM, and FlashMLA.
glm52 = ["moe"]
# Kimi K3: TileLang-generated kernels, FP8xFP4 masked grouped GEMM, and the
diff --git a/pegainfer-kernels/KERNELS.md b/pegainfer-kernels/KERNELS.md
index 57b207b2d..00fc00020 100644
--- a/pegainfer-kernels/KERNELS.md
+++ b/pegainfer-kernels/KERNELS.md
@@ -231,7 +231,7 @@ The crate still builds CUDA/Triton symbols needed by the current root binary:
- Qwen3.5 HD256 full-attention kernels: `csrc/qwen35/prefill_attention_hd256.cu`, `csrc/shared/paged_attention.cu`.
- Qwen3.5 linear-attention decode kernels: `csrc/qwen35/conv1d.cu`, `csrc/qwen35/gated_delta_rule.cu`.
-- Qwen3.5 chunk-wise GDR prefill Triton AOT kernels: `tools/triton/gated_delta_rule_chunkwise_kernels.py`.
+- Qwen3.5 chunk-wise GDR prefill uses the Triton AOT kernels in `tools/triton/gated_delta_rule_chunkwise_kernels.py` generally. A validated build-linked candidate selects the FlashInfer/CuTe AOT specialization in `csrc/qwen35/flashinfer_gdn_aot.c` for single-GPU SM120 Qwen3.5-4B (`Hq/Hk/Hv/D=16/16/32/128`); unsupported SM, geometry, and TP configurations retain Triton.
These are preserved for build compatibility. They are not part of the Qwen3-4B Phase 1 API surface.
diff --git a/pegainfer-kernels/build.rs b/pegainfer-kernels/build.rs
index 949be5c06..d83035e69 100644
--- a/pegainfer-kernels/build.rs
+++ b/pegainfer-kernels/build.rs
@@ -9,6 +9,9 @@ use std::sync::Mutex;
use std::thread;
use std::time::Instant;
+#[cfg(feature = "qwen35")]
+use sha2::Digest as _;
+
struct TritonKernelSpec {
artifact_dir: &'static str,
kernel_path: &'static str,
@@ -39,6 +42,177 @@ struct FlashInferIncludes {
cccl: Vec,
}
+#[cfg(feature = "qwen35")]
+const QWEN35_GDN_AOT_ABI_VERSION: u64 = 1;
+#[cfg(feature = "qwen35")]
+const QWEN35_GDN_AOT_ENV: &str = "PEGAINFER_QWEN35_GDN_AOT_BUNDLE";
+
+#[cfg(feature = "qwen35")]
+fn sha256_file(path: &Path) -> String {
+ let bytes =
+ fs::read(path).unwrap_or_else(|error| panic!("failed to read {}: {error}", path.display()));
+ let digest = sha2::Sha256::digest(bytes);
+ let mut hex = String::with_capacity(digest.len() * 2);
+ for byte in digest {
+ write!(&mut hex, "{byte:02x}").expect("write SHA-256 hex to String");
+ }
+ hex
+}
+
+#[cfg(feature = "qwen35")]
+fn json_u64(value: &serde_json::Value, path: &[&str]) -> u64 {
+ let mut cursor = value;
+ for key in path {
+ cursor = &cursor[*key];
+ }
+ cursor.as_u64().unwrap_or_else(|| {
+ panic!(
+ "GDN AOT manifest field {} must be an unsigned integer",
+ path.join(".")
+ )
+ })
+}
+
+#[cfg(feature = "qwen35")]
+fn json_str<'a>(value: &'a serde_json::Value, path: &[&str]) -> &'a str {
+ let mut cursor = value;
+ for key in path {
+ cursor = &cursor[*key];
+ }
+ cursor
+ .as_str()
+ .unwrap_or_else(|| panic!("GDN AOT manifest field {} must be a string", path.join(".")))
+}
+
+/// Validate and attach the release-provided Qwen3.5 GDN object. The generated
+/// object and its native CuTe runtime archive are linked statically; serving
+/// never reads a manifest, loads PTX, or discovers a Python wheel.
+#[cfg(feature = "qwen35")]
+fn build_qwen35_flashinfer_gdn_aot(
+ root: &Path,
+ out_dir: &Path,
+ cuda_include: &Path,
+) -> (Vec, Option) {
+ println!("cargo:rerun-if-env-changed={QWEN35_GDN_AOT_ENV}");
+ let shim = root.join("csrc/qwen35/flashinfer_gdn_aot.c");
+ let shim_header = root.join("csrc/qwen35/flashinfer_gdn_aot.h");
+ println!("cargo:rerun-if-changed={}", shim.display());
+ println!("cargo:rerun-if-changed={}", shim_header.display());
+
+ let config_header = out_dir.join("flashinfer_gdn_build_config.h");
+ let mut includes = vec![root.join("csrc/qwen35"), out_dir.to_path_buf()];
+ let mut linked_objects = Vec::new();
+ let mut runtime_dir = None;
+ let mut config = String::from(
+ "#pragma once\n#define PEGAINFER_QWEN35_GDN_ARTIFACT_SHA256 \"unavailable\"\n#define PEGAINFER_QWEN35_GDN_WORKSPACE_BYTES_PER_SM 128u\n",
+ );
+
+ if let Some(bundle) = std::env::var_os(QWEN35_GDN_AOT_ENV) {
+ let bundle = PathBuf::from(bundle);
+ let manifest_path = bundle.join("manifest.json");
+ let manifest_bytes = fs::read(&manifest_path).unwrap_or_else(|error| {
+ panic!("read GDN AOT manifest {}: {error}", manifest_path.display())
+ });
+ let manifest: serde_json::Value = serde_json::from_slice(&manifest_bytes)
+ .unwrap_or_else(|error| panic!("parse GDN AOT manifest: {error}"));
+ assert_eq!(json_u64(&manifest, &["schema_version"]), 3);
+ assert_eq!(json_str(&manifest, &["variant"]), "qwen35_4b_candidate");
+ assert_eq!(json_str(&manifest, &["target", "arch"]), "sm_120a");
+ assert_eq!(
+ json_str(&manifest, &["target", "code_object"]),
+ "embedded_cubin"
+ );
+ assert_eq!(
+ json_u64(&manifest, &["abi", "version"]),
+ QWEN35_GDN_AOT_ABI_VERSION
+ );
+ assert_eq!(json_u64(&manifest, &["geometry", "h_q"]), 16);
+ assert_eq!(json_u64(&manifest, &["geometry", "h_k"]), 16);
+ assert_eq!(json_u64(&manifest, &["geometry", "h_v"]), 32);
+ assert_eq!(json_u64(&manifest, &["geometry", "head_dim"]), 128);
+ assert_eq!(json_str(&manifest, &["tokens", "extent"]), "dynamic");
+ assert_eq!(json_u64(&manifest, &["tokens", "minimum"]), 1);
+ assert_eq!(
+ json_str(&manifest, &["abi", "state_layout"]),
+ "openinfer_hkv_v_contiguous"
+ );
+ assert_eq!(json_str(&manifest, &["workspace", "kind"]), "per_sm");
+ let workspace_bytes_per_sm = json_u64(&manifest, &["workspace", "bytes_per_sm"]);
+ assert_eq!(workspace_bytes_per_sm, 128);
+ assert_eq!(json_u64(&manifest, &["workspace", "alignment_bytes"]), 128);
+
+ let header = bundle.join("kernel.h");
+ let object = bundle.join("kernel.o");
+ let runtime = bundle.join("libcuda_dialect_runtime_static.a");
+ for (label, path, hash_path, size_path) in [
+ (
+ "header",
+ &header,
+ ["artifact", "header", "sha256"],
+ ["artifact", "header", "size_bytes"],
+ ),
+ (
+ "object",
+ &object,
+ ["artifact", "object", "sha256"],
+ ["artifact", "object", "size_bytes"],
+ ),
+ (
+ "native runtime",
+ &runtime,
+ ["artifact", "native_runtime", "sha256"],
+ ["artifact", "native_runtime", "size_bytes"],
+ ),
+ ] {
+ assert!(
+ path.is_file(),
+ "GDN AOT {label} is missing: {}",
+ path.display()
+ );
+ assert_eq!(sha256_file(path), json_str(&manifest, &hash_path));
+ assert_eq!(
+ fs::metadata(path).expect("read GDN AOT metadata").len(),
+ json_u64(&manifest, &size_path)
+ );
+ println!("cargo:rerun-if-changed={}", path.display());
+ }
+ println!("cargo:rerun-if-changed={}", manifest_path.display());
+
+ let object_hash = json_str(&manifest, &["artifact", "object", "sha256"]);
+ config = format!(
+ "#pragma once\n#define PEGAINFER_QWEN35_GDN_ARTIFACT_SHA256 \"{object_hash}\"\n#define PEGAINFER_QWEN35_GDN_WORKSPACE_BYTES_PER_SM {workspace_bytes_per_sm}u\n"
+ );
+ includes.push(bundle);
+ linked_objects.push(object);
+ runtime_dir = runtime.parent().map(Path::to_path_buf);
+ }
+ fs::write(&config_header, config).expect("write GDN AOT build config");
+
+ let shim_obj = out_dir.join("qwen35_flashinfer_gdn_aot.o");
+ let compiler = cc::Build::new().get_compiler();
+ let mut command = compiler.to_command();
+ command
+ .arg("-c")
+ .arg(&shim)
+ .arg("-o")
+ .arg(&shim_obj)
+ .arg("-O3")
+ .arg("-std=c11")
+ .arg("-fPIC")
+ .arg("-isystem")
+ .arg(cuda_include);
+ for include in includes {
+ command.arg("-I").arg(include);
+ }
+ if runtime_dir.is_some() {
+ command.arg("-DPEGAINFER_QWEN35_GDN_AOT");
+ }
+ let status = command.status().expect("compile Qwen3.5 GDN AOT shim");
+ assert!(status.success(), "Qwen3.5 GDN AOT shim compilation failed");
+ linked_objects.push(shim_obj);
+ (linked_objects, runtime_dir)
+}
+
const GLM52_TRTLLM_FMHA_CUBINS: &[(&str, &str)] = &[
(
"kGlm52FmhaSparseSeedQ8",
@@ -1904,6 +2078,14 @@ fn main() {
// --- k3: DeepGEMM-only, no DeepEP/NCCL dependency ---
let k3_enabled = cfg!(feature = "k3");
let qwen35_enabled = cfg!(feature = "qwen35");
+ #[cfg(feature = "qwen35")]
+ let (qwen35_gdn_objects, qwen35_gdn_runtime_dir) = if qwen35_enabled {
+ build_qwen35_flashinfer_gdn_aot(&crate_root(), &out_dir, &cuda_include)
+ } else {
+ (Vec::new(), None)
+ };
+ #[cfg(not(feature = "qwen35"))]
+ let (qwen35_gdn_objects, qwen35_gdn_runtime_dir) = (Vec::new(), None::);
if glm52_enabled {
generate_glm52_trtllm_fmha_cubins(&crate_root(), &out_dir);
build_glm52_cutedsl_fp8_dsl(&crate_root(), &out_dir, &cuda_include);
@@ -1970,6 +2152,9 @@ fn main() {
if !kimi_k2_enabled && is_kimi_k2_source(&csrc_dir, path) {
return None;
}
+ if !qwen35_enabled && file_name == "gdn_prepare.cu" {
+ return None;
+ }
// --- k3 ---
if !k3_enabled && is_k3_source(&csrc_dir, path) {
return None;
@@ -2486,6 +2671,7 @@ fn main() {
ar_args.extend(
obj_files
.into_iter()
+ .chain(qwen35_gdn_objects)
.map(|path| path.to_string_lossy().to_string()),
);
@@ -2525,6 +2711,10 @@ fn main() {
toolkit.link_search();
}
println!("cargo:rustc-link-lib=static=kernels_cuda");
+ if let Some(runtime_dir) = qwen35_gdn_runtime_dir {
+ println!("cargo:rustc-link-search=native={}", runtime_dir.display());
+ println!("cargo:rustc-link-lib=static=cuda_dialect_runtime_static");
+ }
println!("cargo:rustc-link-lib=cudart");
println!("cargo:rustc-link-lib=cublas");
println!("cargo:rustc-link-lib=cublasLt");
diff --git a/pegainfer-kernels/csrc/qwen35/flashinfer_gdn_aot.c b/pegainfer-kernels/csrc/qwen35/flashinfer_gdn_aot.c
new file mode 100644
index 000000000..9018f9b55
--- /dev/null
+++ b/pegainfer-kernels/csrc/qwen35/flashinfer_gdn_aot.c
@@ -0,0 +1,184 @@
+#include "flashinfer_gdn_aot.h"
+
+#include
+#include
+#include
+
+#include "flashinfer_gdn_build_config.h"
+
+#ifdef PEGAINFER_QWEN35_GDN_AOT
+static int32_t status_from_cuda(cudaError_t error) {
+ if (error == cudaSuccess) return PEGAINFER_QWEN35_GDN_OK;
+ if (error == cudaErrorNotSupported)
+ return PEGAINFER_QWEN35_GDN_NOT_SUPPORTED;
+ if (error == cudaErrorInvalidValue)
+ return PEGAINFER_QWEN35_GDN_INVALID_ARGUMENT;
+ return PEGAINFER_QWEN35_GDN_CUDA_ERROR;
+}
+#include "kernel.h"
+
+typedef struct {
+ pegainfer_qwen35_gdn_qwen35_4b_candidate_Kernel_Module_t module;
+ int32_t device;
+ size_t workspace_bytes;
+} gdn_handle_t;
+
+static int32_t load_current_device(
+ pegainfer_qwen35_gdn_qwen35_4b_candidate_Kernel_Module_t *module,
+ int32_t device) {
+ cudaError_t ret = cudaSuccess;
+ cudaLibrary_t *library = &module->module;
+ struct {
+ cudaLibrary_t **library;
+ cudaError_t *ret;
+ } init_args = {&library, &ret};
+ _mlir_pegainfer_qwen35_gdn_qwen35_4b_candidate_cuda_init(
+ (void **)&init_args);
+ if (ret != cudaSuccess) return (int32_t)ret;
+ struct {
+ cudaLibrary_t **library;
+ int32_t *device;
+ cudaError_t *ret;
+ } load_args = {&library, &device, &ret};
+ _mlir_pegainfer_qwen35_gdn_qwen35_4b_candidate_cuda_load_to_device(
+ (void **)&load_args);
+ return (int32_t)ret;
+}
+#endif
+
+uint32_t pegainfer_qwen35_gdn_abi_version(void) {
+ return PEGAINFER_QWEN35_GDN_ABI_VERSION;
+}
+
+const char *pegainfer_qwen35_gdn_artifact_sha256(void) {
+ return PEGAINFER_QWEN35_GDN_ARTIFACT_SHA256;
+}
+
+int32_t pegainfer_qwen35_gdn_aot_available(void) {
+#ifdef PEGAINFER_QWEN35_GDN_AOT
+ return 1;
+#else
+ return 0;
+#endif
+}
+
+int32_t pegainfer_qwen35_gdn_create(void **handle, int32_t device) {
+ if (handle == NULL) return PEGAINFER_QWEN35_GDN_INVALID_ARGUMENT;
+ *handle = NULL;
+#ifdef PEGAINFER_QWEN35_GDN_AOT
+ cudaError_t ret = cudaSetDevice(device);
+ if (ret != cudaSuccess) return status_from_cuda(ret);
+ int32_t major = 0, minor = 0;
+ ret = cudaDeviceGetAttribute(&major, cudaDevAttrComputeCapabilityMajor,
+ device);
+ if (ret != cudaSuccess) return status_from_cuda(ret);
+ ret = cudaDeviceGetAttribute(&minor, cudaDevAttrComputeCapabilityMinor,
+ device);
+ if (ret != cudaSuccess) return status_from_cuda(ret);
+ if (major != 12 || minor != 0) return PEGAINFER_QWEN35_GDN_NOT_SUPPORTED;
+ int32_t sm_count = 0;
+ ret = cudaDeviceGetAttribute(&sm_count, cudaDevAttrMultiProcessorCount,
+ device);
+ if (ret != cudaSuccess) return status_from_cuda(ret);
+ gdn_handle_t *owner = (gdn_handle_t *)calloc(1, sizeof(*owner));
+ if (owner == NULL) return PEGAINFER_QWEN35_GDN_CUDA_ERROR;
+ owner->device = device;
+ owner->workspace_bytes =
+ (size_t)sm_count * PEGAINFER_QWEN35_GDN_WORKSPACE_BYTES_PER_SM;
+ int32_t rc = load_current_device(&owner->module, device);
+ if (rc != (int32_t)cudaSuccess) {
+ free(owner);
+ return PEGAINFER_QWEN35_GDN_CUDA_ERROR;
+ }
+ *handle = owner;
+ return PEGAINFER_QWEN35_GDN_OK;
+#else
+ (void)device;
+ return PEGAINFER_QWEN35_GDN_NOT_SUPPORTED;
+#endif
+}
+
+int32_t pegainfer_qwen35_gdn_workspace_bytes(void *handle,
+ size_t *workspace_bytes) {
+ if (handle == NULL || workspace_bytes == NULL)
+ return PEGAINFER_QWEN35_GDN_INVALID_ARGUMENT;
+#ifdef PEGAINFER_QWEN35_GDN_AOT
+ gdn_handle_t *owner = (gdn_handle_t *)handle;
+ *workspace_bytes = owner->workspace_bytes;
+ return PEGAINFER_QWEN35_GDN_OK;
+#else
+ return PEGAINFER_QWEN35_GDN_NOT_SUPPORTED;
+#endif
+}
+
+int32_t pegainfer_qwen35_gdn_launch(void *handle,
+ const pegainfer_qwen35_gdn_args_t *args) {
+#ifdef PEGAINFER_QWEN35_GDN_AOT
+ if (handle == NULL || args == NULL ||
+ args->struct_size != sizeof(*args) || args->tokens == 0 ||
+ args->tokens > (uint32_t)(INT32_MAX / 32) ||
+ args->workspace_bytes > (size_t)INT32_MAX ||
+ args->q == NULL || args->k == NULL || args->v == NULL ||
+ args->output == NULL || args->alpha == NULL || args->beta == NULL ||
+ args->state == NULL || args->initial_state == NULL ||
+ args->workspace == NULL || args->cu_seqlens == NULL ||
+ args->stream == NULL) {
+ return PEGAINFER_QWEN35_GDN_INVALID_ARGUMENT;
+ }
+ if (args->abi_version != PEGAINFER_QWEN35_GDN_ABI_VERSION)
+ return PEGAINFER_QWEN35_GDN_ABI_MISMATCH;
+ gdn_handle_t *owner = (gdn_handle_t *)handle;
+ cudaError_t cuda_rc = cudaSetDevice(owner->device);
+ if (cuda_rc != cudaSuccess) return status_from_cuda(cuda_rc);
+ if (args->workspace_bytes < owner->workspace_bytes)
+ return PEGAINFER_QWEN35_GDN_INVALID_ARGUMENT;
+
+ int32_t tokens = (int32_t)args->tokens;
+ int32_t gates = tokens * 32;
+ int32_t workspace_bytes = (int32_t)args->workspace_bytes;
+ int32_t cu_count = 2;
+ pegainfer_qwen35_gdn_qwen35_4b_candidate_Tensor_g_q_t q = {
+ (void *)args->q, {tokens}};
+ pegainfer_qwen35_gdn_qwen35_4b_candidate_Tensor_g_k_t k = {
+ (void *)args->k, {tokens}};
+ pegainfer_qwen35_gdn_qwen35_4b_candidate_Tensor_g_v_t v = {
+ (void *)args->v, {tokens}};
+ pegainfer_qwen35_gdn_qwen35_4b_candidate_Tensor_g_o_t output = {
+ args->output, {tokens}};
+ pegainfer_qwen35_gdn_qwen35_4b_candidate_Tensor_g_alpha_t alpha = {
+ (void *)args->alpha, {gates}};
+ pegainfer_qwen35_gdn_qwen35_4b_candidate_Tensor_g_beta_t beta = {
+ (void *)args->beta, {gates}};
+ pegainfer_qwen35_gdn_qwen35_4b_candidate_Tensor_g_state_t state = {
+ args->state};
+ pegainfer_qwen35_gdn_qwen35_4b_candidate_Tensor_g_init_state_t initial = {
+ (void *)args->initial_state};
+ pegainfer_qwen35_gdn_qwen35_4b_candidate_Tensor_g_tensormaps_t workspace = {
+ args->workspace, {workspace_bytes}};
+ pegainfer_qwen35_gdn_qwen35_4b_candidate_Tensor_cu_seqlens_t cu_seqlens = {
+ (void *)args->cu_seqlens, {cu_count}};
+ int32_t rc = cute_dsl_pegainfer_qwen35_gdn_qwen35_4b_candidate_wrapper(
+ &owner->module, &q, &k, &v, &output, &alpha, &beta, &state,
+ &initial, &workspace, &cu_seqlens, 0.08838834764831845f,
+ 16, 16, 32, 32, 1, 1, 0, 32, (cudaStream_t)args->stream);
+ return rc == 0 ? PEGAINFER_QWEN35_GDN_OK
+ : PEGAINFER_QWEN35_GDN_CUDA_ERROR;
+#else
+ (void)handle;
+ (void)args;
+ return PEGAINFER_QWEN35_GDN_NOT_SUPPORTED;
+#endif
+}
+
+void pegainfer_qwen35_gdn_destroy(void *handle) {
+#ifdef PEGAINFER_QWEN35_GDN_AOT
+ if (handle != NULL) {
+ gdn_handle_t *owner = (gdn_handle_t *)handle;
+ cudaSetDevice(owner->device);
+ cudaLibraryUnload(owner->module.module);
+ free(owner);
+ }
+#else
+ (void)handle;
+#endif
+}
diff --git a/pegainfer-kernels/csrc/qwen35/flashinfer_gdn_aot.h b/pegainfer-kernels/csrc/qwen35/flashinfer_gdn_aot.h
new file mode 100644
index 000000000..f7d952c7b
--- /dev/null
+++ b/pegainfer-kernels/csrc/qwen35/flashinfer_gdn_aot.h
@@ -0,0 +1,86 @@
+#pragma once
+
+#include
+#include
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+#define PEGAINFER_QWEN35_GDN_ABI_VERSION 1u
+
+typedef enum {
+ PEGAINFER_QWEN35_GDN_OK = 0,
+ PEGAINFER_QWEN35_GDN_NOT_SUPPORTED = 1,
+ PEGAINFER_QWEN35_GDN_INVALID_ARGUMENT = 2,
+ PEGAINFER_QWEN35_GDN_ABI_MISMATCH = 3,
+ PEGAINFER_QWEN35_GDN_CUDA_ERROR = 4,
+} pegainfer_qwen35_gdn_status_t;
+
+typedef struct {
+ uint32_t abi_version;
+ uint32_t struct_size;
+ const void *q;
+ const void *k;
+ const void *v;
+ void *output;
+ const void *alpha;
+ const void *beta;
+ void *state;
+ const void *initial_state;
+ void *workspace;
+ size_t workspace_bytes;
+ const int64_t *cu_seqlens;
+ uint32_t tokens;
+ void *stream;
+} pegainfer_qwen35_gdn_args_t;
+
+#if defined(__cplusplus)
+#define PEGAINFER_GDN_STATIC_ASSERT static_assert
+#define PEGAINFER_GDN_ALIGNOF alignof
+#else
+#define PEGAINFER_GDN_STATIC_ASSERT _Static_assert
+#define PEGAINFER_GDN_ALIGNOF _Alignof
+#endif
+
+#define PEGAINFER_GDN_ASSERT_OFFSET(type, field, expected) \
+ PEGAINFER_GDN_STATIC_ASSERT(offsetof(type, field) == (expected), \
+ #type "." #field " ABI offset changed")
+
+PEGAINFER_GDN_STATIC_ASSERT(sizeof(pegainfer_qwen35_gdn_args_t) == 112,
+ "GDN args ABI size changed");
+PEGAINFER_GDN_STATIC_ASSERT(PEGAINFER_GDN_ALIGNOF(pegainfer_qwen35_gdn_args_t) == 8,
+ "GDN args ABI alignment changed");
+PEGAINFER_GDN_ASSERT_OFFSET(pegainfer_qwen35_gdn_args_t, abi_version, 0);
+PEGAINFER_GDN_ASSERT_OFFSET(pegainfer_qwen35_gdn_args_t, struct_size, 4);
+PEGAINFER_GDN_ASSERT_OFFSET(pegainfer_qwen35_gdn_args_t, q, 8);
+PEGAINFER_GDN_ASSERT_OFFSET(pegainfer_qwen35_gdn_args_t, k, 16);
+PEGAINFER_GDN_ASSERT_OFFSET(pegainfer_qwen35_gdn_args_t, v, 24);
+PEGAINFER_GDN_ASSERT_OFFSET(pegainfer_qwen35_gdn_args_t, output, 32);
+PEGAINFER_GDN_ASSERT_OFFSET(pegainfer_qwen35_gdn_args_t, alpha, 40);
+PEGAINFER_GDN_ASSERT_OFFSET(pegainfer_qwen35_gdn_args_t, beta, 48);
+PEGAINFER_GDN_ASSERT_OFFSET(pegainfer_qwen35_gdn_args_t, state, 56);
+PEGAINFER_GDN_ASSERT_OFFSET(pegainfer_qwen35_gdn_args_t, initial_state, 64);
+PEGAINFER_GDN_ASSERT_OFFSET(pegainfer_qwen35_gdn_args_t, workspace, 72);
+PEGAINFER_GDN_ASSERT_OFFSET(pegainfer_qwen35_gdn_args_t, workspace_bytes, 80);
+PEGAINFER_GDN_ASSERT_OFFSET(pegainfer_qwen35_gdn_args_t, cu_seqlens, 88);
+PEGAINFER_GDN_ASSERT_OFFSET(pegainfer_qwen35_gdn_args_t, tokens, 96);
+PEGAINFER_GDN_ASSERT_OFFSET(pegainfer_qwen35_gdn_args_t, stream, 104);
+
+#undef PEGAINFER_GDN_ASSERT_OFFSET
+#undef PEGAINFER_GDN_STATIC_ASSERT
+#undef PEGAINFER_GDN_ALIGNOF
+
+uint32_t pegainfer_qwen35_gdn_abi_version(void);
+const char *pegainfer_qwen35_gdn_artifact_sha256(void);
+int32_t pegainfer_qwen35_gdn_aot_available(void);
+int32_t pegainfer_qwen35_gdn_workspace_bytes(void *handle,
+ size_t *workspace_bytes);
+int32_t pegainfer_qwen35_gdn_create(void **handle, int32_t device);
+int32_t pegainfer_qwen35_gdn_launch(void *handle,
+ const pegainfer_qwen35_gdn_args_t *args);
+void pegainfer_qwen35_gdn_destroy(void *handle);
+
+#ifdef __cplusplus
+}
+#endif
diff --git a/pegainfer-kernels/csrc/qwen35/gdn_prepare.cu b/pegainfer-kernels/csrc/qwen35/gdn_prepare.cu
new file mode 100644
index 000000000..323a9704f
--- /dev/null
+++ b/pegainfer-kernels/csrc/qwen35/gdn_prepare.cu
@@ -0,0 +1,155 @@
+#include "common.cuh"
+
+#include
+#include
+
+namespace {
+
+constexpr int kHeadDim = 128;
+constexpr int kThreads = 128;
+
+__device__ __forceinline__ float block_sum_128(float value) {
+ __shared__ float warp_sums[4];
+ const int lane = threadIdx.x & 31;
+ const int warp = threadIdx.x >> 5;
+ value = warp_reduce_sum(value);
+ if (lane == 0) {
+ warp_sums[warp] = value;
+ }
+ __syncthreads();
+ return warp_sums[0] + warp_sums[1] + warp_sums[2] + warp_sums[3];
+}
+
+__device__ __forceinline__ void record_non_finite(float value, uint32_t* status) {
+ if (!isfinite(value)) {
+ atomicExch(status, 1u);
+ }
+}
+
+// Production Hv32 specialization. One block owns one native Q or K head and
+// the corresponding V head:
+//
+// item [0,16) -> Q[item] + V[item]
+// item [16,32) -> K[item-16] + V[item]
+//
+// Q and K retain independent reductions and output layouts. Pairing each with
+// one V head removes the separate 32 V CTAs without expanding Q/K. A block
+// reports all non-finite Q/K/V/gate inputs with at most one atomic update.
+__global__ void gdn_prefill_native_prepare_hv32_kernel(
+ const __nv_bfloat16* __restrict__ qkv, // [T, 64*D]
+ const __nv_bfloat16* __restrict__ b_proj, // [T, 32]
+ const __nv_bfloat16* __restrict__ a_proj, // [T, 32]
+ const __nv_bfloat16* __restrict__ dt_bias, // [32]
+ const float* __restrict__ a_log, // [32]
+ __nv_bfloat16* __restrict__ q_out, // [T, 16, D]
+ __nv_bfloat16* __restrict__ k_out, // [T, 16, D]
+ __nv_bfloat16* __restrict__ v_out, // [T, 32, D]
+ float* __restrict__ alpha_out, // [T, 32]
+ float* __restrict__ beta_out, // [T, 32]
+ uint32_t* __restrict__ non_finite_status,
+ int qkv_dim,
+ int tokens) {
+ const int token = blockIdx.x;
+ const int item = blockIdx.y;
+ const int d = threadIdx.x;
+ if (token >= tokens) {
+ return;
+ }
+
+ constexpr int kHq = 16;
+ constexpr int kHk = 16;
+ constexpr int kHv = 32;
+ const bool is_q = item < kHq;
+ const int qk_head = is_q ? item : item - kHq;
+ const int v_head = item;
+ const size_t token_base = static_cast(token) * qkv_dim;
+ const size_t qk_base = is_q ? 0 : static_cast(kHq) * kHeadDim;
+ const size_t v_base = static_cast(kHq + kHk) * kHeadDim;
+
+ const float qk_value =
+ __bfloat162float(qkv[token_base + qk_base + qk_head * kHeadDim + d]);
+ const __nv_bfloat16 v = qkv[token_base + v_base + v_head * kHeadDim + d];
+ const float v_value = __bfloat162float(v);
+ bool non_finite = !isfinite(qk_value) || !isfinite(v_value);
+
+ const float inv_norm = rsqrtf(block_sum_128(qk_value * qk_value) + 1.0e-12f);
+ const __nv_bfloat16 normalized = __float2bfloat16(qk_value * inv_norm);
+ if (is_q) {
+ q_out[(static_cast(token) * kHq + qk_head) * kHeadDim + d] =
+ normalized;
+ } else {
+ k_out[(static_cast(token) * kHk + qk_head) * kHeadDim + d] =
+ normalized;
+ }
+ v_out[(static_cast(token) * kHv + v_head) * kHeadDim + d] = v;
+
+ if (d == 0) {
+ const size_t gate_offset = static_cast(token) * kHv + v_head;
+ const float a = __bfloat162float(a_proj[gate_offset]);
+ const float b = __bfloat162float(b_proj[gate_offset]);
+ const float bias = __bfloat162float(dt_bias[v_head]);
+ const float log_a = a_log[v_head];
+ non_finite |=
+ !isfinite(a) || !isfinite(b) || !isfinite(bias) || !isfinite(log_a);
+
+ const float x = a + bias;
+ const float softplus =
+ x > 20.0f ? x : (x < -20.0f ? expf(x) : log1pf(expf(x)));
+ const float log_alpha = -expf(log_a) * softplus;
+ alpha_out[gate_offset] = expf(log_alpha);
+ const float exp_b = expf(b < 0.0f ? b : -b);
+ beta_out[gate_offset] =
+ b >= 0.0f ? 1.0f / (1.0f + exp_b) : exp_b / (1.0f + exp_b);
+ }
+
+ if (__syncthreads_or(non_finite) && d == 0) {
+ atomicExch(non_finite_status, 1u);
+ }
+}
+
+CUresult map_cuda_error(cudaError_t error) {
+ if (error == cudaSuccess) {
+ return CUDA_SUCCESS;
+ }
+ if (error == cudaErrorInvalidValue || error == cudaErrorInvalidDevicePointer) {
+ return CUDA_ERROR_INVALID_VALUE;
+ }
+ return CUDA_ERROR_UNKNOWN;
+}
+
+} // namespace
+
+extern "C" CUresult gated_delta_rule_prefill_native_prepare_cuda(
+ const __nv_bfloat16* qkv,
+ const __nv_bfloat16* b_proj,
+ const __nv_bfloat16* a_proj,
+ const __nv_bfloat16* dt_bias,
+ const float* a_log,
+ __nv_bfloat16* q_out,
+ __nv_bfloat16* k_out,
+ __nv_bfloat16* v_out,
+ float* alpha_out,
+ float* beta_out,
+ uint32_t* non_finite_status,
+ int tokens,
+ cudaStream_t stream) {
+ constexpr int kHq = 16;
+ constexpr int kHk = 16;
+ constexpr int kHv = 32;
+ constexpr int kQkvDim = (kHq + kHk + kHv) * kHeadDim;
+ if (qkv == nullptr || b_proj == nullptr || a_proj == nullptr || dt_bias == nullptr ||
+ a_log == nullptr || q_out == nullptr || k_out == nullptr || v_out == nullptr ||
+ alpha_out == nullptr || beta_out == nullptr || non_finite_status == nullptr ||
+ tokens <= 0) {
+ return CUDA_ERROR_INVALID_VALUE;
+ }
+
+ // The chunk owner allocates this status word zeroed once. Every layer ORs
+ // into the same sticky status so the host can validate once at the chunk
+ // boundary instead of introducing one D2H synchronization per layer.
+ const dim3 grid(tokens, kHv);
+ gdn_prefill_native_prepare_hv32_kernel<<>>(
+ qkv, b_proj, a_proj, dt_bias, a_log, q_out, k_out, v_out, alpha_out,
+ beta_out, non_finite_status, kQkvDim, tokens);
+ return map_cuda_error(cudaGetLastError());
+}
diff --git a/pegainfer-kernels/src/ffi/qwen35.rs b/pegainfer-kernels/src/ffi/qwen35.rs
index c949dc186..669ea4da2 100644
--- a/pegainfer-kernels/src/ffi/qwen35.rs
+++ b/pegainfer-kernels/src/ffi/qwen35.rs
@@ -1,13 +1,85 @@
+#[cfg(feature = "qwen35")]
+use std::ffi::c_char;
+#[cfg(feature = "qwen35")]
+use std::ffi::c_void;
+
#[cfg(feature = "qwen35")]
use cudarc::driver::sys::CUresult;
use cudarc::driver::sys::CUstream;
use super::Half;
+/// Kernels-private Rust mirror of the stable C ABI. Model crates never import
+/// this struct: the safe `ops::Qwen35GdnAot` wrapper owns validation, workspace,
+/// handle lifetime, and conversion from semantic tensors to device addresses.
+#[cfg(feature = "qwen35")]
+#[repr(C)]
+#[derive(Clone, Copy, Debug)]
+pub struct FlashInferGdnPrefillArgs {
+ pub abi_version: u32,
+ pub struct_size: u32,
+ pub q: u64,
+ pub k: u64,
+ pub v: u64,
+ pub output: u64,
+ pub alpha: u64,
+ pub beta: u64,
+ pub state: u64,
+ pub initial_state: u64,
+ pub workspace: u64,
+ pub workspace_bytes: u64,
+ pub cu_seqlens: u64,
+ pub tokens: u32,
+ pub stream: CUstream,
+}
+
// Qwen3.5-4B private kernels (hybrid linear + HD256 full attention).
// Sources: csrc/qwen35/*.cu. The paged HD256 attention entry points are shared
// with Gemma 4 and are declared in `shared.rs`.
unsafe extern "C" {
+ #[cfg(feature = "qwen35")]
+ pub fn pegainfer_qwen35_gdn_abi_version() -> u32;
+ #[cfg(feature = "qwen35")]
+ pub fn pegainfer_qwen35_gdn_artifact_sha256() -> *const c_char;
+ #[cfg(feature = "qwen35")]
+ pub fn pegainfer_qwen35_gdn_aot_available() -> i32;
+ #[cfg(feature = "qwen35")]
+ pub fn pegainfer_qwen35_gdn_create(handle: *mut *mut c_void, device: i32) -> i32;
+ #[cfg(feature = "qwen35")]
+ pub fn pegainfer_qwen35_gdn_workspace_bytes(
+ handle: *mut c_void,
+ workspace_bytes: *mut usize,
+ ) -> i32;
+ #[cfg(feature = "qwen35")]
+ pub fn pegainfer_qwen35_gdn_launch(
+ handle: *mut c_void,
+ args: *const FlashInferGdnPrefillArgs,
+ ) -> i32;
+ #[cfg(feature = "qwen35")]
+ pub fn pegainfer_qwen35_gdn_destroy(handle: *mut c_void);
+
+ /// Native, non-expanded FlashInfer-GDN input preparation.
+ ///
+ /// `q_out`, `k_out`, and `v_out` are token-major `[T,H,D]`; alpha/beta are
+ /// FP32 `[T,Hv]`. `non_finite_status` is zeroed asynchronously and set to
+ /// one by the kernel if any consumed input is non-finite.
+ #[cfg(feature = "qwen35")]
+ pub fn gated_delta_rule_prefill_native_prepare_cuda(
+ qkv: *const Half,
+ b_proj: *const Half,
+ a_proj: *const Half,
+ dt_bias: *const Half,
+ a_log: *const f32,
+ q_out: *mut Half,
+ k_out: *mut Half,
+ v_out: *mut Half,
+ alpha_out: *mut f32,
+ beta_out: *mut f32,
+ non_finite_status: *mut u32,
+ tokens: i32,
+ stream: CUstream,
+ ) -> CUresult;
+
// Qwen3.5 full-attention prefill prep that writes K/V directly into paged KV.
pub fn prefill_attention_hd256_prep_paged_cuda(
q_full_batch: *const Half,
diff --git a/pegainfer-kernels/src/ops.rs b/pegainfer-kernels/src/ops.rs
index 085e9e53f..9820009c1 100644
--- a/pegainfer-kernels/src/ops.rs
+++ b/pegainfer-kernels/src/ops.rs
@@ -20,6 +20,8 @@ mod kimi_k2;
mod linear;
mod lora;
mod norm;
+#[cfg(feature = "qwen35")]
+mod qwen35;
mod sampling;
pub use attention::Hd512DecodeMetadata;
@@ -178,6 +180,8 @@ pub use norm::rms_norm_gated_batch_into;
pub use norm::rms_norm_into;
pub use norm::rms_norm_offset_into;
pub use norm::rms_norm_rows_into;
+#[cfg(feature = "qwen35")]
+pub use qwen35::*;
pub use sampling::BatchSamplingRow;
pub use sampling::BatchSamplingScratch;
pub use sampling::argmax;
diff --git a/pegainfer-kernels/src/ops/qwen35.rs b/pegainfer-kernels/src/ops/qwen35.rs
new file mode 100644
index 000000000..5d3e086b8
--- /dev/null
+++ b/pegainfer-kernels/src/ops/qwen35.rs
@@ -0,0 +1,267 @@
+//! Stable Qwen3.5 GDN prefill boundary.
+//!
+//! Generated CuTe symbols, tensor wrappers, TMA descriptors, module lifetime,
+//! and the low-level launch ABI stop below this module. Model crates see only
+//! the semantic geometry and device buffers used by Gated DeltaNet prefill.
+
+use std::ffi::CStr;
+use std::ffi::c_void;
+use std::ptr::NonNull;
+
+use anyhow::Context;
+use anyhow::Result;
+use anyhow::ensure;
+use cudarc::driver::CudaSlice;
+use cudarc::driver::DevicePtr;
+use cudarc::driver::DevicePtrMut;
+
+use crate::ffi;
+use crate::tensor::DeviceContext;
+use crate::tensor::HiddenStates;
+
+const QWEN35_GDN_ABI_VERSION: u32 = 1;
+const STATUS_OK: i32 = 0;
+
+#[derive(Clone, Copy, Debug, Eq, PartialEq)]
+pub struct Qwen35GdnGeometry {
+ pub h_q: usize,
+ pub h_k: usize,
+ pub h_v: usize,
+ pub head_dim: usize,
+}
+
+impl Qwen35GdnGeometry {
+ pub const PRODUCTION: Self = Self {
+ h_q: 16,
+ h_k: 16,
+ h_v: 32,
+ head_dim: 128,
+ };
+}
+
+#[derive(Clone, Copy, Debug, Eq, PartialEq)]
+enum Qwen35GdnSupport {
+ Supported,
+ UnsupportedSm,
+ UnsupportedGeometry,
+}
+
+fn qwen35_gdn_capability(sm: i32, geometry: Qwen35GdnGeometry) -> Qwen35GdnSupport {
+ if sm != 120 {
+ Qwen35GdnSupport::UnsupportedSm
+ } else if geometry != Qwen35GdnGeometry::PRODUCTION {
+ Qwen35GdnSupport::UnsupportedGeometry
+ } else {
+ Qwen35GdnSupport::Supported
+ }
+}
+
+#[derive(Debug)]
+pub struct Qwen35GdnAot {
+ handle: NonNull,
+ device_ordinal: usize,
+ geometry: Qwen35GdnGeometry,
+ workspace_bytes: usize,
+}
+
+pub struct Qwen35GdnWorkspace {
+ workspace: CudaSlice,
+ cu_seqlens: CudaSlice,
+ tokens: usize,
+}
+
+// The handle is bound to one CUDA device and all launches are issued by the
+// owning model thread on its DeviceContext stream.
+unsafe impl Send for Qwen35GdnAot {}
+
+impl Qwen35GdnAot {
+ pub fn load_for_production(
+ ctx: &DeviceContext,
+ geometry: Qwen35GdnGeometry,
+ ) -> Result