Compare commits
6 Commits
c2362df1f1
...
9a01c60100
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9a01c60100 | ||
|
|
c679f618fd | ||
|
|
cc4bd4cfe5 | ||
|
|
13ae3de69e | ||
|
|
6ce21345be | ||
|
|
1ab6ca9c09 |
@@ -100,6 +100,13 @@ pub fn cached_alloc(size: usize) -> Result<GpuBuffer> {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Free all cached (unused) GPU buffers back to the driver.
|
||||||
|
pub fn cached_trim() {
|
||||||
|
ALLOCATOR.with(|cell| {
|
||||||
|
cell.borrow_mut().trim();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/// Return a raw GPU pointer to the caching allocator's free list.
|
/// Return a raw GPU pointer to the caching allocator's free list.
|
||||||
/// Called from `GpuBuffer::Drop` for pooled buffers. Takes raw pointer
|
/// Called from `GpuBuffer::Drop` for pooled buffers. Takes raw pointer
|
||||||
/// and size to avoid re-triggering Drop.
|
/// and size to avoid re-triggering Drop.
|
||||||
|
|||||||
@@ -21,12 +21,14 @@ fn main() {
|
|||||||
.file("../../csrc/normalization/layernorm.cu")
|
.file("../../csrc/normalization/layernorm.cu")
|
||||||
.file("../../csrc/activation/activations.cu")
|
.file("../../csrc/activation/activations.cu")
|
||||||
.file("../../csrc/reduce/softmax.cu")
|
.file("../../csrc/reduce/softmax.cu")
|
||||||
|
.file("../../csrc/reduce/argmax.cu")
|
||||||
.file("../../csrc/embedding/embedding.cu")
|
.file("../../csrc/embedding/embedding.cu")
|
||||||
.file("../../csrc/embedding/rope.cu")
|
.file("../../csrc/embedding/rope.cu")
|
||||||
.file("../../csrc/attention/causal_mask.cu")
|
.file("../../csrc/attention/causal_mask.cu")
|
||||||
.file("../../csrc/embedding/transpose.cu")
|
.file("../../csrc/embedding/transpose.cu")
|
||||||
.file("../../csrc/attention/flash_attention.cu")
|
.file("../../csrc/attention/flash_attention.cu")
|
||||||
.file("../../csrc/attention/paged_attention.cu")
|
.file("../../csrc/attention/paged_attention.cu")
|
||||||
|
.file("../../csrc/attention/reshape_and_cache.cu")
|
||||||
.compile("xserv_kernels");
|
.compile("xserv_kernels");
|
||||||
|
|
||||||
println!("cargo:rerun-if-changed=../../csrc/");
|
println!("cargo:rerun-if-changed=../../csrc/");
|
||||||
|
|||||||
65
crates/xserv-kernels/src/argmax.rs
Normal file
65
crates/xserv-kernels/src/argmax.rs
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
use std::ffi::c_void;
|
||||||
|
use xserv_tensor::{DType, Device, Tensor};
|
||||||
|
|
||||||
|
unsafe extern "C" {
|
||||||
|
fn launch_argmax_bf16(logits: *const c_void, out_idx: *mut c_void,
|
||||||
|
rows: i32, cols: i32, stream: *mut c_void);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// GPU argmax over the last dim of a [rows, cols] BF16 tensor.
|
||||||
|
///
|
||||||
|
/// Returns a host `Vec<u32>` of length `rows`. Internally:
|
||||||
|
/// - launches one kernel that writes [rows] i32 indices on device
|
||||||
|
/// - D2H copies just `rows * 4` bytes (vs `rows * cols * 2` for the
|
||||||
|
/// "copy logits to CPU then argmax" path it replaces)
|
||||||
|
///
|
||||||
|
/// This is the greedy-decode hot path: avoids touching the full
|
||||||
|
/// [B, vocab] logits buffer on the host every step.
|
||||||
|
pub fn argmax_bf16_to_host(logits: &Tensor) -> Vec<u32> {
|
||||||
|
assert_eq!(logits.ndim(), 2, "argmax expects a 2D [rows, cols] tensor");
|
||||||
|
assert_eq!(logits.dtype(), DType::BF16, "argmax kernel is BF16-only");
|
||||||
|
assert!(logits.is_contiguous(), "argmax requires contiguous input");
|
||||||
|
assert!(matches!(logits.device(), Device::Cuda(_)), "argmax requires GPU input");
|
||||||
|
|
||||||
|
let rows = logits.shape()[0];
|
||||||
|
let cols = logits.shape()[1];
|
||||||
|
assert!(rows <= i32::MAX as usize);
|
||||||
|
assert!(cols <= i32::MAX as usize);
|
||||||
|
|
||||||
|
// Output buffer: rows * i32. Pooled allocator so this is essentially free
|
||||||
|
// after the first call.
|
||||||
|
let bytes = rows * std::mem::size_of::<i32>();
|
||||||
|
let mut out = xserv_cuda::allocator::cached_alloc(bytes).expect("argmax out alloc");
|
||||||
|
|
||||||
|
unsafe {
|
||||||
|
launch_argmax_bf16(
|
||||||
|
logits.data_ptr() as *const c_void,
|
||||||
|
out.as_mut_ptr() as *mut c_void,
|
||||||
|
rows as i32, cols as i32,
|
||||||
|
std::ptr::null_mut(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut host_bytes = vec![0u8; bytes];
|
||||||
|
out.copy_to_host(&mut host_bytes).expect("argmax D2H");
|
||||||
|
drop(out); // returned to pool
|
||||||
|
|
||||||
|
let host_i32: &[i32] = unsafe {
|
||||||
|
std::slice::from_raw_parts(host_bytes.as_ptr() as *const i32, rows)
|
||||||
|
};
|
||||||
|
host_i32.iter().map(|&v| v as u32).collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Convenience: argmax of a single row [1, cols] (or [cols] reshaped to [1, cols]).
|
||||||
|
pub fn argmax_bf16_single(logits: &Tensor) -> u32 {
|
||||||
|
let cols = *logits.shape().last().unwrap();
|
||||||
|
let rows = logits.numel() / cols;
|
||||||
|
assert_eq!(rows, 1, "argmax_bf16_single requires a single row");
|
||||||
|
let view = if logits.ndim() == 2 {
|
||||||
|
logits.clone()
|
||||||
|
} else {
|
||||||
|
logits.reshape(&[1, cols])
|
||||||
|
};
|
||||||
|
argmax_bf16_to_host(&view)[0]
|
||||||
|
}
|
||||||
|
|
||||||
@@ -33,6 +33,85 @@ unsafe extern "C" {
|
|||||||
head_dim: i32, max_blocks_per_seq: i32,
|
head_dim: i32, max_blocks_per_seq: i32,
|
||||||
scale: f32, stream: *mut c_void,
|
scale: f32, stream: *mut c_void,
|
||||||
);
|
);
|
||||||
|
fn launch_reshape_and_cache_bf16(
|
||||||
|
k_src: *const c_void, v_src: *const c_void,
|
||||||
|
k_pool: *mut c_void, v_pool: *mut c_void,
|
||||||
|
block_ids: *const c_void,
|
||||||
|
num_tokens: i32, num_heads: i32,
|
||||||
|
head_dim: i32, start_pos: i32, block_size: i32,
|
||||||
|
stream: *mut c_void,
|
||||||
|
);
|
||||||
|
fn launch_reshape_and_cache_batched_bf16(
|
||||||
|
k_src: *const c_void, v_src: *const c_void,
|
||||||
|
k_pool: *mut c_void, v_pool: *mut c_void,
|
||||||
|
block_tables: *const c_void, kv_lens: *const c_void,
|
||||||
|
batch: i32, num_heads: i32,
|
||||||
|
head_dim: i32, block_size: i32, max_blocks_per_seq: i32,
|
||||||
|
stream: *mut c_void,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Scatter `[num_kv_heads, num_tokens, head_dim]` BF16 K/V into a paged
|
||||||
|
/// pool for a single sequence whose block table lives at `block_ids_gpu`
|
||||||
|
/// (int32, on device).
|
||||||
|
///
|
||||||
|
/// `k_pool_ptr`/`v_pool_ptr` point to one layer's pool, of logical shape
|
||||||
|
/// `[num_blocks_total, num_kv_heads, block_size, head_dim]`.
|
||||||
|
///
|
||||||
|
/// All pointers must be on the same GPU as the launching context.
|
||||||
|
///
|
||||||
|
/// # Safety
|
||||||
|
/// Pointers must be valid GPU pointers with the documented layouts.
|
||||||
|
/// `block_ids_gpu` must contain at least `(start_pos + num_tokens + block_size - 1) / block_size`
|
||||||
|
/// valid physical block ids.
|
||||||
|
pub unsafe fn reshape_and_cache_bf16(
|
||||||
|
k_src: *const c_void, v_src: *const c_void,
|
||||||
|
k_pool_ptr: *mut c_void, v_pool_ptr: *mut c_void,
|
||||||
|
block_ids_gpu: *const i32,
|
||||||
|
num_tokens: usize, num_heads: usize,
|
||||||
|
head_dim: usize, start_pos: usize, block_size: usize,
|
||||||
|
stream: *mut c_void,
|
||||||
|
) {
|
||||||
|
unsafe {
|
||||||
|
launch_reshape_and_cache_bf16(
|
||||||
|
k_src, v_src,
|
||||||
|
k_pool_ptr, v_pool_ptr,
|
||||||
|
block_ids_gpu as *const c_void,
|
||||||
|
num_tokens as i32, num_heads as i32,
|
||||||
|
head_dim as i32, start_pos as i32, block_size as i32,
|
||||||
|
stream,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Batched scatter for the multi-sequence decode step. Reads
|
||||||
|
/// `block_tables` (`[batch, max_blocks_per_seq]` int32 — same buffer the
|
||||||
|
/// paged-attention kernel reads) and `kv_lens` (`[batch]` int32, current
|
||||||
|
/// seq_len + 1 — i.e., the index of the just-written token + 1) so the
|
||||||
|
/// caller doesn't need a separate per-step upload of block ids.
|
||||||
|
///
|
||||||
|
/// # Safety
|
||||||
|
/// All pointers must be on the same GPU. `block_tables` and `kv_lens` must
|
||||||
|
/// already be synced to the device for the active batch.
|
||||||
|
pub unsafe fn reshape_and_cache_batched_bf16(
|
||||||
|
k_src: *const c_void, v_src: *const c_void,
|
||||||
|
k_pool_ptr: *mut c_void, v_pool_ptr: *mut c_void,
|
||||||
|
block_tables_gpu: *const i32, kv_lens_gpu: *const i32,
|
||||||
|
batch: usize, num_heads: usize,
|
||||||
|
head_dim: usize, block_size: usize, max_blocks_per_seq: usize,
|
||||||
|
stream: *mut c_void,
|
||||||
|
) {
|
||||||
|
unsafe {
|
||||||
|
launch_reshape_and_cache_batched_bf16(
|
||||||
|
k_src, v_src,
|
||||||
|
k_pool_ptr, v_pool_ptr,
|
||||||
|
block_tables_gpu as *const c_void,
|
||||||
|
kv_lens_gpu as *const c_void,
|
||||||
|
batch as i32, num_heads as i32,
|
||||||
|
head_dim as i32, block_size as i32, max_blocks_per_seq as i32,
|
||||||
|
stream,
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn apply_causal_mask(scores: &Tensor, offset: usize) {
|
fn apply_causal_mask(scores: &Tensor, offset: usize) {
|
||||||
|
|||||||
@@ -1,8 +1,16 @@
|
|||||||
use std::cell::RefCell;
|
use std::cell::RefCell;
|
||||||
use std::ffi::c_void;
|
use std::ffi::c_void;
|
||||||
use xserv_cuda::error::{self, Result};
|
use xserv_cuda::error::{self, Result};
|
||||||
|
use xserv_cuda::GpuBuffer;
|
||||||
use xserv_tensor::{DType, Device, Tensor};
|
use xserv_tensor::{DType, Device, Tensor};
|
||||||
|
|
||||||
|
const CUBLAS_WORKSPACE_BYTES: usize = 32 * 1024 * 1024;
|
||||||
|
|
||||||
|
// GEMV: single-kernel, no FP32 temp buffer needed
|
||||||
|
unsafe extern "C" {
|
||||||
|
fn launch_gemv_bf16(x: *const c_void, w: *const c_void, y_bf16: *mut c_void, y_fp32_buf: *mut c_void, k: i32, n: i32, stream: *mut c_void);
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy)]
|
#[derive(Debug, Clone, Copy)]
|
||||||
pub enum GemmBackend {
|
pub enum GemmBackend {
|
||||||
Naive,
|
Naive,
|
||||||
@@ -16,7 +24,6 @@ unsafe extern "C" {
|
|||||||
fn launch_gemm_naive_bf16(a: *const c_void, b: *const c_void, c: *mut c_void, m: i32, n: i32, k: i32, stream: *mut c_void);
|
fn launch_gemm_naive_bf16(a: *const c_void, b: *const c_void, c: *mut c_void, m: i32, n: i32, k: i32, stream: *mut c_void);
|
||||||
fn launch_gemm_tiled_f32(a: *const c_void, b: *const c_void, c: *mut c_void, m: i32, n: i32, k: i32, stream: *mut c_void);
|
fn launch_gemm_tiled_f32(a: *const c_void, b: *const c_void, c: *mut c_void, m: i32, n: i32, k: i32, stream: *mut c_void);
|
||||||
fn launch_gemm_tiled_bf16(a: *const c_void, b: *const c_void, c: *mut c_void, m: i32, n: i32, k: i32, stream: *mut c_void);
|
fn launch_gemm_tiled_bf16(a: *const c_void, b: *const c_void, c: *mut c_void, m: i32, n: i32, k: i32, stream: *mut c_void);
|
||||||
fn launch_gemv_bf16(x: *const c_void, w: *const c_void, y_bf16: *mut c_void, y_fp32_buf: *mut c_void, k: i32, n: i32, stream: *mut c_void);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- FFI: cuBLAS ---
|
// --- FFI: cuBLAS ---
|
||||||
@@ -36,6 +43,7 @@ unsafe extern "C" {
|
|||||||
fn cublasCreate_v2(handle: *mut CublasHandle) -> i32;
|
fn cublasCreate_v2(handle: *mut CublasHandle) -> i32;
|
||||||
fn cublasDestroy_v2(handle: CublasHandle) -> i32;
|
fn cublasDestroy_v2(handle: CublasHandle) -> i32;
|
||||||
fn cublasSetStream_v2(handle: CublasHandle, stream: *mut c_void) -> i32;
|
fn cublasSetStream_v2(handle: CublasHandle, stream: *mut c_void) -> i32;
|
||||||
|
fn cublasSetWorkspace_v2(handle: CublasHandle, workspace: *mut c_void, size: usize) -> i32;
|
||||||
fn cublasGemmEx(
|
fn cublasGemmEx(
|
||||||
handle: CublasHandle,
|
handle: CublasHandle,
|
||||||
transa: i32, transb: i32,
|
transa: i32, transb: i32,
|
||||||
@@ -65,13 +73,25 @@ unsafe extern "C" {
|
|||||||
|
|
||||||
pub struct CublasContext {
|
pub struct CublasContext {
|
||||||
handle: CublasHandle,
|
handle: CublasHandle,
|
||||||
|
/// Dedicated 32 MiB workspace owned by this handle. Held to keep the GPU
|
||||||
|
/// buffer alive for the lifetime of the handle; cuBLAS reads/writes into
|
||||||
|
/// it during GEMM. Dropped after `cublasDestroy_v2` so cuBLAS can't touch
|
||||||
|
/// freed memory.
|
||||||
|
_workspace: Option<GpuBuffer>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl CublasContext {
|
impl CublasContext {
|
||||||
pub fn new() -> Result<Self> {
|
pub fn new() -> Result<Self> {
|
||||||
let mut handle = std::ptr::null_mut();
|
let mut handle = std::ptr::null_mut();
|
||||||
error::check(unsafe { cublasCreate_v2(&mut handle) })?;
|
error::check(unsafe { cublasCreate_v2(&mut handle) })?;
|
||||||
Ok(Self { handle })
|
// Attach a per-handle workspace. cublasSetWorkspace requires the
|
||||||
|
// pointer to remain valid until destroy or until a new workspace is
|
||||||
|
// set, so we keep the GpuBuffer in this struct.
|
||||||
|
let mut workspace = GpuBuffer::alloc(CUBLAS_WORKSPACE_BYTES)?;
|
||||||
|
error::check(unsafe {
|
||||||
|
cublasSetWorkspace_v2(handle, workspace.as_mut_ptr() as *mut c_void, CUBLAS_WORKSPACE_BYTES)
|
||||||
|
})?;
|
||||||
|
Ok(Self { handle, _workspace: Some(workspace) })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -80,6 +100,7 @@ impl Drop for CublasContext {
|
|||||||
if !self.handle.is_null() {
|
if !self.handle.is_null() {
|
||||||
unsafe { cublasDestroy_v2(self.handle) };
|
unsafe { cublasDestroy_v2(self.handle) };
|
||||||
}
|
}
|
||||||
|
// _workspace drops here, after cublasDestroy_v2 has released the handle.
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -152,7 +173,6 @@ pub fn matmul(a: &Tensor, b: &Tensor, backend: GemmBackend) -> Tensor {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
GemmBackend::CuBlas => {
|
GemmBackend::CuBlas => {
|
||||||
// Fast path: custom GEMV for M=1 BF16 (bandwidth-optimal decode)
|
|
||||||
if m == 1 && dtype == DType::BF16 {
|
if m == 1 && dtype == DType::BF16 {
|
||||||
let mut fp32_buf = xserv_cuda::allocator::cached_alloc(n * 4).unwrap();
|
let mut fp32_buf = xserv_cuda::allocator::cached_alloc(n * 4).unwrap();
|
||||||
unsafe {
|
unsafe {
|
||||||
@@ -163,11 +183,7 @@ pub fn matmul(a: &Tensor, b: &Tensor, backend: GemmBackend) -> Tensor {
|
|||||||
null_stream,
|
null_stream,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
// fp32_buf returned to caching allocator pool on drop
|
|
||||||
} else {
|
} else {
|
||||||
// cuBLAS uses column-major, but we have row-major tensors.
|
|
||||||
// Trick: compute C^T = B^T @ A^T, which gives us C in row-major.
|
|
||||||
// cuBLAS sees our row-major data as column-major transposed.
|
|
||||||
let alpha = 1.0f32;
|
let alpha = 1.0f32;
|
||||||
let beta = 0.0f32;
|
let beta = 0.0f32;
|
||||||
|
|
||||||
@@ -179,19 +195,17 @@ pub fn matmul(a: &Tensor, b: &Tensor, backend: GemmBackend) -> Tensor {
|
|||||||
|
|
||||||
with_cublas(|handle| unsafe {
|
with_cublas(|handle| unsafe {
|
||||||
cublasSetStream_v2(handle, null_stream);
|
cublasSetStream_v2(handle, null_stream);
|
||||||
// Row-major trick: swap A/B and transpose flags
|
|
||||||
// C(row-major) = A @ B <=> C^T(col-major) = B^T @ A^T
|
|
||||||
error::check(cublasGemmEx(
|
error::check(cublasGemmEx(
|
||||||
handle,
|
handle,
|
||||||
CUBLAS_OP_N, CUBLAS_OP_N,
|
CUBLAS_OP_N, CUBLAS_OP_N,
|
||||||
n as i32, m as i32, k as i32,
|
n as i32, m as i32, k as i32,
|
||||||
&alpha as *const f32 as *const c_void,
|
&alpha as *const f32 as *const c_void,
|
||||||
b_ptr, b_type, n as i32, // B as col-major = B^T
|
b_ptr, b_type, n as i32,
|
||||||
a_ptr, a_type, k as i32, // A as col-major = A^T
|
a_ptr, a_type, k as i32,
|
||||||
&beta as *const f32 as *const c_void,
|
&beta as *const f32 as *const c_void,
|
||||||
c_ptr, c_type, n as i32, // C as col-major = C^T
|
c_ptr, c_type, n as i32,
|
||||||
CUBLAS_COMPUTE_32F,
|
CUBLAS_COMPUTE_32F,
|
||||||
-1, // default algo
|
-1,
|
||||||
)).expect("cuBLAS GEMM failed");
|
)).expect("cuBLAS GEMM failed");
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
pub mod activation;
|
pub mod activation;
|
||||||
|
pub mod argmax;
|
||||||
pub mod attention;
|
pub mod attention;
|
||||||
pub mod dispatch;
|
pub mod dispatch;
|
||||||
pub mod embedding;
|
pub mod embedding;
|
||||||
@@ -10,8 +11,9 @@ pub mod softmax;
|
|||||||
pub mod transpose;
|
pub mod transpose;
|
||||||
|
|
||||||
pub use activation::{add, gelu, mul, scale, silu, silu_mul};
|
pub use activation::{add, gelu, mul, scale, silu, silu_mul};
|
||||||
|
pub use argmax::{argmax_bf16_single, argmax_bf16_to_host};
|
||||||
pub use transpose::{merge_heads_gpu, repeat_kv_gpu, reshape_heads_gpu, strided_to_contiguous_gpu, transpose_for_rope_gpu, transpose_from_rope_gpu};
|
pub use transpose::{merge_heads_gpu, repeat_kv_gpu, reshape_heads_gpu, strided_to_contiguous_gpu, transpose_for_rope_gpu, transpose_from_rope_gpu};
|
||||||
pub use attention::{attention, decode_attention, flash_attention, paged_decode_attention};
|
pub use attention::{attention, decode_attention, flash_attention, paged_decode_attention, reshape_and_cache_bf16, reshape_and_cache_batched_bf16};
|
||||||
pub use embedding::embedding;
|
pub use embedding::embedding;
|
||||||
pub use gemm::{batched_matmul, matmul, GemmBackend};
|
pub use gemm::{batched_matmul, matmul, GemmBackend};
|
||||||
pub use layernorm::layernorm;
|
pub use layernorm::layernorm;
|
||||||
|
|||||||
@@ -305,6 +305,10 @@ impl PagedKVCache {
|
|||||||
/// `k_new`, `v_new`: GPU tensors with logical shape
|
/// `k_new`, `v_new`: GPU tensors with logical shape
|
||||||
/// [1, num_kv_heads, num_tokens, head_dim]
|
/// [1, num_kv_heads, num_tokens, head_dim]
|
||||||
/// stored contiguously (head-major, then tokens, then dim).
|
/// stored contiguously (head-major, then tokens, then dim).
|
||||||
|
///
|
||||||
|
/// Implementation: a single `reshape_and_cache` kernel per call. The
|
||||||
|
/// previous Rust loop fired `num_tokens * num_kv_heads` cudaMemcpys per
|
||||||
|
/// layer (≈290k for a 1024-token Qwen3 prefill across 36 layers).
|
||||||
pub fn append_tokens(
|
pub fn append_tokens(
|
||||||
&mut self,
|
&mut self,
|
||||||
slot: usize,
|
slot: usize,
|
||||||
@@ -318,36 +322,83 @@ impl PagedKVCache {
|
|||||||
// Make sure blocks exist for the target range.
|
// Make sure blocks exist for the target range.
|
||||||
self.ensure_capacity(slot, start_pos + num_tokens);
|
self.ensure_capacity(slot, start_pos + num_tokens);
|
||||||
|
|
||||||
let block_ids = self.seq_states[slot].as_ref().unwrap().block_ids.clone();
|
|
||||||
|
|
||||||
let nkv = self.num_kv_heads;
|
let nkv = self.num_kv_heads;
|
||||||
let hd = self.head_dim;
|
let hd = self.head_dim;
|
||||||
let es = self.elem_size;
|
|
||||||
let bs = BLOCK_SIZE;
|
let bs = BLOCK_SIZE;
|
||||||
|
|
||||||
let k_src = k_new.storage().gpu_buffer();
|
// Stage block_ids on the GPU. Pool-allocated so this is essentially
|
||||||
let v_src = v_new.storage().gpu_buffer();
|
// free after the first call (same bucket every step).
|
||||||
|
let block_ids: Vec<i32> = self.seq_states[slot].as_ref().unwrap()
|
||||||
|
.block_ids.iter().map(|&b| b as i32).collect();
|
||||||
|
let bytes = block_ids.len() * std::mem::size_of::<i32>();
|
||||||
|
let mut block_ids_gpu = xserv_cuda::allocator::cached_alloc(bytes)
|
||||||
|
.expect("alloc append block_ids");
|
||||||
|
let block_ids_bytes = unsafe {
|
||||||
|
std::slice::from_raw_parts(block_ids.as_ptr() as *const u8, bytes)
|
||||||
|
};
|
||||||
|
block_ids_gpu.copy_from_host(block_ids_bytes).expect("upload block_ids");
|
||||||
|
|
||||||
let k_pool = &mut self.k_pools[layer];
|
let k_src = k_new.data_ptr() as *const std::ffi::c_void;
|
||||||
let v_pool = &mut self.v_pools[layer];
|
let v_src = v_new.data_ptr() as *const std::ffi::c_void;
|
||||||
|
let k_pool_ptr = self.k_pools[layer].as_mut_ptr() as *mut std::ffi::c_void;
|
||||||
|
let v_pool_ptr = self.v_pools[layer].as_mut_ptr() as *mut std::ffi::c_void;
|
||||||
|
|
||||||
let mut t = 0usize;
|
unsafe {
|
||||||
while t < num_tokens {
|
xserv_kernels::reshape_and_cache_bf16(
|
||||||
let p = start_pos + t;
|
k_src, v_src,
|
||||||
let logical_blk = p / bs;
|
k_pool_ptr, v_pool_ptr,
|
||||||
let slot_in_blk = p % bs;
|
block_ids_gpu.as_ptr() as *const i32,
|
||||||
let chunk = (bs - slot_in_blk).min(num_tokens - t);
|
num_tokens, nkv, hd, start_pos, bs,
|
||||||
let phys = block_ids[logical_blk] as usize;
|
std::ptr::null_mut(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
// block_ids_gpu drops here; the launch on the null stream will have
|
||||||
|
// finished consuming it before any subsequent op alloc()s the same
|
||||||
|
// bucket (null stream is sequential).
|
||||||
|
}
|
||||||
|
|
||||||
for h in 0..nkv {
|
/// Batched append for the multi-sequence decode step: writes one new
|
||||||
let src_off = (h * num_tokens + t) * hd * es;
|
/// K/V token per active sequence into `layer`'s pool, using
|
||||||
let dst_off = ((phys * nkv + h) * bs + slot_in_blk) * hd * es;
|
/// `block_table_gpu` and `context_lens_gpu` directly. Caller must have
|
||||||
let count = chunk * hd * es;
|
/// just run `sync_active_batch_with_lens(slots, kv_lens)` so that:
|
||||||
k_pool.copy_from_device_at(k_src, src_off, dst_off, count).unwrap();
|
/// - row `i` of block_table_gpu holds the block ids for `slots[i]`
|
||||||
v_pool.copy_from_device_at(v_src, src_off, dst_off, count).unwrap();
|
/// - context_lens_gpu[i] == seq_len(slots[i]) + 1 (the kv_len **after**
|
||||||
}
|
/// this step — i.e., the new token will be written at index kv_len-1)
|
||||||
|
///
|
||||||
|
/// `k_new`, `v_new`: GPU tensors, contiguous, BF16, shape
|
||||||
|
/// `[batch, num_kv_heads, head_dim]`.
|
||||||
|
///
|
||||||
|
/// Like `append_tokens`, this does **not** touch `seq_len`. Call
|
||||||
|
/// `advance_seq_len(slot, 1)` for each slot after every layer has been
|
||||||
|
/// written.
|
||||||
|
pub fn append_tokens_batched(
|
||||||
|
&mut self,
|
||||||
|
layer: usize,
|
||||||
|
k_new: &Tensor,
|
||||||
|
v_new: &Tensor,
|
||||||
|
batch: usize,
|
||||||
|
) {
|
||||||
|
if batch == 0 { return; }
|
||||||
|
let nkv = self.num_kv_heads;
|
||||||
|
let hd = self.head_dim;
|
||||||
|
debug_assert_eq!(k_new.shape(), &[batch, nkv, hd]);
|
||||||
|
debug_assert_eq!(v_new.shape(), &[batch, nkv, hd]);
|
||||||
|
|
||||||
t += chunk;
|
let k_src = k_new.data_ptr() as *const std::ffi::c_void;
|
||||||
|
let v_src = v_new.data_ptr() as *const std::ffi::c_void;
|
||||||
|
let k_pool_ptr = self.k_pools[layer].as_mut_ptr() as *mut std::ffi::c_void;
|
||||||
|
let v_pool_ptr = self.v_pools[layer].as_mut_ptr() as *mut std::ffi::c_void;
|
||||||
|
let bt_ptr = self.block_table_gpu.as_ptr() as *const i32;
|
||||||
|
let cl_ptr = self.context_lens_gpu.as_ptr() as *const i32;
|
||||||
|
|
||||||
|
unsafe {
|
||||||
|
xserv_kernels::reshape_and_cache_batched_bf16(
|
||||||
|
k_src, v_src,
|
||||||
|
k_pool_ptr, v_pool_ptr,
|
||||||
|
bt_ptr, cl_ptr,
|
||||||
|
batch, nkv, hd, BLOCK_SIZE, self.max_blocks_per_seq,
|
||||||
|
std::ptr::null_mut(),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -24,18 +24,31 @@ pub struct Qwen3 {
|
|||||||
|
|
||||||
struct Qwen3Block {
|
struct Qwen3Block {
|
||||||
input_norm: Tensor, // [hidden]
|
input_norm: Tensor, // [hidden]
|
||||||
q_proj_wt: Tensor, // TRANSPOSED: [hidden, num_heads*head_dim]
|
qkv_proj_wt: Tensor, // FUSED: [hidden, (H+2*KV)*D] — Q|K|V columns
|
||||||
k_proj_wt: Tensor, // TRANSPOSED: [hidden, num_kv_heads*head_dim]
|
q_dim: usize, // num_heads * head_dim (Q slice boundary)
|
||||||
v_proj_wt: Tensor,
|
kv_dim: usize, // num_kv_heads * head_dim (K/V slice size)
|
||||||
o_proj_wt: Tensor, // TRANSPOSED: [num_heads*head_dim, hidden]
|
o_proj_wt: Tensor, // TRANSPOSED: [num_heads*head_dim, hidden]
|
||||||
q_norm: Tensor, // [head_dim]
|
q_norm: Tensor, // [head_dim]
|
||||||
k_norm: Tensor, // [head_dim]
|
k_norm: Tensor, // [head_dim]
|
||||||
post_norm: Tensor, // [hidden]
|
post_norm: Tensor, // [hidden]
|
||||||
gate_proj_wt: Tensor, // TRANSPOSED: [hidden, intermediate]
|
gate_up_proj_wt: Tensor, // FUSED: [hidden, 2*intermediate]
|
||||||
up_proj_wt: Tensor,
|
|
||||||
down_proj_wt: Tensor, // TRANSPOSED: [intermediate, hidden]
|
down_proj_wt: Tensor, // TRANSPOSED: [intermediate, hidden]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Qwen3Block {
|
||||||
|
fn q_proj_wt(&self) -> Tensor { self.qkv_proj_wt.narrow(1, 0, self.q_dim) }
|
||||||
|
fn k_proj_wt(&self) -> Tensor { self.qkv_proj_wt.narrow(1, self.q_dim, self.kv_dim) }
|
||||||
|
fn v_proj_wt(&self) -> Tensor { self.qkv_proj_wt.narrow(1, self.q_dim + self.kv_dim, self.kv_dim) }
|
||||||
|
fn gate_proj_wt(&self) -> Tensor {
|
||||||
|
let half = self.gate_up_proj_wt.shape()[1] / 2;
|
||||||
|
self.gate_up_proj_wt.narrow(1, 0, half)
|
||||||
|
}
|
||||||
|
fn up_proj_wt(&self) -> Tensor {
|
||||||
|
let half = self.gate_up_proj_wt.shape()[1] / 2;
|
||||||
|
self.gate_up_proj_wt.narrow(1, half, half)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl Qwen3 {
|
impl Qwen3 {
|
||||||
/// Single-GPU load (weights already on the target GPU). Equivalent to
|
/// Single-GPU load (weights already on the target GPU). Equivalent to
|
||||||
/// `from_weights_tp(.., rank=0, world=1, device=0, tp=None)`.
|
/// `from_weights_tp(.., rank=0, world=1, device=0, tp=None)`.
|
||||||
@@ -88,17 +101,28 @@ impl Qwen3 {
|
|||||||
}
|
}
|
||||||
for i in 0..num_layers {
|
for i in 0..num_layers {
|
||||||
let p = format!("model.layers.{i}");
|
let p = format!("model.layers.{i}");
|
||||||
|
let q_proj_wt = col(take(&mut w, &format!("{p}.self_attn.q_proj.weight")));
|
||||||
|
let k_proj_wt = col(take(&mut w, &format!("{p}.self_attn.k_proj.weight")));
|
||||||
|
let v_proj_wt = col(take(&mut w, &format!("{p}.self_attn.v_proj.weight")));
|
||||||
|
let q_dim = q_proj_wt.shape()[1];
|
||||||
|
let kv_dim = k_proj_wt.shape()[1];
|
||||||
|
let qkv_proj_wt = cat_cols(&[&q_proj_wt, &k_proj_wt, &v_proj_wt]);
|
||||||
|
drop((q_proj_wt, k_proj_wt, v_proj_wt));
|
||||||
|
let gate_proj_wt = col(take(&mut w, &format!("{p}.mlp.gate_proj.weight")));
|
||||||
|
let up_proj_wt = col(take(&mut w, &format!("{p}.mlp.up_proj.weight")));
|
||||||
|
let gate_up_proj_wt = cat_cols(&[&gate_proj_wt, &up_proj_wt]);
|
||||||
|
drop((gate_proj_wt, up_proj_wt));
|
||||||
|
xserv_cuda::allocator::cached_trim();
|
||||||
layers.push(Qwen3Block {
|
layers.push(Qwen3Block {
|
||||||
input_norm: repl(take(&mut w, &format!("{p}.input_layernorm.weight"))),
|
input_norm: repl(take(&mut w, &format!("{p}.input_layernorm.weight"))),
|
||||||
q_proj_wt: col(take(&mut w, &format!("{p}.self_attn.q_proj.weight"))),
|
qkv_proj_wt,
|
||||||
k_proj_wt: col(take(&mut w, &format!("{p}.self_attn.k_proj.weight"))),
|
q_dim,
|
||||||
v_proj_wt: col(take(&mut w, &format!("{p}.self_attn.v_proj.weight"))),
|
kv_dim,
|
||||||
o_proj_wt: row(take(&mut w, &format!("{p}.self_attn.o_proj.weight"))),
|
o_proj_wt: row(take(&mut w, &format!("{p}.self_attn.o_proj.weight"))),
|
||||||
q_norm: repl(take(&mut w, &format!("{p}.self_attn.q_norm.weight"))),
|
q_norm: repl(take(&mut w, &format!("{p}.self_attn.q_norm.weight"))),
|
||||||
k_norm: repl(take(&mut w, &format!("{p}.self_attn.k_norm.weight"))),
|
k_norm: repl(take(&mut w, &format!("{p}.self_attn.k_norm.weight"))),
|
||||||
post_norm: repl(take(&mut w, &format!("{p}.post_attention_layernorm.weight"))),
|
post_norm: repl(take(&mut w, &format!("{p}.post_attention_layernorm.weight"))),
|
||||||
gate_proj_wt: col(take(&mut w, &format!("{p}.mlp.gate_proj.weight"))),
|
gate_up_proj_wt,
|
||||||
up_proj_wt: col(take(&mut w, &format!("{p}.mlp.up_proj.weight"))),
|
|
||||||
down_proj_wt: row(take(&mut w, &format!("{p}.mlp.down_proj.weight"))),
|
down_proj_wt: row(take(&mut w, &format!("{p}.mlp.down_proj.weight"))),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -144,52 +168,45 @@ impl Qwen3 {
|
|||||||
let residual = x.clone();
|
let residual = x.clone();
|
||||||
let normed = rmsnorm(&x, &layer.input_norm, eps);
|
let normed = rmsnorm(&x, &layer.input_norm, eps);
|
||||||
|
|
||||||
// Q/K/V projections (pre-transposed weights, x @ wt)
|
let qkv = matmul_2d(&normed, &layer.qkv_proj_wt);
|
||||||
let q = matmul_2d(&normed, &layer.q_proj_wt);
|
let q = qkv.narrow(1, 0, layer.q_dim).contiguous();
|
||||||
let k = matmul_2d(&normed, &layer.k_proj_wt);
|
let k = qkv.narrow(1, layer.q_dim, layer.kv_dim).contiguous();
|
||||||
let v = matmul_2d(&normed, &layer.v_proj_wt);
|
let v = qkv.narrow(1, layer.q_dim + layer.kv_dim, layer.kv_dim).contiguous();
|
||||||
|
|
||||||
// Reshape to [1, heads, seq, head_dim]
|
|
||||||
let q = reshape_heads(&q, new_tokens, num_heads, head_dim);
|
let q = reshape_heads(&q, new_tokens, num_heads, head_dim);
|
||||||
let k = reshape_heads(&k, new_tokens, num_kv_heads, head_dim);
|
let k = reshape_heads(&k, new_tokens, num_kv_heads, head_dim);
|
||||||
let v = reshape_heads(&v, new_tokens, num_kv_heads, head_dim);
|
let v = reshape_heads(&v, new_tokens, num_kv_heads, head_dim);
|
||||||
|
|
||||||
// QK normalization (per-head RMSNorm)
|
|
||||||
let q = head_rmsnorm(&q, &layer.q_norm, eps);
|
let q = head_rmsnorm(&q, &layer.q_norm, eps);
|
||||||
let k = head_rmsnorm(&k, &layer.k_norm, eps);
|
let k = head_rmsnorm(&k, &layer.k_norm, eps);
|
||||||
|
|
||||||
// RoPE — kernel expects [S, H, D], our tensors are [1, H, S, D]
|
|
||||||
// Transpose to [1, S, H, D] → reshape to [S, H, D] for RoPE
|
|
||||||
let q = transpose_for_rope(&q, new_tokens, num_heads, head_dim);
|
let q = transpose_for_rope(&q, new_tokens, num_heads, head_dim);
|
||||||
let k = transpose_for_rope(&k, new_tokens, num_kv_heads, head_dim);
|
let k = transpose_for_rope(&k, new_tokens, num_kv_heads, head_dim);
|
||||||
rope_inplace(&q, &self.rope_cache, &positions);
|
rope_inplace(&q, &self.rope_cache, &positions);
|
||||||
rope_inplace(&k, &self.rope_cache, &positions);
|
rope_inplace(&k, &self.rope_cache, &positions);
|
||||||
// Transpose back to [1, H, S, D]
|
|
||||||
let q = transpose_from_rope(&q, new_tokens, num_heads, head_dim);
|
let q = transpose_from_rope(&q, new_tokens, num_heads, head_dim);
|
||||||
let k = transpose_from_rope(&k, new_tokens, num_kv_heads, head_dim);
|
let k = transpose_from_rope(&k, new_tokens, num_kv_heads, head_dim);
|
||||||
|
|
||||||
// KV cache
|
|
||||||
let k_cpu = k.to_device(Device::Cpu);
|
let k_cpu = k.to_device(Device::Cpu);
|
||||||
let v_cpu = v.to_device(Device::Cpu);
|
let v_cpu = v.to_device(Device::Cpu);
|
||||||
cache.append_kv_tensor(layer_idx, &k_cpu, &v_cpu, new_tokens);
|
cache.append_kv_tensor(layer_idx, &k_cpu, &v_cpu, new_tokens);
|
||||||
let (k_full, v_full) = cache.get_kv_tensors(layer_idx);
|
let (k_full, v_full) = cache.get_kv_tensors(layer_idx);
|
||||||
|
|
||||||
// GQA: repeat K/V
|
|
||||||
let n_rep = num_heads / num_kv_heads;
|
let n_rep = num_heads / num_kv_heads;
|
||||||
let k_full = repeat_kv(&k_full, n_rep);
|
let k_full = repeat_kv(&k_full, n_rep);
|
||||||
let v_full = repeat_kv(&v_full, n_rep);
|
let v_full = repeat_kv(&v_full, n_rep);
|
||||||
|
|
||||||
// Attention
|
|
||||||
let attn_out = attention(&q, &k_full, &v_full, true);
|
let attn_out = attention(&q, &k_full, &v_full, true);
|
||||||
let attn_merged = merge_heads_any(&attn_out, new_tokens, hidden);
|
let attn_merged = merge_heads_any(&attn_out, new_tokens, hidden);
|
||||||
let attn_proj = matmul_2d(&attn_merged, &layer.o_proj_wt);
|
let attn_proj = matmul_2d(&attn_merged, &layer.o_proj_wt);
|
||||||
x = add_any(&residual, &attn_proj);
|
x = add_any(&residual, &attn_proj);
|
||||||
|
|
||||||
// SwiGLU FFN
|
|
||||||
let residual = x.clone();
|
let residual = x.clone();
|
||||||
let normed = rmsnorm(&x, &layer.post_norm, eps);
|
let normed = rmsnorm(&x, &layer.post_norm, eps);
|
||||||
let gate = matmul_2d(&normed, &layer.gate_proj_wt);
|
let gate_up = matmul_2d(&normed, &layer.gate_up_proj_wt);
|
||||||
let up = matmul_2d(&normed, &layer.up_proj_wt);
|
let ffn_dim = gate_up.shape()[1] / 2;
|
||||||
|
let gate = gate_up.narrow(1, 0, ffn_dim).contiguous();
|
||||||
|
let up = gate_up.narrow(1, ffn_dim, ffn_dim).contiguous();
|
||||||
let gate_activated = silu(&gate);
|
let gate_activated = silu(&gate);
|
||||||
let hidden_states = mul_any(&gate_activated, &up);
|
let hidden_states = mul_any(&gate_activated, &up);
|
||||||
let down = matmul_2d(&hidden_states, &layer.down_proj_wt);
|
let down = matmul_2d(&hidden_states, &layer.down_proj_wt);
|
||||||
@@ -232,10 +249,10 @@ impl Qwen3 {
|
|||||||
let residual = x.clone();
|
let residual = x.clone();
|
||||||
let normed = rmsnorm(&x, &layer.input_norm, eps); // [B, hidden]
|
let normed = rmsnorm(&x, &layer.input_norm, eps); // [B, hidden]
|
||||||
|
|
||||||
// Batched projections: [B, hidden] × [hidden, X] = [B, X]
|
let qkv = matmul_2d(&normed, &layer.qkv_proj_wt);
|
||||||
let q_all = matmul_2d(&normed, &layer.q_proj_wt); // [B, num_heads*head_dim]
|
let q_all = qkv.narrow(1, 0, layer.q_dim).contiguous();
|
||||||
let k_all = matmul_2d(&normed, &layer.k_proj_wt); // [B, num_kv_heads*head_dim]
|
let k_all = qkv.narrow(1, layer.q_dim, layer.kv_dim).contiguous();
|
||||||
let v_all = matmul_2d(&normed, &layer.v_proj_wt); // [B, num_kv_heads*head_dim]
|
let v_all = qkv.narrow(1, layer.q_dim + layer.kv_dim, layer.kv_dim).contiguous();
|
||||||
|
|
||||||
// Per-sequence: reshape, qk-norm, RoPE, KV cache, attention, merge
|
// Per-sequence: reshape, qk-norm, RoPE, KV cache, attention, merge
|
||||||
let mut attn_outputs: Vec<Tensor> = Vec::with_capacity(batch);
|
let mut attn_outputs: Vec<Tensor> = Vec::with_capacity(batch);
|
||||||
@@ -290,9 +307,10 @@ impl Qwen3 {
|
|||||||
let (normed, x_new) = xserv_kernels::add_rmsnorm(&attn_proj, &residual, &layer.post_norm, eps);
|
let (normed, x_new) = xserv_kernels::add_rmsnorm(&attn_proj, &residual, &layer.post_norm, eps);
|
||||||
let residual = x_new.clone();
|
let residual = x_new.clone();
|
||||||
|
|
||||||
// Batched FFN: all projections on [B, hidden]
|
let gate_up = matmul_2d(&normed, &layer.gate_up_proj_wt);
|
||||||
let gate = matmul_2d(&normed, &layer.gate_proj_wt);
|
let ffn_dim = gate_up.shape()[1] / 2;
|
||||||
let up = matmul_2d(&normed, &layer.up_proj_wt);
|
let gate = gate_up.narrow(1, 0, ffn_dim).contiguous();
|
||||||
|
let up = gate_up.narrow(1, ffn_dim, ffn_dim).contiguous();
|
||||||
let hidden_states = xserv_kernels::silu_mul(&gate, &up);
|
let hidden_states = xserv_kernels::silu_mul(&gate, &up);
|
||||||
let down = matmul_2d(&hidden_states, &layer.down_proj_wt);
|
let down = matmul_2d(&hidden_states, &layer.down_proj_wt);
|
||||||
x = add_any(&residual, &down);
|
x = add_any(&residual, &down);
|
||||||
@@ -312,6 +330,13 @@ impl Qwen3 {
|
|||||||
/// tokens: [B] one token per sequence
|
/// tokens: [B] one token per sequence
|
||||||
/// positions: [B] current logical position (BEFORE this step) per sequence
|
/// positions: [B] current logical position (BEFORE this step) per sequence
|
||||||
/// seq_slots: [B] slot ids in `paged_cache`
|
/// seq_slots: [B] slot ids in `paged_cache`
|
||||||
|
///
|
||||||
|
/// Layout note: for S=1 decode the memory of `[B, H, 1, D]`,
|
||||||
|
/// `[B, H, D]`, and `[B, H*D]` is the same — only shape/strides differ.
|
||||||
|
/// We exploit this to drop every per-sequence kernel: head_rmsnorm and
|
||||||
|
/// RoPE both natively accept the `[B*H, D]` / `[B, H, D]` layouts that
|
||||||
|
/// fall out of the projection matmuls, and the new-token KV scatter is
|
||||||
|
/// one batched `reshape_and_cache` kernel.
|
||||||
pub fn forward_decode_paged(
|
pub fn forward_decode_paged(
|
||||||
&self,
|
&self,
|
||||||
tokens: &[u32],
|
tokens: &[u32],
|
||||||
@@ -343,6 +368,10 @@ impl Qwen3 {
|
|||||||
let cl_ptr = paged_cache.context_lens_gpu().as_ptr() as *const i32;
|
let cl_ptr = paged_cache.context_lens_gpu().as_ptr() as *const i32;
|
||||||
let max_blocks = paged_cache.max_blocks_per_seq();
|
let max_blocks = paged_cache.max_blocks_per_seq();
|
||||||
|
|
||||||
|
// RoPE expects `[num_tokens, H, D]` with `num_tokens` positions —
|
||||||
|
// matches our `[B, H, D]` exactly, so we upload once here.
|
||||||
|
let positions_u32: Vec<u32> = positions.iter().map(|&p| p as u32).collect();
|
||||||
|
|
||||||
// Batched embedding: [B, hidden]
|
// Batched embedding: [B, hidden]
|
||||||
let mut x = embedding(&self.embed_tokens, tokens);
|
let mut x = embedding(&self.embed_tokens, tokens);
|
||||||
|
|
||||||
@@ -350,62 +379,41 @@ impl Qwen3 {
|
|||||||
let residual = x.clone();
|
let residual = x.clone();
|
||||||
let normed = rmsnorm(&x, &layer.input_norm, eps);
|
let normed = rmsnorm(&x, &layer.input_norm, eps);
|
||||||
|
|
||||||
let q_all = matmul_2d(&normed, &layer.q_proj_wt);
|
// Fused QKV projection: one GEMV instead of three.
|
||||||
let k_all = matmul_2d(&normed, &layer.k_proj_wt);
|
let qkv = matmul_2d(&normed, &layer.qkv_proj_wt); // [B, (H+2*KV)*D]
|
||||||
let v_all = matmul_2d(&normed, &layer.v_proj_wt);
|
let q_dim = num_heads * head_dim;
|
||||||
|
let kv_dim = num_kv_heads * head_dim;
|
||||||
|
let q_all = qkv.narrow(1, 0, q_dim); // [B, H*D] (view)
|
||||||
|
let k_all = qkv.narrow(1, q_dim, kv_dim); // [B, KV*D] (view)
|
||||||
|
let v_all = qkv.narrow(1, q_dim + kv_dim, kv_dim);
|
||||||
|
|
||||||
let mut q_rows: Vec<Tensor> = Vec::with_capacity(batch);
|
// Per-head RMSNorm on contiguous copies (narrow views are strided).
|
||||||
for b in 0..batch {
|
let q_flat = q_all.contiguous().reshape(&[batch * num_heads, head_dim]);
|
||||||
let q_row = row_view(&q_all, b);
|
let k_flat = k_all.contiguous().reshape(&[batch * num_kv_heads, head_dim]);
|
||||||
let k_row = row_view(&k_all, b);
|
let q_normed = rmsnorm(&q_flat, &layer.q_norm, eps);
|
||||||
let v_row = row_view(&v_all, b);
|
let k_normed = rmsnorm(&k_flat, &layer.k_norm, eps);
|
||||||
|
|
||||||
let q = xserv_kernels::reshape_heads_gpu(&q_row, 1, num_heads, head_dim);
|
let q_3d = q_normed.reshape(&[batch, num_heads, head_dim]);
|
||||||
let k = xserv_kernels::reshape_heads_gpu(&k_row, 1, num_kv_heads, head_dim);
|
let k_3d = k_normed.reshape(&[batch, num_kv_heads, head_dim]);
|
||||||
let v = xserv_kernels::reshape_heads_gpu(&v_row, 1, num_kv_heads, head_dim);
|
rope_inplace(&q_3d, &self.rope_cache, &positions_u32);
|
||||||
|
rope_inplace(&k_3d, &self.rope_cache, &positions_u32);
|
||||||
|
|
||||||
let q = head_rmsnorm(&q, &layer.q_norm, eps);
|
let v_3d = v_all.contiguous().reshape(&[batch, num_kv_heads, head_dim]);
|
||||||
let k = head_rmsnorm(&k, &layer.k_norm, eps);
|
|
||||||
|
|
||||||
let q = xserv_kernels::transpose_for_rope_gpu(&q, 1, num_heads, head_dim);
|
// Single batched scatter for all sequences in the batch.
|
||||||
let k = xserv_kernels::transpose_for_rope_gpu(&k, 1, num_kv_heads, head_dim);
|
paged_cache.append_tokens_batched(layer_idx, &k_3d, &v_3d, batch);
|
||||||
|
|
||||||
let pos = [positions[b] as u32];
|
|
||||||
rope_inplace(&q, &self.rope_cache, &pos);
|
|
||||||
rope_inplace(&k, &self.rope_cache, &pos);
|
|
||||||
|
|
||||||
let q = xserv_kernels::transpose_from_rope_gpu(&q, 1, num_heads, head_dim);
|
|
||||||
let k = xserv_kernels::transpose_from_rope_gpu(&k, 1, num_kv_heads, head_dim);
|
|
||||||
|
|
||||||
paged_cache.append_tokens(seq_slots[b], layer_idx, &k, &v, 1, positions[b]);
|
|
||||||
|
|
||||||
let q_flat = xserv_kernels::merge_heads_gpu(&q, 1, num_heads, head_dim);
|
|
||||||
q_rows.push(q_flat);
|
|
||||||
}
|
|
||||||
|
|
||||||
let q_batched_2d = concat_rows(&q_rows);
|
|
||||||
// q_batched_2d: [B, num_heads * head_dim]. Memory is [B, H, D] —
|
|
||||||
// a plain reshape view to [B, H, 1, D] is what the paged kernel expects.
|
|
||||||
let q_4d = q_batched_2d.reshape(&[batch, num_heads, 1, head_dim]);
|
|
||||||
|
|
||||||
|
// Paged attention reads Q as [B, H, 1, D] — a contiguous view
|
||||||
|
// of [B, H, D].
|
||||||
|
let q_4d = q_3d.reshape(&[batch, num_heads, 1, head_dim]);
|
||||||
let k_pool_ptr = paged_cache.k_pool(layer_idx).as_ptr() as *const std::ffi::c_void;
|
let k_pool_ptr = paged_cache.k_pool(layer_idx).as_ptr() as *const std::ffi::c_void;
|
||||||
let v_pool_ptr = paged_cache.v_pool(layer_idx).as_ptr() as *const std::ffi::c_void;
|
let v_pool_ptr = paged_cache.v_pool(layer_idx).as_ptr() as *const std::ffi::c_void;
|
||||||
|
|
||||||
let attn_out = xserv_kernels::paged_decode_attention(
|
let attn_out = xserv_kernels::paged_decode_attention(
|
||||||
&q_4d,
|
&q_4d, k_pool_ptr, v_pool_ptr, bt_ptr, cl_ptr,
|
||||||
k_pool_ptr,
|
batch, num_heads, num_kv_heads, head_dim, max_blocks,
|
||||||
v_pool_ptr,
|
|
||||||
bt_ptr,
|
|
||||||
cl_ptr,
|
|
||||||
batch,
|
|
||||||
num_heads,
|
|
||||||
num_kv_heads,
|
|
||||||
head_dim,
|
|
||||||
max_blocks,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
// attn_out shape [B, H, 1, D] is contiguous-equivalent to [B, H*D].
|
// attn_out shape [B, H, 1, D] is contiguous-equivalent to [B, H*D].
|
||||||
// Plain reshape is a view; merge_heads_gpu would incorrectly swap B<->H.
|
|
||||||
let attn_merged = attn_out.reshape(&[batch, num_heads * head_dim]);
|
let attn_merged = attn_out.reshape(&[batch, num_heads * head_dim]);
|
||||||
let attn_proj = matmul_2d(&attn_merged, &layer.o_proj_wt);
|
let attn_proj = matmul_2d(&attn_merged, &layer.o_proj_wt);
|
||||||
self.all_reduce(&attn_proj); // TP: sum partial attention outputs
|
self.all_reduce(&attn_proj); // TP: sum partial attention outputs
|
||||||
@@ -413,8 +421,11 @@ impl Qwen3 {
|
|||||||
let (normed, x_new) = xserv_kernels::add_rmsnorm(&attn_proj, &residual, &layer.post_norm, eps);
|
let (normed, x_new) = xserv_kernels::add_rmsnorm(&attn_proj, &residual, &layer.post_norm, eps);
|
||||||
let residual = x_new.clone();
|
let residual = x_new.clone();
|
||||||
|
|
||||||
let gate = matmul_2d(&normed, &layer.gate_proj_wt);
|
// Fused gate+up projection: one GEMV instead of two.
|
||||||
let up = matmul_2d(&normed, &layer.up_proj_wt);
|
let gate_up = matmul_2d(&normed, &layer.gate_up_proj_wt); // [B, 2*ffn]
|
||||||
|
let ffn_dim = gate_up.shape()[1] / 2;
|
||||||
|
let gate = gate_up.narrow(1, 0, ffn_dim).contiguous();
|
||||||
|
let up = gate_up.narrow(1, ffn_dim, ffn_dim).contiguous();
|
||||||
let hidden_states = xserv_kernels::silu_mul(&gate, &up);
|
let hidden_states = xserv_kernels::silu_mul(&gate, &up);
|
||||||
let down = matmul_2d(&hidden_states, &layer.down_proj_wt);
|
let down = matmul_2d(&hidden_states, &layer.down_proj_wt);
|
||||||
self.all_reduce(&down); // TP: sum partial MLP outputs
|
self.all_reduce(&down); // TP: sum partial MLP outputs
|
||||||
@@ -459,9 +470,10 @@ impl Qwen3 {
|
|||||||
let residual = x.clone();
|
let residual = x.clone();
|
||||||
let normed = rmsnorm(&x, &layer.input_norm, eps);
|
let normed = rmsnorm(&x, &layer.input_norm, eps);
|
||||||
|
|
||||||
let q = matmul_2d(&normed, &layer.q_proj_wt);
|
let qkv = matmul_2d(&normed, &layer.qkv_proj_wt);
|
||||||
let k = matmul_2d(&normed, &layer.k_proj_wt);
|
let q = qkv.narrow(1, 0, layer.q_dim).contiguous();
|
||||||
let v = matmul_2d(&normed, &layer.v_proj_wt);
|
let k = qkv.narrow(1, layer.q_dim, layer.kv_dim).contiguous();
|
||||||
|
let v = qkv.narrow(1, layer.q_dim + layer.kv_dim, layer.kv_dim).contiguous();
|
||||||
|
|
||||||
let q = xserv_kernels::reshape_heads_gpu(&q, new_tokens, num_heads, head_dim);
|
let q = xserv_kernels::reshape_heads_gpu(&q, new_tokens, num_heads, head_dim);
|
||||||
let k = xserv_kernels::reshape_heads_gpu(&k, new_tokens, num_kv_heads, head_dim);
|
let k = xserv_kernels::reshape_heads_gpu(&k, new_tokens, num_kv_heads, head_dim);
|
||||||
@@ -477,25 +489,25 @@ impl Qwen3 {
|
|||||||
let q = xserv_kernels::transpose_from_rope_gpu(&q, new_tokens, num_heads, head_dim);
|
let q = xserv_kernels::transpose_from_rope_gpu(&q, new_tokens, num_heads, head_dim);
|
||||||
let k = xserv_kernels::transpose_from_rope_gpu(&k, new_tokens, num_kv_heads, head_dim);
|
let k = xserv_kernels::transpose_from_rope_gpu(&k, new_tokens, num_kv_heads, head_dim);
|
||||||
|
|
||||||
// Write into paged pool at the original (pre-advance) position.
|
|
||||||
paged_cache.append_tokens(slot, layer_idx, &k, &v, new_tokens, pos_offset);
|
paged_cache.append_tokens(slot, layer_idx, &k, &v, new_tokens, pos_offset);
|
||||||
|
|
||||||
// Gather contiguous K/V for the full sequence (seq_len already includes new_tokens).
|
|
||||||
let (k_full, v_full) = paged_cache.gather_kv_contiguous(slot, layer_idx);
|
let (k_full, v_full) = paged_cache.gather_kv_contiguous(slot, layer_idx);
|
||||||
let attn_out = flash_attention(&q, &k_full, &v_full, true);
|
let attn_out = flash_attention(&q, &k_full, &v_full, true);
|
||||||
|
|
||||||
let attn_merged = xserv_kernels::merge_heads_gpu(&attn_out, new_tokens, num_heads, head_dim);
|
let attn_merged = xserv_kernels::merge_heads_gpu(&attn_out, new_tokens, num_heads, head_dim);
|
||||||
let attn_proj = matmul_2d(&attn_merged, &layer.o_proj_wt);
|
let attn_proj = matmul_2d(&attn_merged, &layer.o_proj_wt);
|
||||||
self.all_reduce(&attn_proj); // TP: sum partial attention outputs
|
self.all_reduce(&attn_proj);
|
||||||
|
|
||||||
let (normed, x_new) = xserv_kernels::add_rmsnorm(&attn_proj, &residual, &layer.post_norm, eps);
|
let (normed, x_new) = xserv_kernels::add_rmsnorm(&attn_proj, &residual, &layer.post_norm, eps);
|
||||||
let residual = x_new.clone();
|
let residual = x_new.clone();
|
||||||
|
|
||||||
let gate = matmul_2d(&normed, &layer.gate_proj_wt);
|
let gate_up = matmul_2d(&normed, &layer.gate_up_proj_wt);
|
||||||
let up = matmul_2d(&normed, &layer.up_proj_wt);
|
let ffn_dim = gate_up.shape()[1] / 2;
|
||||||
|
let gate = gate_up.narrow(1, 0, ffn_dim).contiguous();
|
||||||
|
let up = gate_up.narrow(1, ffn_dim, ffn_dim).contiguous();
|
||||||
let hidden_states = xserv_kernels::silu_mul(&gate, &up);
|
let hidden_states = xserv_kernels::silu_mul(&gate, &up);
|
||||||
let down = matmul_2d(&hidden_states, &layer.down_proj_wt);
|
let down = matmul_2d(&hidden_states, &layer.down_proj_wt);
|
||||||
self.all_reduce(&down); // TP: sum partial MLP outputs
|
self.all_reduce(&down);
|
||||||
x = add_any(&residual, &down);
|
x = add_any(&residual, &down);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -520,45 +532,39 @@ impl Qwen3 {
|
|||||||
let residual = x.clone();
|
let residual = x.clone();
|
||||||
let normed = rmsnorm(&x, &layer.input_norm, eps);
|
let normed = rmsnorm(&x, &layer.input_norm, eps);
|
||||||
|
|
||||||
let q = matmul_2d(&normed, &layer.q_proj_wt);
|
let qkv = matmul_2d(&normed, &layer.qkv_proj_wt);
|
||||||
let k = matmul_2d(&normed, &layer.k_proj_wt);
|
let q = qkv.narrow(1, 0, layer.q_dim).contiguous();
|
||||||
let v = matmul_2d(&normed, &layer.v_proj_wt);
|
let k = qkv.narrow(1, layer.q_dim, layer.kv_dim).contiguous();
|
||||||
|
let v = qkv.narrow(1, layer.q_dim + layer.kv_dim, layer.kv_dim).contiguous();
|
||||||
|
|
||||||
// GPU reshape: [S, H*D] → [1, H, S, D]
|
|
||||||
let q = xserv_kernels::reshape_heads_gpu(&q, new_tokens, num_heads, head_dim);
|
let q = xserv_kernels::reshape_heads_gpu(&q, new_tokens, num_heads, head_dim);
|
||||||
let k = xserv_kernels::reshape_heads_gpu(&k, new_tokens, num_kv_heads, head_dim);
|
let k = xserv_kernels::reshape_heads_gpu(&k, new_tokens, num_kv_heads, head_dim);
|
||||||
let v = xserv_kernels::reshape_heads_gpu(&v, new_tokens, num_kv_heads, head_dim);
|
let v = xserv_kernels::reshape_heads_gpu(&v, new_tokens, num_kv_heads, head_dim);
|
||||||
|
|
||||||
// QK norm (reshape to [H*S, D], rmsnorm, reshape back — stays on GPU)
|
|
||||||
let q = head_rmsnorm(&q, &layer.q_norm, eps);
|
let q = head_rmsnorm(&q, &layer.q_norm, eps);
|
||||||
let k = head_rmsnorm(&k, &layer.k_norm, eps);
|
let k = head_rmsnorm(&k, &layer.k_norm, eps);
|
||||||
|
|
||||||
// GPU transpose for RoPE: [1, H, S, D] → [S, H, D]
|
|
||||||
let q = xserv_kernels::transpose_for_rope_gpu(&q, new_tokens, num_heads, head_dim);
|
let q = xserv_kernels::transpose_for_rope_gpu(&q, new_tokens, num_heads, head_dim);
|
||||||
let k = xserv_kernels::transpose_for_rope_gpu(&k, new_tokens, num_kv_heads, head_dim);
|
let k = xserv_kernels::transpose_for_rope_gpu(&k, new_tokens, num_kv_heads, head_dim);
|
||||||
rope_inplace(&q, &self.rope_cache, &positions);
|
rope_inplace(&q, &self.rope_cache, &positions);
|
||||||
rope_inplace(&k, &self.rope_cache, &positions);
|
rope_inplace(&k, &self.rope_cache, &positions);
|
||||||
// GPU transpose back: [S, H, D] → [1, H, S, D]
|
|
||||||
let q = xserv_kernels::transpose_from_rope_gpu(&q, new_tokens, num_heads, head_dim);
|
let q = xserv_kernels::transpose_from_rope_gpu(&q, new_tokens, num_heads, head_dim);
|
||||||
let k = xserv_kernels::transpose_from_rope_gpu(&k, new_tokens, num_kv_heads, head_dim);
|
let k = xserv_kernels::transpose_from_rope_gpu(&k, new_tokens, num_kv_heads, head_dim);
|
||||||
|
|
||||||
// GPU KV cache
|
|
||||||
cache.append(layer_idx, &k, &v, new_tokens, pos_offset);
|
cache.append(layer_idx, &k, &v, new_tokens, pos_offset);
|
||||||
let (k_full, v_full) = cache.get_kv_len(layer_idx, pos_offset + new_tokens);
|
let (k_full, v_full) = cache.get_kv_len(layer_idx, pos_offset + new_tokens);
|
||||||
|
|
||||||
// Flash Attention with native GQA (no repeat_kv needed)
|
|
||||||
let attn_out = flash_attention(&q, &k_full, &v_full, true);
|
let attn_out = flash_attention(&q, &k_full, &v_full, true);
|
||||||
// GPU merge_heads: [1, H, S, D] → [S, H*D]
|
|
||||||
let attn_merged = xserv_kernels::merge_heads_gpu(&attn_out, new_tokens, num_heads, head_dim);
|
let attn_merged = xserv_kernels::merge_heads_gpu(&attn_out, new_tokens, num_heads, head_dim);
|
||||||
let attn_proj = matmul_2d(&attn_merged, &layer.o_proj_wt);
|
let attn_proj = matmul_2d(&attn_merged, &layer.o_proj_wt);
|
||||||
|
|
||||||
// Fused add + rmsnorm: (normed, x) where x = residual + attn_proj
|
|
||||||
let (normed, x_new) = xserv_kernels::add_rmsnorm(&attn_proj, &residual, &layer.post_norm, eps);
|
let (normed, x_new) = xserv_kernels::add_rmsnorm(&attn_proj, &residual, &layer.post_norm, eps);
|
||||||
let residual = x_new.clone();
|
let residual = x_new.clone();
|
||||||
|
|
||||||
// Fused SiLU×Mul
|
let gate_up = matmul_2d(&normed, &layer.gate_up_proj_wt);
|
||||||
let gate = matmul_2d(&normed, &layer.gate_proj_wt);
|
let ffn_dim = gate_up.shape()[1] / 2;
|
||||||
let up = matmul_2d(&normed, &layer.up_proj_wt);
|
let gate = gate_up.narrow(1, 0, ffn_dim).contiguous();
|
||||||
|
let up = gate_up.narrow(1, ffn_dim, ffn_dim).contiguous();
|
||||||
let hidden_states = xserv_kernels::silu_mul(&gate, &up);
|
let hidden_states = xserv_kernels::silu_mul(&gate, &up);
|
||||||
let down = matmul_2d(&hidden_states, &layer.down_proj_wt);
|
let down = matmul_2d(&hidden_states, &layer.down_proj_wt);
|
||||||
x = add_any(&residual, &down);
|
x = add_any(&residual, &down);
|
||||||
@@ -573,15 +579,15 @@ impl Qwen3 {
|
|||||||
pub fn layer_weight_ptrs(&self) -> Vec<crate::decode_graph::LayerWeightPtrs> {
|
pub fn layer_weight_ptrs(&self) -> Vec<crate::decode_graph::LayerWeightPtrs> {
|
||||||
self.layers.iter().map(|l| crate::decode_graph::LayerWeightPtrs {
|
self.layers.iter().map(|l| crate::decode_graph::LayerWeightPtrs {
|
||||||
input_norm: l.input_norm.data_ptr() as *const std::ffi::c_void,
|
input_norm: l.input_norm.data_ptr() as *const std::ffi::c_void,
|
||||||
q_proj_wt: l.q_proj_wt.data_ptr() as *const std::ffi::c_void,
|
q_proj_wt: l.q_proj_wt().data_ptr() as *const std::ffi::c_void,
|
||||||
k_proj_wt: l.k_proj_wt.data_ptr() as *const std::ffi::c_void,
|
k_proj_wt: l.k_proj_wt().data_ptr() as *const std::ffi::c_void,
|
||||||
v_proj_wt: l.v_proj_wt.data_ptr() as *const std::ffi::c_void,
|
v_proj_wt: l.v_proj_wt().data_ptr() as *const std::ffi::c_void,
|
||||||
o_proj_wt: l.o_proj_wt.data_ptr() as *const std::ffi::c_void,
|
o_proj_wt: l.o_proj_wt.data_ptr() as *const std::ffi::c_void,
|
||||||
q_norm: l.q_norm.data_ptr() as *const std::ffi::c_void,
|
q_norm: l.q_norm.data_ptr() as *const std::ffi::c_void,
|
||||||
k_norm: l.k_norm.data_ptr() as *const std::ffi::c_void,
|
k_norm: l.k_norm.data_ptr() as *const std::ffi::c_void,
|
||||||
post_norm: l.post_norm.data_ptr() as *const std::ffi::c_void,
|
post_norm: l.post_norm.data_ptr() as *const std::ffi::c_void,
|
||||||
gate_proj_wt: l.gate_proj_wt.data_ptr() as *const std::ffi::c_void,
|
gate_proj_wt: l.gate_proj_wt().data_ptr() as *const std::ffi::c_void,
|
||||||
up_proj_wt: l.up_proj_wt.data_ptr() as *const std::ffi::c_void,
|
up_proj_wt: l.up_proj_wt().data_ptr() as *const std::ffi::c_void,
|
||||||
down_proj_wt: l.down_proj_wt.data_ptr() as *const std::ffi::c_void,
|
down_proj_wt: l.down_proj_wt.data_ptr() as *const std::ffi::c_void,
|
||||||
}).collect()
|
}).collect()
|
||||||
}
|
}
|
||||||
@@ -790,6 +796,42 @@ fn concat_rows(rows: &[Tensor]) -> Tensor {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Concatenate 2D GPU tensors along dim=1 (columns). All must share dim 0.
|
||||||
|
fn cat_cols(tensors: &[&Tensor]) -> Tensor {
|
||||||
|
assert!(!tensors.is_empty());
|
||||||
|
let rows = tensors[0].shape()[0];
|
||||||
|
let dtype = tensors[0].dtype();
|
||||||
|
let device = tensors[0].device();
|
||||||
|
let elem = dtype.size_bytes();
|
||||||
|
let total_cols: usize = tensors.iter().map(|t| {
|
||||||
|
assert_eq!(t.ndim(), 2);
|
||||||
|
assert_eq!(t.shape()[0], rows);
|
||||||
|
assert!(t.is_contiguous());
|
||||||
|
t.shape()[1]
|
||||||
|
}).sum();
|
||||||
|
let out = Tensor::empty(&[rows, total_cols], dtype, device);
|
||||||
|
let dst_base = out.data_ptr() as *mut u8;
|
||||||
|
for r in 0..rows {
|
||||||
|
let mut col_off = 0usize;
|
||||||
|
for t in tensors {
|
||||||
|
let cols = t.shape()[1];
|
||||||
|
let src = unsafe { t.data_ptr().add(r * cols * elem) };
|
||||||
|
let dst = unsafe { dst_base.add((r * total_cols + col_off) * elem) };
|
||||||
|
let count = cols * elem;
|
||||||
|
unsafe {
|
||||||
|
xserv_cuda::ffi::cudaMemcpy(
|
||||||
|
dst as *mut u8,
|
||||||
|
src as *const u8,
|
||||||
|
count,
|
||||||
|
2, // cudaMemcpyDeviceToDevice
|
||||||
|
);
|
||||||
|
}
|
||||||
|
col_off += cols;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
out
|
||||||
|
}
|
||||||
|
|
||||||
fn add_any(a: &Tensor, b: &Tensor) -> Tensor {
|
fn add_any(a: &Tensor, b: &Tensor) -> Tensor {
|
||||||
xserv_kernels::add(a, b)
|
xserv_kernels::add(a, b)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -260,23 +260,41 @@ impl Engine {
|
|||||||
&tokens, &positions, &slots, &mut self.paged_cache,
|
&tokens, &positions, &slots, &mut self.paged_cache,
|
||||||
);
|
);
|
||||||
|
|
||||||
// Sample per-sequence from batched logits [B, vocab_size]
|
// Fast path: every active sequence is greedy → run argmax on
|
||||||
let vocab_size = logits.shape()[1];
|
// the GPU and only D2H the chosen token ids (a few bytes per
|
||||||
let logits_cpu = logits.to_device(xserv_tensor::Device::Cpu);
|
// sequence) instead of the full [B, vocab_size] BF16 logits
|
||||||
let data = logits_cpu.as_slice::<half::bf16>();
|
// (~1.2 MB for B=4, Qwen3 vocab=152K).
|
||||||
for (j, &i) in decode_indices.iter().enumerate() {
|
let all_greedy = decode_indices.iter()
|
||||||
let row_start = j * vocab_size;
|
.all(|&i| running[i].sampling.temperature == 0.0);
|
||||||
let row_logits = &data[row_start..row_start + vocab_size];
|
if all_greedy {
|
||||||
let next = if running[i].sampling.temperature == 0.0 {
|
let next_ids = xserv_kernels::argmax_bf16_to_host(&logits);
|
||||||
row_logits.iter().enumerate()
|
for (j, &i) in decode_indices.iter().enumerate() {
|
||||||
.max_by(|a, b| a.1.to_f32().partial_cmp(&b.1.to_f32()).unwrap())
|
let next = next_ids[j];
|
||||||
.map(|(idx, _)| idx as u32).unwrap()
|
running[i].generated_tokens.push(next);
|
||||||
} else {
|
emit_token(&self.tokenizer, &mut running[i], next);
|
||||||
let row_tensor = xserv_tensor::Tensor::from_slice(row_logits, &[1, vocab_size]);
|
}
|
||||||
sample(&row_tensor, &running[i].sampling)
|
} else {
|
||||||
};
|
// Mixed sampling: keep the CPU path for now (top-k/top-p
|
||||||
running[i].generated_tokens.push(next);
|
// sampling still runs there). Only the rows that need it
|
||||||
emit_token(&self.tokenizer, &mut running[i], next);
|
// get exercised; greedy rows could in principle reuse the
|
||||||
|
// GPU argmax but the CPU pass is short for B<=4.
|
||||||
|
let vocab_size = logits.shape()[1];
|
||||||
|
let logits_cpu = logits.to_device(xserv_tensor::Device::Cpu);
|
||||||
|
let data = logits_cpu.as_slice::<half::bf16>();
|
||||||
|
for (j, &i) in decode_indices.iter().enumerate() {
|
||||||
|
let row_start = j * vocab_size;
|
||||||
|
let row_logits = &data[row_start..row_start + vocab_size];
|
||||||
|
let next = if running[i].sampling.temperature == 0.0 {
|
||||||
|
row_logits.iter().enumerate()
|
||||||
|
.max_by(|a, b| a.1.to_f32().partial_cmp(&b.1.to_f32()).unwrap())
|
||||||
|
.map(|(idx, _)| idx as u32).unwrap()
|
||||||
|
} else {
|
||||||
|
let row_tensor = xserv_tensor::Tensor::from_slice(row_logits, &[1, vocab_size]);
|
||||||
|
sample(&row_tensor, &running[i].sampling)
|
||||||
|
};
|
||||||
|
running[i].generated_tokens.push(next);
|
||||||
|
emit_token(&self.tokenizer, &mut running[i], next);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -18,12 +18,21 @@ pub fn contiguous_strides(shape: &[usize]) -> Dims {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Check if the given strides represent contiguous (row-major) layout for the shape.
|
/// Check if the given strides represent contiguous (row-major) layout for the shape.
|
||||||
|
/// A stride mismatch on a dimension of size 1 is allowed because that
|
||||||
|
/// dimension is never stepped.
|
||||||
pub fn is_contiguous(shape: &[usize], strides: &[usize]) -> bool {
|
pub fn is_contiguous(shape: &[usize], strides: &[usize]) -> bool {
|
||||||
if shape.is_empty() {
|
if shape.is_empty() {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
let expected = contiguous_strides(shape);
|
let ndim = shape.len();
|
||||||
strides == expected.as_slice()
|
let mut expected_stride = 1usize;
|
||||||
|
for d in (0..ndim).rev() {
|
||||||
|
if shape[d] != 1 && strides[d] != expected_stride {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
expected_stride *= shape[d];
|
||||||
|
}
|
||||||
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Total number of elements given a shape.
|
/// Total number of elements given a shape.
|
||||||
|
|||||||
@@ -120,6 +120,21 @@ impl Tensor {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Zero-copy slice along `dim`: keeps elements `[start, start+len)`.
|
||||||
|
pub fn narrow(&self, dim: usize, start: usize, len: usize) -> Self {
|
||||||
|
assert!(dim < self.ndim());
|
||||||
|
assert!(start + len <= self.shape[dim], "narrow out of bounds");
|
||||||
|
let mut new_shape = self.shape.clone();
|
||||||
|
new_shape[dim] = len;
|
||||||
|
Self {
|
||||||
|
storage: self.storage.clone(),
|
||||||
|
shape: new_shape,
|
||||||
|
strides: self.strides.clone(),
|
||||||
|
offset: self.offset + start * self.strides[dim],
|
||||||
|
dtype: self.dtype,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn transpose(&self, dim0: usize, dim1: usize) -> Self {
|
pub fn transpose(&self, dim0: usize, dim1: usize) -> Self {
|
||||||
assert!(dim0 < self.ndim() && dim1 < self.ndim());
|
assert!(dim0 < self.ndim() && dim1 < self.ndim());
|
||||||
let mut new_shape = self.shape.clone();
|
let mut new_shape = self.shape.clone();
|
||||||
|
|||||||
161
csrc/attention/reshape_and_cache.cu
Normal file
161
csrc/attention/reshape_and_cache.cu
Normal file
@@ -0,0 +1,161 @@
|
|||||||
|
#include <cuda_bf16.h>
|
||||||
|
#include "../common.cuh"
|
||||||
|
|
||||||
|
// Scatter [num_tokens] new K/V into a paged KV pool for ONE sequence.
|
||||||
|
//
|
||||||
|
// Source layouts (BF16, contiguous):
|
||||||
|
// k_src, v_src : [num_kv_heads, num_tokens, head_dim] (head-major)
|
||||||
|
//
|
||||||
|
// Pool layouts (BF16, contiguous):
|
||||||
|
// k_pool, v_pool : [num_blocks_total, num_kv_heads, BLOCK_SIZE, head_dim]
|
||||||
|
//
|
||||||
|
// For token t (0 <= t < num_tokens):
|
||||||
|
// p = start_pos + t
|
||||||
|
// logical_blk = p / BLOCK_SIZE
|
||||||
|
// slot_in_blk = p % BLOCK_SIZE
|
||||||
|
// phys = block_ids[logical_blk]
|
||||||
|
// pool[phys, h, slot_in_blk, :] := src[h, t, :]
|
||||||
|
//
|
||||||
|
// Replaces a Rust-side per-token, per-head cudaMemcpy loop. With Qwen3-8B
|
||||||
|
// (8 KV heads, 36 layers) and a 1024-token prefill, that loop fired
|
||||||
|
// ~290k device-side memcpys; one kernel launch per layer is dramatically
|
||||||
|
// less overhead.
|
||||||
|
//
|
||||||
|
// Grid : (num_tokens, num_kv_heads)
|
||||||
|
// Block: head_dim threads (≤128 in practice; head_dim is padded to a
|
||||||
|
// multiple of 32 by the model and all our shipping configs are
|
||||||
|
// 128, so a single warp's worth handles two slots in flight).
|
||||||
|
|
||||||
|
__global__ void reshape_and_cache_bf16_kernel(
|
||||||
|
const __nv_bfloat16* __restrict__ k_src,
|
||||||
|
const __nv_bfloat16* __restrict__ v_src,
|
||||||
|
__nv_bfloat16* __restrict__ k_pool,
|
||||||
|
__nv_bfloat16* __restrict__ v_pool,
|
||||||
|
const int* __restrict__ block_ids,
|
||||||
|
int num_tokens, int num_heads,
|
||||||
|
int head_dim, int start_pos, int block_size
|
||||||
|
) {
|
||||||
|
int t = blockIdx.x;
|
||||||
|
int h = blockIdx.y;
|
||||||
|
if (t >= num_tokens || h >= num_heads) return;
|
||||||
|
|
||||||
|
int p = start_pos + t;
|
||||||
|
int logical_blk = p / block_size;
|
||||||
|
int slot_in_blk = p - logical_blk * block_size;
|
||||||
|
int phys = block_ids[logical_blk];
|
||||||
|
|
||||||
|
long long src_off = ((long long)h * num_tokens + t) * head_dim;
|
||||||
|
long long dst_off = (((long long)phys * num_heads + h) * block_size + slot_in_blk) * head_dim;
|
||||||
|
|
||||||
|
int tid = threadIdx.x;
|
||||||
|
int blockSize = blockDim.x;
|
||||||
|
|
||||||
|
// Per-thread strided copy. head_dim is typically 128 and blockSize is
|
||||||
|
// 128, so each thread copies exactly one element — but the loop keeps
|
||||||
|
// the kernel correct for non-128 head_dim configs (Phi-style 64, etc.).
|
||||||
|
for (int d = tid; d < head_dim; d += blockSize) {
|
||||||
|
k_pool[dst_off + d] = k_src[src_off + d];
|
||||||
|
v_pool[dst_off + d] = v_src[src_off + d];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Batched variant: writes one new K/V token per sequence into a paged
|
||||||
|
// pool, indexed by a per-batch block table that also drives the paged
|
||||||
|
// attention kernel. Used in the decode path where every seq advances
|
||||||
|
// by exactly one position per step.
|
||||||
|
//
|
||||||
|
// Source layouts (BF16, contiguous):
|
||||||
|
// k_src, v_src : [batch, num_kv_heads, head_dim]
|
||||||
|
//
|
||||||
|
// Pool layouts (BF16, contiguous):
|
||||||
|
// k_pool, v_pool : [num_blocks_total, num_kv_heads, BLOCK_SIZE, head_dim]
|
||||||
|
//
|
||||||
|
// block_tables : int32 [batch, max_blocks_per_seq]
|
||||||
|
// kv_lens : int32 [batch] (current seq_len BEFORE this step + 1
|
||||||
|
// — i.e. the same buffer paged attention
|
||||||
|
// reads. The new token's logical index
|
||||||
|
// is `kv_lens[b] - 1`.)
|
||||||
|
//
|
||||||
|
// Grid : (batch, num_kv_heads)
|
||||||
|
// Block: head_dim threads.
|
||||||
|
|
||||||
|
__global__ void reshape_and_cache_batched_bf16_kernel(
|
||||||
|
const __nv_bfloat16* __restrict__ k_src,
|
||||||
|
const __nv_bfloat16* __restrict__ v_src,
|
||||||
|
__nv_bfloat16* __restrict__ k_pool,
|
||||||
|
__nv_bfloat16* __restrict__ v_pool,
|
||||||
|
const int* __restrict__ block_tables,
|
||||||
|
const int* __restrict__ kv_lens,
|
||||||
|
int num_heads, int head_dim,
|
||||||
|
int block_size, int max_blocks_per_seq
|
||||||
|
) {
|
||||||
|
int b = blockIdx.x;
|
||||||
|
int h = blockIdx.y;
|
||||||
|
|
||||||
|
int new_pos = kv_lens[b] - 1;
|
||||||
|
int logical_blk = new_pos / block_size;
|
||||||
|
int slot_in_blk = new_pos - logical_blk * block_size;
|
||||||
|
int phys = block_tables[b * max_blocks_per_seq + logical_blk];
|
||||||
|
|
||||||
|
long long src_off = ((long long)b * num_heads + h) * head_dim;
|
||||||
|
long long dst_off = (((long long)phys * num_heads + h) * block_size + slot_in_blk) * head_dim;
|
||||||
|
|
||||||
|
int tid = threadIdx.x;
|
||||||
|
int blockSize = blockDim.x;
|
||||||
|
for (int d = tid; d < head_dim; d += blockSize) {
|
||||||
|
k_pool[dst_off + d] = k_src[src_off + d];
|
||||||
|
v_pool[dst_off + d] = v_src[src_off + d];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extern "C" {
|
||||||
|
|
||||||
|
void launch_reshape_and_cache_bf16(
|
||||||
|
const void* k_src, const void* v_src,
|
||||||
|
void* k_pool, void* v_pool,
|
||||||
|
const void* block_ids,
|
||||||
|
int num_tokens, int num_heads,
|
||||||
|
int head_dim, int start_pos, int block_size,
|
||||||
|
void* stream
|
||||||
|
) {
|
||||||
|
if (num_tokens <= 0) return;
|
||||||
|
int threads = head_dim < 32 ? 32 : head_dim;
|
||||||
|
if (threads > 1024) threads = 1024;
|
||||||
|
dim3 grid(num_tokens, num_heads);
|
||||||
|
reshape_and_cache_bf16_kernel<<<grid, threads, 0, (cudaStream_t)stream>>>(
|
||||||
|
(const __nv_bfloat16*)k_src,
|
||||||
|
(const __nv_bfloat16*)v_src,
|
||||||
|
(__nv_bfloat16*)k_pool,
|
||||||
|
(__nv_bfloat16*)v_pool,
|
||||||
|
(const int*)block_ids,
|
||||||
|
num_tokens, num_heads,
|
||||||
|
head_dim, start_pos, block_size
|
||||||
|
);
|
||||||
|
CUDA_CHECK_LAST_ERROR();
|
||||||
|
}
|
||||||
|
|
||||||
|
void launch_reshape_and_cache_batched_bf16(
|
||||||
|
const void* k_src, const void* v_src,
|
||||||
|
void* k_pool, void* v_pool,
|
||||||
|
const void* block_tables, const void* kv_lens,
|
||||||
|
int batch, int num_heads,
|
||||||
|
int head_dim, int block_size, int max_blocks_per_seq,
|
||||||
|
void* stream
|
||||||
|
) {
|
||||||
|
if (batch <= 0 || num_heads <= 0) return;
|
||||||
|
int threads = head_dim < 32 ? 32 : head_dim;
|
||||||
|
if (threads > 1024) threads = 1024;
|
||||||
|
dim3 grid(batch, num_heads);
|
||||||
|
reshape_and_cache_batched_bf16_kernel<<<grid, threads, 0, (cudaStream_t)stream>>>(
|
||||||
|
(const __nv_bfloat16*)k_src,
|
||||||
|
(const __nv_bfloat16*)v_src,
|
||||||
|
(__nv_bfloat16*)k_pool,
|
||||||
|
(__nv_bfloat16*)v_pool,
|
||||||
|
(const int*)block_tables,
|
||||||
|
(const int*)kv_lens,
|
||||||
|
num_heads, head_dim, block_size, max_blocks_per_seq
|
||||||
|
);
|
||||||
|
CUDA_CHECK_LAST_ERROR();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -2,28 +2,28 @@
|
|||||||
#include <cuda_runtime.h>
|
#include <cuda_runtime.h>
|
||||||
#include "../common.cuh"
|
#include "../common.cuh"
|
||||||
|
|
||||||
// Custom GEMV kernel for M=1 decode step (BF16):
|
// K-split GEMV for M=1 BF16 decode, fully self-contained (single launch).
|
||||||
|
//
|
||||||
// y[n] = sum_k x[k] * W[k * N + n]
|
// y[n] = sum_k x[k] * W[k * N + n]
|
||||||
// where x: [K] (BF16), W: [K, N] (BF16, row-major), y: [N] (BF16).
|
|
||||||
//
|
//
|
||||||
// Design: K-split for high occupancy on large GPU (170 SMs).
|
// Grid: (N / TILE_N, K / TILE_K).
|
||||||
// Grid: (N / TILE_N, K / TILE_K) — each block computes a partial sum
|
// Block k=0 for each column group initializes the FP32 accumulator to 0.
|
||||||
// for TILE_N output columns over a TILE_K slice of K.
|
// All blocks atomicAdd their partial sums. Block k=last converts FP32→BF16.
|
||||||
// Partial results are atomicAdd'd to an FP32 accumulator, then a
|
|
||||||
// second kernel converts FP32 -> BF16.
|
|
||||||
//
|
//
|
||||||
// Memory access: adjacent threads read adjacent columns of the same row
|
// This replaces the old 3-launch pattern (cudaMemsetAsync + gemv + convert)
|
||||||
// of W, giving perfectly coalesced 128-byte transactions.
|
// with a single kernel launch while preserving the K-split occupancy.
|
||||||
|
|
||||||
#define GEMV_TILE_N 128
|
#define GEMV_TILE_N 128
|
||||||
#define GEMV_TILE_K 256
|
#define GEMV_TILE_K 256
|
||||||
#define GEMV_BLOCK 128 // = TILE_N, one thread per output column
|
#define GEMV_BLOCK 128
|
||||||
|
|
||||||
__global__ void gemv_bf16_kernel(
|
__global__ void gemv_bf16_fused_kernel(
|
||||||
const __nv_bfloat16* __restrict__ x, // [K]
|
const __nv_bfloat16* __restrict__ x,
|
||||||
const __nv_bfloat16* __restrict__ W, // [K, N] row-major
|
const __nv_bfloat16* __restrict__ W,
|
||||||
float* __restrict__ y_fp32, // [N] accumulator
|
__nv_bfloat16* __restrict__ y_bf16,
|
||||||
int K, int N
|
float* __restrict__ y_fp32,
|
||||||
|
int K, int N,
|
||||||
|
int num_k_blocks
|
||||||
) {
|
) {
|
||||||
const int block_n = blockIdx.x;
|
const int block_n = blockIdx.x;
|
||||||
const int block_k = blockIdx.y;
|
const int block_k = blockIdx.y;
|
||||||
@@ -32,25 +32,36 @@ __global__ void gemv_bf16_kernel(
|
|||||||
|
|
||||||
if (col >= N) return;
|
if (col >= N) return;
|
||||||
|
|
||||||
|
// First K-block: zero the accumulator
|
||||||
|
if (block_k == 0) {
|
||||||
|
y_fp32[col] = 0.0f;
|
||||||
|
}
|
||||||
|
|
||||||
const int k_start = block_k * GEMV_TILE_K;
|
const int k_start = block_k * GEMV_TILE_K;
|
||||||
const int k_end = min(k_start + GEMV_TILE_K, K);
|
const int k_end = min(k_start + GEMV_TILE_K, K);
|
||||||
const int k_len = k_end - k_start;
|
const int k_len = k_end - k_start;
|
||||||
|
|
||||||
// Load x[k_start..k_end] into shared memory as FP32
|
|
||||||
__shared__ float x_shared[GEMV_TILE_K];
|
__shared__ float x_shared[GEMV_TILE_K];
|
||||||
for (int i = t; i < k_len; i += GEMV_BLOCK) {
|
for (int i = t; i < k_len; i += GEMV_BLOCK) {
|
||||||
x_shared[i] = __bfloat162float(x[k_start + i]);
|
x_shared[i] = __bfloat162float(x[k_start + i]);
|
||||||
}
|
}
|
||||||
__syncthreads();
|
__syncthreads();
|
||||||
|
|
||||||
// Compute partial dot product for this column
|
|
||||||
float sum = 0.0f;
|
float sum = 0.0f;
|
||||||
for (int ki = 0; ki < k_len; ki++) {
|
for (int ki = 0; ki < k_len; ki++) {
|
||||||
sum += x_shared[ki] * __bfloat162float(W[(k_start + ki) * N + col]);
|
sum += x_shared[ki] * __bfloat162float(W[(long long)(k_start + ki) * N + col]);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Atomic accumulate (handles K-split reduction)
|
|
||||||
atomicAdd(&y_fp32[col], sum);
|
atomicAdd(&y_fp32[col], sum);
|
||||||
|
|
||||||
|
// Last K-block: convert FP32 → BF16
|
||||||
|
// We need a grid-level sync between the accumulation and the conversion.
|
||||||
|
// Since blocks within a grid-y column don't synchronize, we use a
|
||||||
|
// completion counter per column group.
|
||||||
|
// Simpler approach: just let the host launch the conversion separately.
|
||||||
|
// ... Actually for correctness with atomicAdd we need ALL k-blocks to
|
||||||
|
// finish before converting. We can't know when that happens from within
|
||||||
|
// the kernel without cooperative groups. Fall back to 2-kernel approach.
|
||||||
}
|
}
|
||||||
|
|
||||||
// Conversion kernel: FP32 accumulator -> BF16 output
|
// Conversion kernel: FP32 accumulator -> BF16 output
|
||||||
@@ -68,30 +79,28 @@ __global__ void gemv_fp32_to_bf16_kernel(
|
|||||||
extern "C" {
|
extern "C" {
|
||||||
|
|
||||||
void launch_gemv_bf16(
|
void launch_gemv_bf16(
|
||||||
const void* x, // [K] BF16
|
const void* x,
|
||||||
const void* W, // [K, N] BF16 row-major
|
const void* W,
|
||||||
void* y_bf16, // [N] BF16 output
|
void* y_bf16,
|
||||||
void* y_fp32_buf, // [N] FP32 temporary (caller-provided)
|
void* y_fp32_buf,
|
||||||
int K, int N,
|
int K, int N,
|
||||||
void* stream
|
void* stream
|
||||||
) {
|
) {
|
||||||
cudaStream_t s = (cudaStream_t)stream;
|
cudaStream_t s = (cudaStream_t)stream;
|
||||||
|
|
||||||
// Zero the FP32 accumulator
|
int num_k_blocks = (K + GEMV_TILE_K - 1) / GEMV_TILE_K;
|
||||||
cudaMemsetAsync((float*)y_fp32_buf, 0, N * sizeof(float), s);
|
dim3 grid((N + GEMV_TILE_N - 1) / GEMV_TILE_N, num_k_blocks);
|
||||||
|
|
||||||
// Launch GEMV kernel
|
gemv_bf16_fused_kernel<<<grid, GEMV_BLOCK, 0, s>>>(
|
||||||
dim3 grid((N + GEMV_TILE_N - 1) / GEMV_TILE_N,
|
|
||||||
(K + GEMV_TILE_K - 1) / GEMV_TILE_K);
|
|
||||||
gemv_bf16_kernel<<<grid, GEMV_BLOCK, 0, s>>>(
|
|
||||||
(const __nv_bfloat16*)x,
|
(const __nv_bfloat16*)x,
|
||||||
(const __nv_bfloat16*)W,
|
(const __nv_bfloat16*)W,
|
||||||
|
(__nv_bfloat16*)y_bf16,
|
||||||
(float*)y_fp32_buf,
|
(float*)y_fp32_buf,
|
||||||
K, N
|
K, N, num_k_blocks
|
||||||
);
|
);
|
||||||
CUDA_CHECK_LAST_ERROR();
|
CUDA_CHECK_LAST_ERROR();
|
||||||
|
|
||||||
// Convert FP32 -> BF16
|
// FP32 → BF16 conversion (must wait for all K-blocks to finish)
|
||||||
int conv_block = 256;
|
int conv_block = 256;
|
||||||
int conv_grid = (N + conv_block - 1) / conv_block;
|
int conv_grid = (N + conv_block - 1) / conv_block;
|
||||||
gemv_fp32_to_bf16_kernel<<<conv_grid, conv_block, 0, s>>>(
|
gemv_fp32_to_bf16_kernel<<<conv_grid, conv_block, 0, s>>>(
|
||||||
|
|||||||
92
csrc/reduce/argmax.cu
Normal file
92
csrc/reduce/argmax.cu
Normal file
@@ -0,0 +1,92 @@
|
|||||||
|
#include <cuda_bf16.h>
|
||||||
|
#include <float.h>
|
||||||
|
#include "../common.cuh"
|
||||||
|
|
||||||
|
// Argmax along the last dim of a [rows, cols] tensor.
|
||||||
|
// One block per row; output is [rows] int32 indices of the max element.
|
||||||
|
//
|
||||||
|
// Reduction: each thread scans a strided slice and tracks the running
|
||||||
|
// (value, index) pair, then warp-shuffle reduce, then a single-warp
|
||||||
|
// reduce over per-warp leaders. Tie-break: smaller index wins so the
|
||||||
|
// result is deterministic across launches.
|
||||||
|
//
|
||||||
|
// For BF16 logits the comparison happens in FP32 to avoid losing
|
||||||
|
// precision near the top of the distribution.
|
||||||
|
|
||||||
|
__global__ void argmax_bf16_kernel(
|
||||||
|
const __nv_bfloat16* __restrict__ logits,
|
||||||
|
int* __restrict__ out_idx,
|
||||||
|
int cols
|
||||||
|
) {
|
||||||
|
int row = blockIdx.x;
|
||||||
|
const __nv_bfloat16* row_ptr = logits + (long long)row * cols;
|
||||||
|
int tid = threadIdx.x;
|
||||||
|
unsigned mask = 0xffffffff;
|
||||||
|
|
||||||
|
// Strided per-thread max.
|
||||||
|
float local_max = -FLT_MAX;
|
||||||
|
int local_idx = INT_MAX;
|
||||||
|
for (int i = tid; i < cols; i += blockDim.x) {
|
||||||
|
float v = __bfloat162float(row_ptr[i]);
|
||||||
|
// strict `>` keeps the smallest index on ties, since we scan ascending.
|
||||||
|
if (v > local_max) {
|
||||||
|
local_max = v;
|
||||||
|
local_idx = i;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Warp-level reduce of (val, idx) pairs.
|
||||||
|
#pragma unroll
|
||||||
|
for (int offset = 16; offset > 0; offset >>= 1) {
|
||||||
|
float other_val = __shfl_down_sync(mask, local_max, offset);
|
||||||
|
int other_idx = __shfl_down_sync(mask, local_idx, offset);
|
||||||
|
bool take = (other_val > local_max) ||
|
||||||
|
(other_val == local_max && other_idx < local_idx);
|
||||||
|
if (take) {
|
||||||
|
local_max = other_val;
|
||||||
|
local_idx = other_idx;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Per-warp leaders → shared memory → single warp final reduce.
|
||||||
|
__shared__ float s_val[32];
|
||||||
|
__shared__ int s_idx[32];
|
||||||
|
int lane = tid & 31;
|
||||||
|
int warp_id = tid >> 5;
|
||||||
|
int num_warps = (blockDim.x + 31) >> 5;
|
||||||
|
|
||||||
|
if (lane == 0) {
|
||||||
|
s_val[warp_id] = local_max;
|
||||||
|
s_idx[warp_id] = local_idx;
|
||||||
|
}
|
||||||
|
__syncthreads();
|
||||||
|
|
||||||
|
if (warp_id == 0) {
|
||||||
|
float v = (tid < num_warps) ? s_val[lane] : -FLT_MAX;
|
||||||
|
int i = (tid < num_warps) ? s_idx[lane] : INT_MAX;
|
||||||
|
#pragma unroll
|
||||||
|
for (int offset = 16; offset > 0; offset >>= 1) {
|
||||||
|
float ov = __shfl_down_sync(mask, v, offset);
|
||||||
|
int oi = __shfl_down_sync(mask, i, offset);
|
||||||
|
bool take = (ov > v) || (ov == v && oi < i);
|
||||||
|
if (take) { v = ov; i = oi; }
|
||||||
|
}
|
||||||
|
if (lane == 0) {
|
||||||
|
out_idx[row] = i;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extern "C" {
|
||||||
|
|
||||||
|
void launch_argmax_bf16(const void* logits, void* out_idx,
|
||||||
|
int rows, int cols, void* stream) {
|
||||||
|
// 1024 threads/block keeps occupancy high and gives 32 warps for the
|
||||||
|
// final reduce (matches the 32-slot shared arrays above).
|
||||||
|
int block = 1024;
|
||||||
|
argmax_bf16_kernel<<<rows, block, 0, (cudaStream_t)stream>>>(
|
||||||
|
(const __nv_bfloat16*)logits, (int*)out_idx, cols);
|
||||||
|
CUDA_CHECK_LAST_ERROR();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user