1373 lines
74 KiB
C
1373 lines
74 KiB
C
/* SPDX-License-Identifier: AGPL-3.0-or-later */
|
|
/* Copyright (C) 2026 David (Corey) Herrera */
|
|
/*
|
|
* fc_cuda.h — CUDA GPU compute backend for Foundry
|
|
*
|
|
* Module prefix: fc_ (foundry cuda)
|
|
* Build flag: USE_CUDA=1
|
|
*
|
|
* Provides GPU-accelerated tensor operations via NVIDIA CUDA.
|
|
* Uses dlopen() for zero compile-time CUDA dependency — falls back
|
|
* to CPU if CUDA is unavailable at runtime.
|
|
*
|
|
* Key difference from Metal: CUDA targets discrete GPUs with separate
|
|
* VRAM, so buffers require explicit Host-to-Device / Device-to-Host
|
|
* copies. Weights should be uploaded once and kept resident.
|
|
*
|
|
* Target: CUDA 11.4 / sm_37 (K80) — no FP16 math, no tensor cores,
|
|
* uses __shfl_down() not __shfl_down_sync().
|
|
*/
|
|
|
|
#ifndef FOUNDRY_CUDA_H
|
|
#define FOUNDRY_CUDA_H
|
|
|
|
#include <stdbool.h>
|
|
#include <stddef.h>
|
|
#include <stdint.h>
|
|
|
|
#ifdef __cplusplus
|
|
extern "C" {
|
|
#endif
|
|
|
|
/* ================================================================
|
|
* Opaque types
|
|
* ================================================================ */
|
|
|
|
typedef struct fc_buffer fc_buffer;
|
|
|
|
/* Per-device CUDA context. Holds one device's context, stream,
|
|
* cuBLAS handle, PTX modules and kernel function handles. One
|
|
* instance per K80 die lets a single process drive multiple dies
|
|
* concurrently. The dlopen'd driver function table is process-wide
|
|
* and shared by all contexts. Opaque — see fc_cuda.c for fields. */
|
|
typedef struct fc_cuda_ctx fc_cuda_ctx;
|
|
|
|
/* ================================================================
|
|
* Process-wide driver loading
|
|
*
|
|
* The CUDA driver / cuBLAS shared libraries are dlopen'd once per
|
|
* process; their function pointers are shared by all contexts.
|
|
* fc_cuda_init_device() loads them lazily on first call, so most
|
|
* callers never need fc_driver_load() directly.
|
|
* ================================================================ */
|
|
|
|
/* dlopen libcuda + libcublas and resolve all driver symbols, then
|
|
* cuInit(0). Idempotent. Returns 0 on success, -1 on failure
|
|
* (e.g. no CUDA driver present — the Mac case). */
|
|
int fc_driver_load(void);
|
|
|
|
/* Returns true once fc_driver_load() has succeeded. */
|
|
bool fc_driver_loaded(void);
|
|
|
|
/* ================================================================
|
|
* Per-device context lifecycle (multi-die)
|
|
*
|
|
* Each context owns one device's CUcontext, stream, cuBLAS handle,
|
|
* and a private copy of the PTX modules/kernels. Operations taking
|
|
* an fc_cuda_ctx* make that context current (push) for the call and
|
|
* restore the previous context (pop) on return, so contexts for
|
|
* different dies never bleed into each other.
|
|
* ================================================================ */
|
|
|
|
/* Initialize CUDA for a specific device id: dlopen driver (if not
|
|
* already), cuDeviceGet, cuCtxCreate, stream + cuBLAS create, and
|
|
* per-context cuModuleLoadData of the embedded PTX.
|
|
*
|
|
* @param device_id CUDA ordinal of the die to bind (0-based)
|
|
* @return Owned context handle, or NULL on failure.
|
|
* Free with fc_cuda_ctx_free().
|
|
*
|
|
* Thread-safe: no (driver load + cuCtxCreate are not serialized here)
|
|
* Reentrant: no */
|
|
fc_cuda_ctx* fc_cuda_init_device(int device_id);
|
|
|
|
/* Destroy a per-device context: sync, unload modules, destroy
|
|
* cuBLAS handle, stream and context. Safe to call with NULL. */
|
|
void fc_cuda_ctx_free(fc_cuda_ctx *ctx);
|
|
|
|
/* Returns true if the context is initialized and ready. */
|
|
bool fc_cuda_ctx_available(const fc_cuda_ctx *ctx);
|
|
|
|
/* Returns the CUDA device ordinal this context is bound to, or -1. */
|
|
int fc_cuda_ctx_device_id(const fc_cuda_ctx *ctx);
|
|
|
|
/* ================================================================
|
|
* MemoryAware VRAM signals (MemoryAware.md §2.2)
|
|
* ================================================================ */
|
|
|
|
/* Query this die's free/total VRAM via cuMemGetInfo, with ctx made current for
|
|
* the call. Authoritative at the moment of the call but racy under concurrent
|
|
* alloc, so it is a decision-point cross-check, not a per-token input. Either
|
|
* output pointer may be NULL. Returns 0 on success, -1 if unavailable.
|
|
*
|
|
* Thread-safe: no (makes ctx current; serialize with other ctx ops)
|
|
* Reentrant: no */
|
|
int fc_mem_info_ctx(fc_cuda_ctx *ctx, size_t *free_bytes, size_t *total_bytes);
|
|
|
|
/* Same reading, but for a die this process does NOT own: creates a bare
|
|
* context (no cuBLAS handle, no PTX modules), reads cuMemGetInfo, destroys it.
|
|
* The die is left byte-identical. Use this for enumeration/probing; use
|
|
* fc_mem_info_ctx for a die you already hold a context on.
|
|
*
|
|
* Probing with fc_cuda_init_device() instead is what stranded a full ~124 MiB
|
|
* context on every die a process merely looked at — including dies belonging
|
|
* to another serve.
|
|
*
|
|
* Thread-safe: no (creates/destroys a context on the calling thread)
|
|
* Reentrant: no */
|
|
int fc_cuda_probe_device(int device_id, size_t *free_bytes, size_t *total_bytes);
|
|
|
|
/* Running sum of bytes this context has allocated and not yet freed, tallied at
|
|
* the buffer choke points. Free + predictive — the number admission schedules
|
|
* against (MemoryAware.md §6). Returns 0 for a NULL ctx.
|
|
*
|
|
* Thread-safe: no (plain read of a counter mutated on the alloc path)
|
|
* Reentrant: yes */
|
|
size_t fc_ctx_alloc_bytes(const fc_cuda_ctx *ctx);
|
|
|
|
/* ================================================================
|
|
* Crash-time teardown (MemoryAware.md §8)
|
|
* ================================================================ */
|
|
|
|
/* Number of ready contexts currently registered (live dies). */
|
|
int fc_cuda_live_ctx_count(void);
|
|
|
|
/* Drain + cuCtxDestroy every live context, then the caller MUST exit so
|
|
* libcuda closes the /dev/nvidiaN fd — the only step that reclaims Kepler VRAM
|
|
* (cuCtxDestroy alone does not). Intended for the SIGINT/SIGTERM shutdown path.
|
|
*
|
|
* Thread-safe: no
|
|
* Reentrant: no
|
|
* Signal-safe: NO — calls into libcuda. Use only from a controlled-shutdown
|
|
* signal (SIGINT/SIGTERM), never from SIGSEGV/SIGABRT (there, _exit() and
|
|
* lean on kernel fd-close + startup stranded-VRAM detection). */
|
|
void fc_cuda_emergency_teardown(void);
|
|
|
|
/* ================================================================
|
|
* Lifecycle (process-default context — back-compat single-die path)
|
|
*
|
|
* These keep every existing single-context call site working. The
|
|
* first fc_init_on() creates a process-default fc_cuda_ctx bound to
|
|
* the device it names; all the non-ctx functions below (fc_buffer_*,
|
|
* fc_sgemm, kernels, fc_sync) delegate to it. Multi-die callers should
|
|
* use the fc_cuda_ctx* variants instead.
|
|
* ================================================================ */
|
|
|
|
/* Initialize CUDA on `device_id` into the process-default context:
|
|
* dlopen libcuda + libcublas, cuInit, cuCtxCreate, load embedded PTX
|
|
* kernels. Returns 0 on success, -1 on failure. Idempotent — the FIRST
|
|
* call wins, so a caller that knows its placement must call this before
|
|
* anything else forces a default context onto a die it does not own. */
|
|
int fc_init_on(int device_id);
|
|
|
|
/* fc_init_on(0). Only correct when the process genuinely owns device 0
|
|
* (or CUDA_VISIBLE_DEVICES has pinned its die to ordinal 0). Callers that
|
|
* have a placement MUST use fc_init_on() — this is how a run pinned to
|
|
* two other dies ended up holding a context on die 0. */
|
|
int fc_init(void);
|
|
|
|
/* True once libcuda is loadable and cuInit has succeeded. Unlike
|
|
* fc_available() this creates NO context, so it is the right gate for
|
|
* "is there a GPU at all" questions asked before placement is known. */
|
|
bool fc_driver_available(void);
|
|
|
|
/* Cleanup the process-default context and unload driver libraries. */
|
|
void fc_free(void);
|
|
|
|
/* Returns true if the process-default context is initialized. */
|
|
bool fc_available(void);
|
|
|
|
/* Returns the process-default context, or NULL if fc_init() has not
|
|
* succeeded. Lets a caller bridge from the back-compat path to the
|
|
* explicit-ctx API without re-initializing. */
|
|
fc_cuda_ctx* fc_default_ctx(void);
|
|
|
|
/* ================================================================
|
|
* Buffers (discrete GPU memory — explicit H2D/D2H copies)
|
|
*
|
|
* Unlike Metal's unified memory, CUDA buffers live in GPU VRAM.
|
|
* fc_buffer_from_ptr() allocates GPU memory and copies data from
|
|
* CPU. Use fc_buffer_to_cpu() to read results back.
|
|
*
|
|
* For persistent data (model weights), create the buffer once at
|
|
* load time and reuse it across inference steps.
|
|
* ================================================================ */
|
|
|
|
/* --- Per-context variants (multi-die) ---
|
|
* Each allocation is tagged with its owning context; frees and copies
|
|
* automatically make that context current. The buffer must only be
|
|
* used with the context that created it. */
|
|
|
|
/* Allocate a GPU buffer on ctx's device and copy CPU data into it. */
|
|
fc_buffer* fc_buffer_from_ptr_ctx(fc_cuda_ctx *ctx, const void *cpu, size_t bytes);
|
|
|
|
/* Allocate an uninitialized GPU buffer on ctx's device. */
|
|
fc_buffer* fc_buffer_alloc_ctx(fc_cuda_ctx *ctx, size_t bytes);
|
|
|
|
/* --- Process-default-context variants (back-compat) --- */
|
|
|
|
/* Allocate GPU buffer and copy CPU data to it (Host-to-Device). */
|
|
fc_buffer* fc_buffer_from_ptr(const void *cpu, size_t bytes);
|
|
|
|
/* Allocate GPU buffer without initializing (for output buffers). */
|
|
fc_buffer* fc_buffer_alloc(size_t bytes);
|
|
|
|
/* Free a GPU buffer (uses the buffer's owning context). */
|
|
void fc_buffer_free(fc_buffer *buf);
|
|
|
|
/* Non-owning offset view: returns a heap fc_buffer whose device pointer is
|
|
* base's + byte_off (size shrunk by byte_off), sharing base's owning context.
|
|
* It does NOT own the device memory — free ONLY with fc_buffer_free_view
|
|
* (which frees the wrapper struct, never the underlying allocation). Used by
|
|
* the die-interleave (L5) to launch a slot kernel over a sub-range
|
|
* [slot_base, slot_base+count) of a persistent per-slot ring while the batch
|
|
* scratch stays group-local [0,count): the ring buffer is offset by
|
|
* slot_base*row_bytes so the kernel's slot index 0 lands at slot_base.
|
|
* Returns NULL on bad args (byte_off past the end). */
|
|
fc_buffer* fc_buffer_offset_view(fc_buffer *base, size_t byte_off);
|
|
|
|
/* Free a view created by fc_buffer_offset_view: frees the wrapper struct only,
|
|
* NOT the aliased device memory. A no-op on NULL. */
|
|
void fc_buffer_free_view(fc_buffer *view);
|
|
|
|
/* Copy GPU buffer contents back to CPU (Device-to-Host). */
|
|
void fc_buffer_to_cpu(fc_buffer *buf, void *dst);
|
|
|
|
/* Copy CPU data into an existing GPU buffer (Host-to-Device). */
|
|
void fc_buffer_from_cpu(fc_buffer *buf, const void *src);
|
|
|
|
/* Get buffer size in bytes. */
|
|
size_t fc_buffer_size(fc_buffer *buf);
|
|
|
|
/* Partial copies (first `bytes` of the buffer; clamped to its size). The
|
|
* batch path sizes persistent buffers for the max batch but moves only the
|
|
* live n tokens' bytes per call. */
|
|
void fc_buffer_to_cpu_n(fc_buffer *buf, void *dst, size_t bytes);
|
|
void fc_buffer_from_cpu_n(fc_buffer *buf, const void *src, size_t bytes);
|
|
|
|
/* Async device-to-host copy queued on the buffer's owning ctx stream (non-
|
|
* blocking). Stream-ordered after prior kernel work; the caller MUST
|
|
* fc_sync_ctx(owner) before reading `dst`. Returns 0 on success, -1 if async
|
|
* DtoH is unavailable (caller should fall back to the blocking variant). */
|
|
int fc_buffer_to_cpu_n_async(fc_buffer *buf, void *dst, size_t bytes);
|
|
|
|
/* Host-to-device copy into a byte offset of the buffer (bounds-checked
|
|
* no-op on overflow). Used to re-sync a row range of the GPU KV cache
|
|
* from the host mirror. */
|
|
void fc_buffer_from_cpu_at(fc_buffer *buf, size_t dst_off, const void *src,
|
|
size_t bytes);
|
|
|
|
/* Device-to-host copy from a byte offset of the buffer (bounds-checked
|
|
* no-op on overflow). Used to refresh a row range of the host KV mirror
|
|
* from the GPU cache byte-for-byte. */
|
|
void fc_buffer_to_cpu_at(fc_buffer *buf, void *dst, size_t src_off,
|
|
size_t bytes);
|
|
|
|
/* Async device-to-device range copy on ctx's stream (both buffers must
|
|
* belong to ctx; bounds-checked no-op otherwise). */
|
|
void fc_buffer_copy_d2d_ctx(fc_cuda_ctx *ctx, fc_buffer *dst, size_t dst_off,
|
|
fc_buffer *src, size_t src_off, size_t bytes);
|
|
|
|
/* ================================================================
|
|
* Peer (die-to-die) copies — for multi-die tensor/vocab split on cards
|
|
* whose dies are separate CUDA devices (K80: two GK210 dies behind an
|
|
* on-card PLX switch, cudaDeviceCanAccessPeer==1, PIX single-bridge path).
|
|
* ================================================================ */
|
|
|
|
/* Enable P2P access between two contexts' devices in BOTH directions.
|
|
* Idempotent: a second call for an already-enabled pair returns 0. Returns
|
|
* 0 on success (or if the pair is already enabled), -1 if either context is
|
|
* unavailable, the driver lacks the peer symbols, or the devices cannot
|
|
* access each other (cuDeviceCanAccessPeer==0). Call once at model load. */
|
|
int fc_cuda_enable_peer(fc_cuda_ctx *a, fc_cuda_ctx *b);
|
|
|
|
/* Blocking device-to-device peer copy: copy `bytes` from src (owned by
|
|
* src's context/device) into dst (owned by dst's context/device), across
|
|
* the peer link (cuMemcpyPeer). Both buffers may live on different devices.
|
|
* Synchronous (uses cuMemcpyPeer, which blocks until complete). Bounds-
|
|
* checked no-op if bytes exceeds either buffer. Requires fc_cuda_enable_peer
|
|
* to have succeeded for the pair. Returns 0 on success, -1 on error. */
|
|
int fc_buffer_copy_peer(fc_buffer *dst, fc_buffer *src, size_t bytes);
|
|
|
|
/* Async (non-blocking) device-to-device peer copy queued on stream_ctx's
|
|
* stream — stream-ordered after prior work on that stream, does NOT block the
|
|
* host. stream_ctx must be one of the two buffers' owners (normally dst's, so
|
|
* the receiving die both issues the copy and can event-sync before reading
|
|
* dst). Caller must fc_sync_ctx(stream_ctx) or wait on an event recorded after
|
|
* this before reading dst. Requires fc_cuda_enable_peer for the pair. Returns 0
|
|
* on success, -1 if cuMemcpyPeerAsync is unavailable (fall back to the blocking
|
|
* fc_buffer_copy_peer). */
|
|
int fc_buffer_copy_peer_async(fc_buffer *dst, fc_buffer *src, size_t bytes,
|
|
fc_cuda_ctx *stream_ctx);
|
|
|
|
/* ================================================================
|
|
* Dispatch thresholds
|
|
*
|
|
* On discrete GPU, even single-token decode can benefit from GPU
|
|
* since GPU memory bandwidth (240 GB/s on K80) exceeds CPU
|
|
* (~20 GB/s on server). However, PCIe transfer overhead means
|
|
* we need sufficient work to amortize the copy cost.
|
|
*
|
|
* These thresholds assume weights are already resident in VRAM
|
|
* (only activations transfer per token).
|
|
* ================================================================ */
|
|
|
|
#define FC_GEMM_THRESHOLD (64L * 1024 * 1024) /* ~64M ops */
|
|
#define FC_GEMV_THRESHOLD (4096L * 4096) /* ~16M ops */
|
|
#define FC_Q8_THRESHOLD (0) /* always use GPU for Q8 matvec */
|
|
#define FC_ELEM_THRESHOLD (1024 * 1024) /* 1M elements */
|
|
|
|
/* ================================================================
|
|
* BLAS via cuBLAS (dlopen'd)
|
|
*
|
|
* All matrices are row-major float32. cuBLAS is column-major,
|
|
* so we swap A/B and transpose the problem internally.
|
|
* ================================================================ */
|
|
|
|
/* C = alpha * A @ B + beta * C
|
|
* A: [M, K], B: [K, N], C: [M, N] — all row-major float32. */
|
|
void fc_sgemm_ctx(fc_cuda_ctx *ctx, fc_buffer *A, fc_buffer *B, fc_buffer *C,
|
|
int M, int N, int K, float alpha, float beta);
|
|
void fc_sgemm(fc_buffer *A, fc_buffer *B, fc_buffer *C,
|
|
int M, int N, int K, float alpha, float beta);
|
|
|
|
/* y = alpha * A @ x + beta * y
|
|
* A: [M, K] row-major, x: [K], y: [M] — all float32. */
|
|
void fc_sgemv_ctx(fc_cuda_ctx *ctx, fc_buffer *A, fc_buffer *x, fc_buffer *y,
|
|
int M, int K, float alpha, float beta);
|
|
void fc_sgemv(fc_buffer *A, fc_buffer *x, fc_buffer *y,
|
|
int M, int K, float alpha, float beta);
|
|
|
|
/* ================================================================
|
|
* Custom kernels (loaded from embedded PTX)
|
|
* ================================================================ */
|
|
|
|
/* RMS normalization over last dimension.
|
|
* x: [n], weight: [n] (may be NULL), out: [n] */
|
|
void fc_rms_norm_ctx(fc_cuda_ctx *ctx, fc_buffer *x, fc_buffer *weight,
|
|
fc_buffer *out, int n, float eps);
|
|
void fc_rms_norm(fc_buffer *x, fc_buffer *weight, fc_buffer *out,
|
|
int n, float eps);
|
|
|
|
/* Softmax over last dimension.
|
|
* x: [rows, cols], out: [rows, cols] */
|
|
void fc_softmax_ctx(fc_cuda_ctx *ctx, fc_buffer *x, fc_buffer *out,
|
|
int rows, int cols);
|
|
void fc_softmax(fc_buffer *x, fc_buffer *out, int rows, int cols);
|
|
|
|
/* SiLU activation: out = x * sigmoid(x) */
|
|
void fc_silu_ctx(fc_cuda_ctx *ctx, fc_buffer *x, fc_buffer *out, int n);
|
|
void fc_silu(fc_buffer *x, fc_buffer *out, int n);
|
|
|
|
/* Elementwise addition: out = a + b */
|
|
void fc_add_ctx(fc_cuda_ctx *ctx, fc_buffer *a, fc_buffer *b, fc_buffer *out, int n);
|
|
void fc_add(fc_buffer *a, fc_buffer *b, fc_buffer *out, int n);
|
|
|
|
/* Elementwise multiplication: out = a * b */
|
|
void fc_mul_ctx(fc_cuda_ctx *ctx, fc_buffer *a, fc_buffer *b, fc_buffer *out, int n);
|
|
void fc_mul(fc_buffer *a, fc_buffer *b, fc_buffer *out, int n);
|
|
|
|
/* INT8 quantized matrix-vector product.
|
|
* x_q: [cols] int8, w: [rows, cols] int8, w_scales: [rows] float32,
|
|
* out: [rows] float32.
|
|
* Computes: out[i] = x_scale * w_scales[i] * dot(x_q, w[i]) */
|
|
void fc_matvec_q8_ctx(fc_cuda_ctx *ctx, fc_buffer *x_q, fc_buffer *w,
|
|
fc_buffer *w_scales, fc_buffer *out,
|
|
float x_scale, int rows, int cols);
|
|
void fc_matvec_q8(fc_buffer *x_q, fc_buffer *w, fc_buffer *w_scales,
|
|
fc_buffer *out, float x_scale, int rows, int cols);
|
|
|
|
/* ================================================================
|
|
* qwen35moe kernels (loaded from embedded PTX)
|
|
*
|
|
* The CUDA halves of the qwen35moe forward pass. Each mirrors a CPU
|
|
* reference (cited in its cuda_kernels .cu source) bit-for-bit and is
|
|
* validated against the golden vectors in Pantheon/tests/golden/.
|
|
* ================================================================ */
|
|
|
|
/* Q3_K dequant + matrix-vector product.
|
|
* w: raw Q3_K weight bytes (row-major, row_stride bytes/row),
|
|
* x: [cols] float32 activation, out: [rows] float32.
|
|
* cols must be a multiple of 256; row_stride = (cols/256) * 110.
|
|
* Computes: out[r] = dequant_q3k(w[r]) · x. */
|
|
void fc_matvec_q3k_ctx(fc_cuda_ctx *ctx, fc_buffer *w, fc_buffer *x,
|
|
fc_buffer *out, int rows, int cols, int row_stride);
|
|
void fc_matvec_q3k(fc_buffer *w, fc_buffer *x, fc_buffer *out,
|
|
int rows, int cols, int row_stride);
|
|
|
|
/* ---- Q3_K peer-stream variants (scheme 9a) ----
|
|
* BIT-EXACT alternatives to fc_matvec_q3k that differ only in how many weight
|
|
* loads a warp keeps in flight. They exist for the case where the die running
|
|
* the kernel reads ANOTHER die's VRAM over the K80's on-card peer link, which
|
|
* is LATENCY-bound (9.3-12.4 GB/s available, 2.7-4.2 GB/s achieved) — see
|
|
* plans/results/phase9a_peer_streaming.md. On a die's OWN memory the kernel is
|
|
* issue-bound and the stock shape is the right one.
|
|
* r2 = row pair per warp (the stock shape), r1 = one row per warp (2x warps)
|
|
* pN = N super-blocks' weight loads issued before the first is consumed
|
|
* All variants also stage x with 128-bit loads. */
|
|
enum {
|
|
FC_Q3K_PS_R2P1 = 0, FC_Q3K_PS_R2P2, FC_Q3K_PS_R2P4, FC_Q3K_PS_R2P8,
|
|
FC_Q3K_PS_R1P1, FC_Q3K_PS_R1P4, FC_Q3K_PS_R1P8,
|
|
FC_Q3K_PS_COUNT
|
|
};
|
|
|
|
/* Human-readable name of a variant, or NULL if `variant` is out of range. */
|
|
const char *fc_matvec_q3k_ps_name(int variant);
|
|
|
|
/* Launch peer-stream variant `variant`. Same contract and same output bytes as
|
|
* fc_matvec_q3k_ctx. Returns 0 if launched, -1 if the variant is out of range
|
|
* or absent from this build's PTX — the caller must handle -1 rather than let
|
|
* a missing kernel silently launch nothing. */
|
|
int fc_matvec_q3k_ps_ctx(fc_cuda_ctx *ctx, fc_buffer *w, fc_buffer *x,
|
|
fc_buffer *out, int rows, int cols, int row_stride,
|
|
int variant);
|
|
|
|
/* Mixed-quant dequant + matvec family (matvec_kquants.cu). Same contract as
|
|
* fc_matvec_q3k_ctx for each GGUF quant type the Q3_K_M mini mixes in:
|
|
* Q4_K (144 B / 256 elems), Q5_K (176 B / 256), Q6_K (210 B / 256),
|
|
* Q8_0 (34 B / 32), and plain F32 rows (row_stride = cols * 4). */
|
|
void fc_matvec_q4k_ctx(fc_cuda_ctx *ctx, fc_buffer *w, fc_buffer *x,
|
|
fc_buffer *out, int rows, int cols, int row_stride);
|
|
void fc_matvec_q5k_ctx(fc_cuda_ctx *ctx, fc_buffer *w, fc_buffer *x,
|
|
fc_buffer *out, int rows, int cols, int row_stride);
|
|
void fc_matvec_q6k_ctx(fc_cuda_ctx *ctx, fc_buffer *w, fc_buffer *x,
|
|
fc_buffer *out, int rows, int cols, int row_stride);
|
|
void fc_matvec_q80_ctx(fc_cuda_ctx *ctx, fc_buffer *w, fc_buffer *x,
|
|
fc_buffer *out, int rows, int cols, int row_stride);
|
|
/* Bonsai native ternary (GGUF type 42, Q2_0_g128): conditional add/sub, no
|
|
* dequant. Repack via fc_repack_weight(42, ...). Warp-per-row like q80. */
|
|
void fc_matvec_q2_0_ctx(fc_cuda_ctx *ctx, fc_buffer *w, fc_buffer *x,
|
|
fc_buffer *out, int rows, int cols, int row_stride);
|
|
void fc_matvec_f32_ctx(fc_cuda_ctx *ctx, fc_buffer *w, fc_buffer *x,
|
|
fc_buffer *out, int rows, int cols, int row_stride);
|
|
/* F16 rows (GGUF type 1): the dense half of an F16 model — attention q/k/v/o,
|
|
* shared expert, router, LM head. 2-byte load widened in-register (sm_37 has no
|
|
* fp16 ARITHMETIC; cvt.f32.f16 is a storage conversion and predates it), fp32
|
|
* accumulate. row_stride is bytes (cols * 2) and the weight is NOT repacked —
|
|
* the GGUF bytes are already the device layout, which is what keeps a 70 GB
|
|
* upload from needing a second host copy of every tensor.
|
|
* These are OPTIONAL PTX symbols: probe fc_matvec_f16_available_ctx before
|
|
* routing a model's dense weights here. With the kernel absent the launcher
|
|
* does NOTHING, so a caller that skipped the probe would write no output at
|
|
* all instead of falling back. */
|
|
int fc_matvec_f16_available_ctx(fc_cuda_ctx *ctx);
|
|
void fc_matvec_f16_ctx(fc_cuda_ctx *ctx, fc_buffer *w, fc_buffer *x,
|
|
fc_buffer *out, int rows, int cols, int row_stride);
|
|
/* BF16 rows (GGUF type 30): the same dense tensors as the F16 pair above,
|
|
* carrying the values the model actually SHIPS — the checkpoints are bf16
|
|
* safetensors, and converting bf16 -> F16 re-rounds every weight (7 stored
|
|
* mantissa bits against 10, and neither field contains the other), so an F16
|
|
* run measures a perturbed model. That fidelity is the only reason to prefer
|
|
* it: the widen is a shift into the high half rather than a cvt, but ptxas
|
|
* emits ONE instruction either way and the two decode at the same speed
|
|
* (measured; see fc_bf16_to_f32 in moe_expert_group.cu). Identical
|
|
* contract to the F16 launcher otherwise: row_stride in bytes (cols * 2), NOT
|
|
* repacked, OPTIONAL PTX symbols, silent when absent — probe
|
|
* fc_matvec_bf16_available_ctx before routing weights here. */
|
|
int fc_matvec_bf16_available_ctx(fc_cuda_ctx *ctx);
|
|
void fc_matvec_bf16_ctx(fc_cuda_ctx *ctx, fc_buffer *w, fc_buffer *x,
|
|
fc_buffer *out, int rows, int cols, int row_stride);
|
|
|
|
/* ---- Batched (multi-column) matvecs: the prefill/batch GEMM path ----
|
|
* out[t][rows] = dequant(w) . x[t] for the n activation columns of a batch:
|
|
* x is [n][cols] row-major (token-major), out is [n][rows]. Each weight
|
|
* block is decoded ONCE per 4-column tile and reused across the columns, so
|
|
* the dequant instruction stream (the single-token kernels' issue bound on
|
|
* sm_37) amortizes over the batch. BIT-EXACT per column: column t of the
|
|
* batched output is bit-identical to the corresponding single-column
|
|
* fc_matvec_* call on x[t] (same per-lane chain order + reduction; verified
|
|
* by cuda/tests/test_matvec_nx). Same layout/stride contracts as the
|
|
* single-column variants. */
|
|
void fc_matvec_q3k_nx_ctx(fc_cuda_ctx *ctx, fc_buffer *w, fc_buffer *x,
|
|
fc_buffer *out, int rows, int cols, int row_stride,
|
|
int n);
|
|
void fc_matvec_q4k_nx_ctx(fc_cuda_ctx *ctx, fc_buffer *w, fc_buffer *x,
|
|
fc_buffer *out, int rows, int cols, int row_stride,
|
|
int n);
|
|
void fc_matvec_q5k_nx_ctx(fc_cuda_ctx *ctx, fc_buffer *w, fc_buffer *x,
|
|
fc_buffer *out, int rows, int cols, int row_stride,
|
|
int n);
|
|
void fc_matvec_q6k_nx_ctx(fc_cuda_ctx *ctx, fc_buffer *w, fc_buffer *x,
|
|
fc_buffer *out, int rows, int cols, int row_stride,
|
|
int n);
|
|
void fc_matvec_q80_nx_ctx(fc_cuda_ctx *ctx, fc_buffer *w, fc_buffer *x,
|
|
fc_buffer *out, int rows, int cols, int row_stride,
|
|
int n);
|
|
void fc_matvec_f32_nx_ctx(fc_cuda_ctx *ctx, fc_buffer *w, fc_buffer *x,
|
|
fc_buffer *out, int rows, int cols, int row_stride,
|
|
int n);
|
|
void fc_matvec_f16_nx_ctx(fc_cuda_ctx *ctx, fc_buffer *w, fc_buffer *x,
|
|
fc_buffer *out, int rows, int cols, int row_stride,
|
|
int n);
|
|
void fc_matvec_bf16_nx_ctx(fc_cuda_ctx *ctx, fc_buffer *w, fc_buffer *x,
|
|
fc_buffer *out, int rows, int cols, int row_stride,
|
|
int n);
|
|
|
|
/* ================================================================
|
|
* Quant-block repack (host-side, done once at weight upload)
|
|
*
|
|
* The raw GGUF block layouts are unaligned (110/144/176/210/34 B)
|
|
* and interleave packed scales with the quant bits, which caps the
|
|
* warp-per-row matvec kernels at 10-40 GB/s (per-lane byte loads +
|
|
* per-element scale unpack). fc_repack_weight converts a weight's
|
|
* blocks ONCE into the 4-byte-aligned device layout those kernels
|
|
* read: numerically identical values, scales pre-unpacked, block
|
|
* scale d (and dmin) pre-decoded to f32, and the quant bytes
|
|
* transposed so lane l of a warp issues ONE aligned 32-bit load per
|
|
* super-block. Per-super-block layouts (offsets in bytes):
|
|
*
|
|
* Q3_K 110->148: u32 qw[32] @0 qw[l] = qs[l] | qs[32+l]<<8 | hmask[l]<<16
|
|
* s8 sc[16] @128 (6-bit scales, unpacked, -32 applied)
|
|
* f32 d @144
|
|
* Q4_K 144->152: u32 qw[32] @0 qw[l] byte j = qs[32*j + l]
|
|
* f32 d @128, f32 dmin @132, u8 sc[8] @136, u8 mn[8] @144
|
|
* Q5_K 176->184: u32 qw[32] @0 (as Q4_K), u8 qh[32] @128,
|
|
* f32 d @160, f32 dmin @164, u8 sc[8] @168, u8 mn[8] @176
|
|
* Q6_K 210->212: u32 qlw[32] @0 qlw[l] byte t = ql[32*t + l]
|
|
* u16 qh2[32] @128 qh2[l] = qh[l] | qh[32+l]<<8
|
|
* s8 sc[16] @192, f32 d @208
|
|
* Q8_0 34-> 36: f32 d @0, s8 qs[32] @4 (per 32-elem block)
|
|
*
|
|
* fc_repack_row_bytes returns the repacked bytes-per-row for a GGUF
|
|
* quant type id (8=Q8_0, 11=Q3_K, 12=Q4_K, 13=Q5_K, 14=Q6_K), or 0
|
|
* if the type/cols combination has no repacked form (caller must not
|
|
* use the quant matvec kernels then).
|
|
* ================================================================ */
|
|
size_t fc_repack_row_bytes(int gguf_quant, int cols);
|
|
int fc_repack_weight(int gguf_quant, const void *src, void *dst,
|
|
long rows, int cols);
|
|
|
|
/* Partial NEOX rotary position embedding, in place.
|
|
* data: [n_heads * head_dim] float32. Only the first rope_dim dims of
|
|
* each head are rotated (NEOX pairing j vs j+rope_dim/2); dims
|
|
* [rope_dim, head_dim) pass through unchanged. */
|
|
void fc_rope_neox_partial_ctx(fc_cuda_ctx *ctx, fc_buffer *data,
|
|
int n_heads, int head_dim, int rope_dim,
|
|
int pos, float theta_base);
|
|
void fc_rope_neox_partial(fc_buffer *data, int n_heads, int head_dim,
|
|
int rope_dim, int pos, float theta_base);
|
|
|
|
/* Per-head QK-RMSNorm, in place.
|
|
* data: [n_heads * head_dim] float32; each head is RMS-normalized over
|
|
* its own head_dim and scaled by the shared weight [head_dim].
|
|
* weight may be NULL (no scaling). */
|
|
void fc_qk_rms_norm_ctx(fc_cuda_ctx *ctx, fc_buffer *data, fc_buffer *weight,
|
|
int n_heads, int head_dim, float eps);
|
|
void fc_qk_rms_norm(fc_buffer *data, fc_buffer *weight,
|
|
int n_heads, int head_dim, float eps);
|
|
|
|
/* MoE router: softmax over n_expert logits (in place), top-k select,
|
|
* norm_topk_prob renormalize, apply expert_weights_scale.
|
|
* logits: [n_expert] float32 (overwritten with softmax probs),
|
|
* top_idx: [k] int32 output, top_wt: [k] float32 output.
|
|
* k must be <= 32 (FC_MOE_MAX_TOPK). */
|
|
void fc_moe_router_topk_ctx(fc_cuda_ctx *ctx, fc_buffer *logits,
|
|
fc_buffer *top_idx, fc_buffer *top_wt,
|
|
int n_expert, int k, float expert_weights_scale);
|
|
void fc_moe_router_topk(fc_buffer *logits, fc_buffer *top_idx,
|
|
fc_buffer *top_wt, int n_expert, int k,
|
|
float expert_weights_scale);
|
|
|
|
/* Device-side exact top-k over LM-head logits (topk_logits.cu).
|
|
* logits: [n] float32 device buffer (left intact on return).
|
|
* out_idx/out_val: HOST arrays of k entries, filled in descending value
|
|
* order; equal values order by ascending index (the host argmax tie-break,
|
|
* so the k=1 result IS the host argmax). Values are bit-exact — selection
|
|
* is compare-only — and only k (idx, val) pairs cross the host link
|
|
* instead of the full n-float buffer. k must be <= FC_TOPK_MAX.
|
|
* Synchronous; returns 0 on success, -1 on failure (caller should fall
|
|
* back to a full-logits D2H copy). */
|
|
#define FC_TOPK_MAX 256
|
|
int fc_topk_logits_ctx(fc_cuda_ctx *ctx, fc_buffer *logits, int n, int k,
|
|
int *out_idx, float *out_val);
|
|
int fc_topk_logits(fc_buffer *logits, int n, int k,
|
|
int *out_idx, float *out_val);
|
|
|
|
/* Batched (multi-slot) top-k (L8): the same exact selection over every row
|
|
* of a [nslots][n] logits matrix IN PLACE (no per-row staging copy), one
|
|
* sync + one nslots*k-pair D2H for the whole batch. out_idx/out_val are
|
|
* HOST arrays of nslots*k entries (row u's pairs at [u*k .. u*k+k)); each
|
|
* row's pairs are bit-exact to fc_topk_logits_ctx on that row. Returns 0
|
|
* on success, -1 on failure / kernels absent (caller falls back to the
|
|
* per-slot path). */
|
|
int fc_topk_logits_slots_ctx(fc_cuda_ctx *ctx, fc_buffer *logits, int nslots,
|
|
int n, int k, int *out_idx, float *out_val);
|
|
|
|
/* MoE weighted accumulate: moe_out[i] += weight * expert_out[i].
|
|
* Both buffers are [n_embd] float32. */
|
|
void fc_moe_weighted_add_ctx(fc_cuda_ctx *ctx, fc_buffer *expert_out,
|
|
fc_buffer *moe_out, float weight, int n_embd);
|
|
void fc_moe_weighted_add(fc_buffer *expert_out, fc_buffer *moe_out,
|
|
float weight, int n_embd);
|
|
|
|
/* MoE shared-expert sigmoid gate: shexp_out[i] *= sigmoid(gate_logit).
|
|
* shexp_out: [n_embd] float32, gated in place. */
|
|
void fc_moe_shared_gate_ctx(fc_cuda_ctx *ctx, fc_buffer *shexp_out,
|
|
float gate_logit, int n_embd);
|
|
void fc_moe_shared_gate(fc_buffer *shexp_out, float gate_logit, int n_embd);
|
|
|
|
/* ---- Grouped top-k expert kernels (moe_expert_group.cu) ----
|
|
* The whole routed-expert SwiGLU block in two launches, reading the router
|
|
* selection (top_idx/top_wt) directly on device — no host read-back. */
|
|
|
|
/* Are the 16-bit expert kernels (F16, BF16) routed to their uint2-load form?
|
|
* Set by FC_MOE_VEC_LOADS=1 in the environment and read per call. An
|
|
* unsupported combination FAILS CLOSED (no launch) rather than quietly running
|
|
* the scalar kernel, so a bench cannot end up timing the wrong thing.
|
|
*
|
|
* The vec kernels move the SAME DRAM bytes in HALF the LSU transactions (a
|
|
* warp-wide 16-bit load fills 64 B of a 128-B transaction; a uint2 load fills
|
|
* it), and on die 7 that is worth 1.31x on gate+up (0.2418 -> 0.1849 ms, 139 ->
|
|
* 181 GB/s) and 1.32x on down (0.1304 -> 0.0986 ms, 129 -> 170 GB/s), the same
|
|
* for both formats. 181 GB/s is 76% of the die's 240 GB/s theoretical, against
|
|
* 58% for the scalar form.
|
|
*
|
|
* It is opt-in and not the default because it is not a pure win: to load four
|
|
* contiguous weights per lane the kernels must hand lane l the run {4l..4l+3}
|
|
* instead of the scalar map's stride-32 set {32i+l}. That repartitions the
|
|
* products across lanes, so the per-lane fmaf chains and the shfl tree
|
|
* associate differently and the output is NOT bit-identical to the scalar
|
|
* kernels'. Each set is bit-exact against its own CPU replica and neither is
|
|
* more accurate; but a run that has to reproduce an earlier run's exact logits
|
|
* must pin this setting, so flipping the default is a deliberate call, not a
|
|
* tuning detail. Exposed so a test can pick the matching replica from the same
|
|
* source of truth the dispatch uses. */
|
|
int fc_moe_vec_loads_enabled(void);
|
|
|
|
/* Grouped gate+up SwiGLU for the k selected experts.
|
|
* gate_all/up_all: merged [n_expert * ff * row_stride] repacked quant bytes,
|
|
* x: [cols] f32, top_idx: [k] int32 (device), hid: [k*ff] f32 out.
|
|
* hid[s*ff+row] = silu(gate[e_s,row] . x) * (up[e_s,row] . x).
|
|
*
|
|
* gguf_quant selects the decoder (11 = Q3_K, 12 = Q4_K, 1 = F16, 30 = BF16),
|
|
* exactly as it does
|
|
* for fc_moe_down_accum_ctx below. FAILS CLOSED: an unsupported quant, or a
|
|
* supported one whose kernel is missing from the loaded PTX, logs a named
|
|
* error and launches NOTHING (hid untouched) rather than falling through to
|
|
* another quant's decode. Returns 0 on launch, -1 otherwise.
|
|
*
|
|
* Probe fc_moe_gateup_quant_available_ctx BEFORE committing a model to the
|
|
* grouped path: the Q4_K kernels are OPTIONAL symbols, so an fc_cuda_ptx.h
|
|
* regenerated before they existed answers 0 for Q4_K and the caller can take
|
|
* the per-expert fallback instead of failing on every layer at run time. */
|
|
int fc_moe_gateup_quant_available_ctx(fc_cuda_ctx *ctx, int gguf_quant);
|
|
int fc_moe_gateup_ctx(fc_cuda_ctx *ctx, int gguf_quant, fc_buffer *gate_all,
|
|
fc_buffer *up_all, fc_buffer *x, fc_buffer *top_idx,
|
|
fc_buffer *hid, int k, int ff, int cols, int row_stride);
|
|
|
|
/* Q3_K-pinned shorthand for fc_moe_gateup_ctx (benches, older callers). */
|
|
void fc_moe_gateup_q3k_ctx(fc_cuda_ctx *ctx, fc_buffer *gate_all,
|
|
fc_buffer *up_all, fc_buffer *x, fc_buffer *top_idx,
|
|
fc_buffer *hid, int k, int ff, int cols,
|
|
int row_stride);
|
|
|
|
/* Grouped down-projection + weighted accumulate:
|
|
* out[row] = sum_s top_wt[s] * (down[e_s,row] . hid[s]).
|
|
* gguf_quant selects the decoder (11 = Q3_K, 12 = Q4_K, 14 = Q6_K, 1 = F16,
|
|
* 30 = BF16), and a
|
|
* merged ffn_down_exps really is mixed WITHIN one file — Q3_K/Q4_K by layer in
|
|
* a Q3_K_M, Q4_K/Q6_K by layer in a Q4_K_M. Unsupported quants log a named
|
|
* error and launch nothing. out is WRITTEN.
|
|
*
|
|
* Q6_K is an OPTIONAL PTX symbol (a header regenerated before it existed leaves
|
|
* it NULL); probe fc_moe_down_quant_available_ctx before committing a model to
|
|
* the grouped path. */
|
|
int fc_moe_down_quant_available_ctx(fc_cuda_ctx *ctx, int gguf_quant);
|
|
void fc_moe_down_accum_ctx(fc_cuda_ctx *ctx, int gguf_quant, fc_buffer *down_all,
|
|
fc_buffer *hid, fc_buffer *top_idx, fc_buffer *top_wt,
|
|
fc_buffer *out, int k, int rows, int ff,
|
|
int row_stride);
|
|
|
|
/* Warps cooperating per down-projection output row pair (the wpp launch arg
|
|
* of fc_moe_down_q3k/q4k; FC_MOE_DOWN_WPP env-overridable for tuning, block
|
|
* size via FC_MOE_DOWN_THREADS). The 512-col down dot is 2 super-blocks, so
|
|
* one warp per row-pair stalls on latency; wpp warps split the k slots and
|
|
* shared-mem-reduce. Must divide the block's warp count. wpp=2 with
|
|
* 128-thread blocks measured best on a K80 (the kernel is
|
|
* instruction-issue bound at ~4.3 IPC — see moe_expert_group.cu). Mirrored
|
|
* by the bench_matvec CPU replica. */
|
|
#define FC_MOE_DOWN_WPP_DEFAULT 2
|
|
#define FC_MOE_DOWN_THREADS_DEFAULT 128
|
|
|
|
/* MoE expert-union dedup request-tile depth. Must match FC_MOE_DEDUP_RT in
|
|
* cuda_kernels/moe_expert_group.cu: it sets both the per-block register
|
|
* footprint and grid.z = ceil(maxr / RT). An expert with R requests has its
|
|
* rows read ceil(R/RT) times, so RT >= the typical R is what makes the dedup
|
|
* pay. */
|
|
#define FC_MOE_DEDUP_RT 8
|
|
|
|
/* Fused SwiGLU elementwise: out[i] = silu(a[i]) * b[i] (in-place safe). */
|
|
void fc_moe_silu_mul_ctx(fc_cuda_ctx *ctx, fc_buffer *a, fc_buffer *b,
|
|
fc_buffer *out, int n);
|
|
|
|
/* Shared-expert scalar gate fully on-device:
|
|
* shexp[i] *= sigmoid(gate_w . x). gate_w, x: [n] f32. */
|
|
void fc_moe_shexp_dot_gate_ctx(fc_cuda_ctx *ctx, fc_buffer *gate_w,
|
|
fc_buffer *x, fc_buffer *shexp, int n);
|
|
|
|
/* ================================================================
|
|
* Fused per-layer launches (fused_layer.cu)
|
|
*
|
|
* Decode on the K80 is launch-bound (~80 us/launch), so fc_fused_multi
|
|
* batches up to FC_FUSED_MAX_SEG independent matvec "segments" that read
|
|
* the SAME x into ONE launch, each with an optional fused epilogue. Every
|
|
* segment's math is bit-identical to its stand-alone kernel (the kernel
|
|
* bodies are duplicated verbatim; see fused_layer.cu).
|
|
* ================================================================ */
|
|
|
|
#define FC_FUSED_MAX_SEG 8
|
|
|
|
/* One matvec segment. kind: 0 = quant warp-per-row (qtype 8/11/12/13/14,
|
|
* repacked layout), 1 = F32 block-per-row (cols >= 256), 2 = quant gate+up
|
|
* pair (w = gate, w2 = up; writes silu(g.x)*(u.x)). ep (epilogue applied to
|
|
* the finished row value): 0 none; 1 sigmoid; 2 decay gate out[r] =
|
|
* aux1[r]*softplus(v+aux0[r]); 3 residual out[r] = v, aux1[r] += v;
|
|
* 4 moe combine out[r] = aux0[r] + v*aux2[0], aux1[r] += out[r]. */
|
|
typedef struct {
|
|
fc_buffer *w; /* weight bytes (repacked quant, or raw F32 rows) */
|
|
fc_buffer *w2; /* kind 2 only: the up weight (same type/stride) */
|
|
fc_buffer *out; /* [rows] f32 */
|
|
fc_buffer *aux0; /* epilogue operand (see ep) or NULL */
|
|
fc_buffer *aux1;
|
|
fc_buffer *aux2;
|
|
int rows, cols, row_stride;
|
|
int kind, qtype, ep;
|
|
} fc_fused_seg;
|
|
|
|
/* Validate the segments and upload their device descriptor ONCE (all
|
|
* referenced buffers must be persistent). Returns the descriptor buffer
|
|
* (free with fc_buffer_free) and the launch shape, or NULL if any segment
|
|
* is unsupported (caller keeps its unfused path). */
|
|
fc_buffer *fc_fused_build_ctx(fc_cuda_ctx *ctx, const fc_fused_seg *segs,
|
|
int nseg, int *total_blocks, int *shared_bytes);
|
|
|
|
/* Launch the fused multi-matvec: every segment's dot(s) against x in one
|
|
* kernel. desc/total_blocks/shared_bytes come from fc_fused_build_ctx. */
|
|
void fc_fused_multi_ctx(fc_cuda_ctx *ctx, fc_buffer *desc, int nseg,
|
|
int total_blocks, int shared_bytes, fc_buffer *x);
|
|
|
|
/* Fused per-head QK-RMSNorm + partial NEOX RoPE on Q and K in one launch
|
|
* (replaces fc_qk_rms_norm x2 + fc_rope_neox_partial x2; per-head math
|
|
* identical). Returns 0 on launch, -1 if the kernel is unavailable (caller
|
|
* must fall back to the four stand-alone launches). */
|
|
int fc_attn_qknorm_rope_ctx(fc_cuda_ctx *ctx, fc_buffer *q, fc_buffer *k,
|
|
fc_buffer *q_norm_w, fc_buffer *k_norm_w,
|
|
int n_head, int n_head_kv, int head_dim, float eps,
|
|
int rope_dim, int pos, float theta_base);
|
|
|
|
/* ================================================================
|
|
* SSM Gated-Delta-Net kernels (loaded from embedded PTX)
|
|
*
|
|
* The GPU halves of the single-token Gated-Delta-Net SSM block. Each
|
|
* mirrors a static helper in Pantheon/src/models/qwen35moe_ssm.c
|
|
* (cited in ssm_gated_delta.cu) and is validated against the SSM
|
|
* golden vectors in Pantheon/tests/golden/.
|
|
* ================================================================ */
|
|
|
|
/* Per-token delta-rule recurrence (the centerpiece). For one token, per
|
|
* (head h, row j): decays the transposed state row, computes the rank-1
|
|
* delta update in place, and reads y back after the write.
|
|
* s: [n_v_heads * S * S] transposed state (s[h*S*S+j*S+i]==S_h[i][j]),
|
|
* updated in place; q,k,v: [n_v_heads * S] (q/k L2-normed + GQA-
|
|
* repeated on host, v straight); g: [n_v_heads] PRE-exp decay;
|
|
* beta: [n_v_heads] sigmoid'd; y: [n_v_heads * S] output.
|
|
* scale = 1/sqrt(state_size). Mirrors qwen35moe_compute_recur_heads. */
|
|
void fc_ssm_recurrence_ctx(fc_cuda_ctx *ctx, fc_buffer *s, fc_buffer *q,
|
|
fc_buffer *k, fc_buffer *v, fc_buffer *g,
|
|
fc_buffer *beta, fc_buffer *y,
|
|
int n_v_heads, int state_size, float scale);
|
|
void fc_ssm_recurrence(fc_buffer *s, fc_buffer *q, fc_buffer *k, fc_buffer *v,
|
|
fc_buffer *g, fc_buffer *beta, fc_buffer *y,
|
|
int n_v_heads, int state_size, float scale);
|
|
|
|
/* Causal depthwise conv1d (kernel size conv_kernel) + SiLU over the full
|
|
* conv_dim stream, priming from conv_state and rolling it forward.
|
|
* qkv: [conv_dim] raw input, weight: [conv_dim * conv_kernel] row-major,
|
|
* conv_state: [conv_dim * (conv_kernel-1)] history (updated in place),
|
|
* out: [conv_dim] SiLU'd output. Mirrors qwen35moe_conv1d_silu. */
|
|
void fc_ssm_conv1d_silu_ctx(fc_cuda_ctx *ctx, fc_buffer *qkv, fc_buffer *weight,
|
|
fc_buffer *conv_state, fc_buffer *out,
|
|
int conv_dim, int conv_kernel);
|
|
void fc_ssm_conv1d_silu(fc_buffer *qkv, fc_buffer *weight, fc_buffer *conv_state,
|
|
fc_buffer *out, int conv_dim, int conv_kernel);
|
|
|
|
/* Gated RMSNorm: gated[h*S+d] = RMSNorm(y_h)[d] * weight[d] * silu(z[h*S+d]),
|
|
* per V-head over state_size dims (norm weight shared across heads). Omits the
|
|
* trailing ssm_out projection (reuse fc_matvec_q3k). y,z,gated: [n_v_heads*S],
|
|
* weight: [state_size]. Mirrors qwen35moe_gated_norm. */
|
|
void fc_ssm_gated_norm_ctx(fc_cuda_ctx *ctx, fc_buffer *y, fc_buffer *weight,
|
|
fc_buffer *z, fc_buffer *gated,
|
|
int n_v_heads, int state_size, float eps);
|
|
void fc_ssm_gated_norm(fc_buffer *y, fc_buffer *weight, fc_buffer *z,
|
|
fc_buffer *gated, int n_v_heads, int state_size, float eps);
|
|
|
|
/* SSM "glue" kernels — replace host round-trips in the qwen35moe GPU forward. */
|
|
/* beta = sigmoid(beta), in place [n]. */
|
|
void fc_sigmoid_ctx(fc_cuda_ctx *ctx, fc_buffer *x, int n);
|
|
/* g[h] = ssm_a[h] * softplus(alpha[h] + ssm_dt[h]), per V-head [n]. */
|
|
void fc_ssm_decay_gate_ctx(fc_cuda_ctx *ctx, fc_buffer *alpha, fc_buffer *ssm_dt,
|
|
fc_buffer *ssm_a, fc_buffer *g, int n);
|
|
/* Per-K-head L2-norm of q,k over state_size dims + GQA interleaved-repeat to
|
|
* qg/kg [n_v_heads*S]; v (conv_out[2*key_dim..], val_dim) straight-copied to vg.
|
|
* conv_out layout: [q key_dim | k key_dim | v val_dim]. */
|
|
void fc_ssm_qknorm_gqa_ctx(fc_cuda_ctx *ctx, fc_buffer *conv_out, fc_buffer *qg,
|
|
fc_buffer *kg, fc_buffer *vg, int n_k_heads,
|
|
int kv_groups, int state_size, int key_dim,
|
|
int val_dim, float eps);
|
|
|
|
/* ================================================================
|
|
* Batched (prefill) kernels — n positions of ONE sequence per launch
|
|
*
|
|
* The launch-count killers for batched prefill: each covers all n
|
|
* positions of a batch in one launch, with per-position math BIT-EXACT
|
|
* to its single-position counterpart (the stateful SSM kernels scan the
|
|
* positions inside the kernel, carrying state exactly as n sequential
|
|
* launches would). Batched activations are token-major: a [n][dim]
|
|
* buffer holds position t's vector at offset t*dim.
|
|
* ================================================================ */
|
|
|
|
/* Batched RMS norm: x/out [rows][n], one block per row. */
|
|
void fc_rms_norm_nx_ctx(fc_cuda_ctx *ctx, fc_buffer *x, fc_buffer *weight,
|
|
fc_buffer *out, int n, float eps, int rows);
|
|
|
|
/* Batched partial NEOX RoPE: data [npos][n_heads*head_dim], row t rotated
|
|
* at position pos0 + t, in place. */
|
|
void fc_rope_neox_partial_nx_ctx(fc_cuda_ctx *ctx, fc_buffer *data,
|
|
int n_heads, int head_dim, int rope_dim,
|
|
int pos0, float theta_base, int npos);
|
|
|
|
/* Batched MoE router: logits [ntok][n_expert] (softmax'd in place),
|
|
* top_idx [ntok][k], top_wt [ntok][k]. */
|
|
void fc_moe_router_topk_nx_ctx(fc_cuda_ctx *ctx, fc_buffer *logits,
|
|
fc_buffer *top_idx, fc_buffer *top_wt,
|
|
int n_expert, int k,
|
|
float expert_weights_scale, int ntok);
|
|
|
|
/* Batched grouped gate+up SwiGLU: x [ntok][cols], top_idx [ntok][k],
|
|
* hid [ntok][k*ff]. gguf_quant and the fail-closed contract are exactly
|
|
* fc_moe_gateup_ctx's; per token the result is bit-exact with it. */
|
|
int fc_moe_gateup_nx_ctx(fc_cuda_ctx *ctx, int gguf_quant, fc_buffer *gate_all,
|
|
fc_buffer *up_all, fc_buffer *x, fc_buffer *top_idx,
|
|
fc_buffer *hid, int k, int ff, int cols,
|
|
int row_stride, int ntok);
|
|
|
|
/* Q3_K-pinned shorthand for fc_moe_gateup_nx_ctx. */
|
|
void fc_moe_gateup_q3k_nx_ctx(fc_cuda_ctx *ctx, fc_buffer *gate_all,
|
|
fc_buffer *up_all, fc_buffer *x,
|
|
fc_buffer *top_idx, fc_buffer *hid, int k,
|
|
int ff, int cols, int row_stride, int ntok);
|
|
|
|
/* Expert-union dedup for the batched gate+up. Q3_K GATE/UP ONLY — there is no
|
|
* Q4_K twin of these kernels (dedup is an opt-in, prefill-only speed lever, not
|
|
* a format the engine must support), so a caller with Q4_K experts must keep to
|
|
* fc_moe_gateup_nx_ctx. fc_moe_dedup_build_ctx
|
|
* turns top_idx [ntok*k] into per-expert request lists (ecount [n_expert],
|
|
* ereq [n_expert*maxr]); fc_moe_gateup_q3k_dedup_ctx then runs the grouped
|
|
* gate+up over the union, reading each unique expert's weights once and
|
|
* applying them to every routing token. hid is bit-exact with the _nx path,
|
|
* so the existing fc_moe_down_accum_nx_ctx consumes it unchanged. maxr must
|
|
* be >= the max per-expert request count (== ntok).
|
|
*
|
|
* Both dedup calls are no-ops when their kernels are missing (an
|
|
* fc_cuda_ptx.h predating them), which would leave hid UNWRITTEN rather than
|
|
* wrong-but-plausible. Callers must gate on fc_moe_dedup_available_ctx and
|
|
* fall back to fc_moe_gateup_q3k_nx_ctx.
|
|
*
|
|
* FC_MOE_DEDUP_SHX=1 (default off) swaps in fc_moe_gateup_q3k_dedup_shx,
|
|
* which stages x in shared like the _nx kernel and keeps the block-uniform
|
|
* request list in shared instead of 16 per-thread registers: 62 registers and
|
|
* 8 blocks/SMX against 95 and 5, and bit-exact with both other paths.
|
|
* Measured +20% on the kernel at ntok=128 / n_expert=256. */
|
|
int fc_moe_dedup_available_ctx(fc_cuda_ctx *ctx);
|
|
void fc_moe_dedup_build_ctx(fc_cuda_ctx *ctx, fc_buffer *top_idx,
|
|
fc_buffer *ecount, fc_buffer *ereq, int n_expert,
|
|
int k, int ntok, int maxr);
|
|
void fc_moe_gateup_q3k_dedup_ctx(fc_cuda_ctx *ctx, fc_buffer *gate_all,
|
|
fc_buffer *up_all, fc_buffer *x,
|
|
fc_buffer *ecount, fc_buffer *ereq,
|
|
fc_buffer *hid, int k, int ff, int cols,
|
|
int row_stride, int n_expert, int maxr);
|
|
|
|
/* ---- COMPACTED-GRID dedup gate+up (FC_MOE_DEDUP_COMPACT, default OFF) ----
|
|
*
|
|
* The uncompacted launch above is (ceil(ff/8), n_expert, ceil(maxr/RT)) =
|
|
* (64, 256, 16) = 262,144 blocks at the shipped prefill shape, of which only
|
|
* 6.3 % do any work; MEASURED, the empty ones cost 18.6 % of the kernel
|
|
* (results/moe_dedup_occupancy_probe.md §5.2). The compacted path folds
|
|
* (expert, request-tile) onto ONE grid axis over a device-built work list and
|
|
* launches grid.y at fc_moe_dedup_wmax's exact upper bound — 352 instead of
|
|
* 4096, i.e. 22,528 blocks — with NO device->host sync. Bit-exact: same work
|
|
* items, same kernel body, one plain store per output, so block order is not
|
|
* observable (the full argument is in moe_expert_group.cu).
|
|
*
|
|
* Sequence per MoE block, replacing the two calls above:
|
|
* fc_moe_dedup_build_ctx (UNCHANGED — still builds ecount/ereq)
|
|
* fc_moe_dedup_build_wl_ctx (compacts ecount into wlist)
|
|
* fc_moe_gateup_q3k_dedup_compact_ctx
|
|
*
|
|
* Both new kernels exist only in a
|
|
* cd cuda_kernels && make NVCCFLAGS_EXTRA=-DFC_MOE_DEDUP_COMPACT=1
|
|
* build of the PTX, so with the shipped header fc_moe_dedup_compact_enabled_ctx
|
|
* is 0 whatever the environment says and callers keep the uncompacted launch.
|
|
*/
|
|
|
|
/* Exact host-side upper bound on the number of (expert, request-tile) work
|
|
* items — the grid.y a compacted launch must cover, and the int2 element count
|
|
* the wlist buffer must hold. W = sum over routed experts of
|
|
* ceil(R_e / FC_MOE_DEDUP_RT), and each of these three bounds is individually
|
|
* exact, with T = ntok*k requests and U = min(n_expert, T) routed experts:
|
|
* 1. W <= (T + (RT-1)*U) / RT — ATTAINED: at T=1024, U=256 a router giving
|
|
* 160 experts R=1 and 96 experts R=9 produces 160 + 192 = 352 tiles, so
|
|
* this bound cannot be tightened without knowing the router.
|
|
* 2. W <= U * ceil(maxr/RT) — an expert's list is at most maxr long
|
|
* 3. W <= T — every work item holds at least one request
|
|
* At the shipped prefill shape (ntok=128, k=8, n_expert=256, maxr=128) that is
|
|
* min(352, 4096, 1024) = 352 against the uncompacted grid.y*grid.z of 4096.
|
|
* Returns 0 when the shape cannot be compacted (n_expert or maxr outside the
|
|
* work-item encoding / the builder's scan capacity), which callers must treat as
|
|
* "no compaction available". Pure function; no ctx, no device. */
|
|
int fc_moe_dedup_wmax(int n_expert, int k, int ntok, int maxr);
|
|
/* Both compaction kernels present in the loaded PTX? */
|
|
int fc_moe_dedup_compact_available_ctx(fc_cuda_ctx *ctx);
|
|
/* ...and FC_MOE_DEDUP_COMPACT=1 in the environment. Default OFF. */
|
|
int fc_moe_dedup_compact_enabled_ctx(fc_cuda_ctx *ctx);
|
|
void fc_moe_dedup_build_wl_ctx(fc_cuda_ctx *ctx, fc_buffer *ecount,
|
|
fc_buffer *wlist, int n_expert, int wmax);
|
|
void fc_moe_gateup_q3k_dedup_compact_ctx(fc_cuda_ctx *ctx, fc_buffer *gate_all,
|
|
fc_buffer *up_all, fc_buffer *x,
|
|
fc_buffer *ereq, fc_buffer *wlist,
|
|
fc_buffer *hid, int k, int ff,
|
|
int cols, int row_stride, int wmax,
|
|
int maxr);
|
|
|
|
/* Batched grouped down + weighted accumulate: hid [ntok][k*ff],
|
|
* top_idx/top_wt [ntok][k], out [ntok][rows] (WRITTEN). */
|
|
void fc_moe_down_accum_nx_ctx(fc_cuda_ctx *ctx, int gguf_quant,
|
|
fc_buffer *down_all, fc_buffer *hid,
|
|
fc_buffer *top_idx, fc_buffer *top_wt,
|
|
fc_buffer *out, int k, int rows, int ff,
|
|
int row_stride, int ntok);
|
|
|
|
/* Batched shared-expert scalar gate: x/shexp [ntok][n], per token
|
|
* shexp[t] *= sigmoid(gate_w . x[t]). */
|
|
void fc_moe_shexp_dot_gate_nx_ctx(fc_cuda_ctx *ctx, fc_buffer *gate_w,
|
|
fc_buffer *x, fc_buffer *shexp, int n,
|
|
int ntok);
|
|
|
|
/* SSM conv1d + SiLU SCAN over npos positions: qkv/out [npos][conv_dim],
|
|
* conv_state rolled forward through all npos (updated in place). Requires
|
|
* conv_kernel <= 4. snaps (optional, NULL = off): per-position conv_state
|
|
* snapshots [npos][conv_dim*(conv_kernel-1)] — snaps[t] = state after
|
|
* position t, for speculative-decode partial-accept rollback. */
|
|
void fc_ssm_conv1d_silu_seq_ctx(fc_cuda_ctx *ctx, fc_buffer *qkv,
|
|
fc_buffer *weight, fc_buffer *conv_state,
|
|
fc_buffer *out, int conv_dim, int conv_kernel,
|
|
int npos, fc_buffer *snaps);
|
|
|
|
/* SSM delta-rule recurrence SCAN over npos positions: q/k/v/y
|
|
* [npos][n_v_heads*S], g/beta [npos][n_v_heads], state s updated in place
|
|
* through all npos. snaps (optional, NULL = off): per-position state
|
|
* snapshots [npos][n_v_heads*S*S] — snaps[t] = state after position t, for
|
|
* speculative-decode partial-accept rollback. */
|
|
void fc_ssm_recurrence_seq_ctx(fc_cuda_ctx *ctx, fc_buffer *s, fc_buffer *q,
|
|
fc_buffer *k, fc_buffer *v, fc_buffer *g,
|
|
fc_buffer *beta, fc_buffer *y,
|
|
int n_v_heads, int state_size, int npos,
|
|
float scale, fc_buffer *snaps);
|
|
|
|
/* Batched decay gate: alpha/g [npos][n_heads], dt/a broadcast [n_heads]. */
|
|
void fc_ssm_decay_gate_nx_ctx(fc_cuda_ctx *ctx, fc_buffer *alpha,
|
|
fc_buffer *ssm_dt, fc_buffer *ssm_a,
|
|
fc_buffer *g, int n_heads, int npos);
|
|
|
|
/* Batched q/k L2-norm + GQA repeat (+ v passthrough): conv_out
|
|
* [npos][conv_dim], qg/kg/vg [npos][val_dim]. */
|
|
void fc_ssm_qknorm_gqa_nx_ctx(fc_cuda_ctx *ctx, fc_buffer *conv_out,
|
|
fc_buffer *qg, fc_buffer *kg, fc_buffer *vg,
|
|
int n_k_heads, int kv_groups, int state_size,
|
|
int key_dim, int val_dim, float eps, int npos);
|
|
|
|
/* ================================================================
|
|
* On-die causal GQA flash attention (attention.cu)
|
|
*
|
|
* Replaces the host-side O(N^2) causal softmax of the qwen35moe
|
|
* batched prefill: the KV cache lives on the die ([n_head_kv][max_seq]
|
|
* [head_dim], the host cache's exact layout) and one launch runs the
|
|
* tiled QK^T -> online-softmax -> PV pipeline for all n query rows.
|
|
* Online softmax reduces in tile order (running max + running sum), so
|
|
* outputs drift ~1e-6 relative vs the host single-pass softmax — same
|
|
* math, different reduction order, NOT bit-exact by design.
|
|
* ================================================================ */
|
|
|
|
/* 1 if this ctx has the full flash-attention kernel set loaded (callers
|
|
* pick the host-softmax path otherwise). */
|
|
int fc_attn_flash_available_ctx(fc_cuda_ctx *ctx);
|
|
|
|
/* Split the joint Q+gate projection: qg [n][n_head][2*head_dim] (per head
|
|
* [q | z]) into contiguous q and z, each [n][n_head][head_dim]. */
|
|
void fc_attn_qz_split_ctx(fc_cuda_ctx *ctx, fc_buffer *qg, fc_buffer *q,
|
|
fc_buffer *z, int n_head, int head_dim, int npos);
|
|
|
|
/* Append npos roped K/V rows to the GPU ring KV cache: k_new/v_new
|
|
* [npos][n_head_kv][head_dim], caches [n_head_kv][max_seq][head_dim],
|
|
* row t landing at (pos0 + t) % max_seq per head. */
|
|
void fc_attn_kv_append_ctx(fc_cuda_ctx *ctx, fc_buffer *k_new,
|
|
fc_buffer *v_new, fc_buffer *k_cache,
|
|
fc_buffer *v_cache, int n_head_kv, int head_dim,
|
|
int max_seq, int pos0, int npos);
|
|
|
|
/* Causal GQA flash attention (prefill): q/out [npos][n_head][head_dim],
|
|
* caches as above with rows [0, pos0+npos) live; query row t attends
|
|
* cache rows [0, pos0+t]. Requires pos0+npos <= max_seq (no ring wrap),
|
|
* head_dim a multiple of 16 (<= 1024) and n_head/n_head_kv <= 8.
|
|
* Returns 0 on launch, -1 if unavailable/unsupported (caller falls back
|
|
* to its host path). */
|
|
/* attn_sink/attn_win drive StreamingLLM sliding-window attention: when
|
|
* attn_win > 0 each query attends only [0, attn_sink) U the last attn_win keys
|
|
* (prefill O(N^2) -> O(N*W)); attn_win <= 0 is full attention (byte-identical
|
|
* to the pre-window kernel). Orthogonal to the Q4 sink/win fp16-shadow. */
|
|
int fc_attn_flash_prefill_ctx(fc_cuda_ctx *ctx, fc_buffer *q,
|
|
fc_buffer *k_cache, fc_buffer *v_cache,
|
|
fc_buffer *out, int n_head, int n_head_kv,
|
|
int head_dim, int max_seq, int pos0, int npos,
|
|
int attn_sink, int attn_win, float scale);
|
|
|
|
/* ---- TurboQuant Q4 KV cache (QWEN35_KV_Q4) ------------------------------
|
|
* The cache is stored ggml-Q4_0 (32-element blocks, one fp16 scale each,
|
|
* ~7x smaller than fp32) and dequantized in-kernel by the flash path — the
|
|
* lever for the model's full 256K context on one card. Byte layout matches
|
|
* Pantheon's pn_block_q4_0 so host<->device copies are byte identical. Row
|
|
* bytes = (head_dim/32)*18; head_dim must be a multiple of 32. */
|
|
|
|
/* 1 if this ctx has both Q4 KV kernels loaded. */
|
|
int fc_attn_flash_q4_available_ctx(fc_cuda_ctx *ctx);
|
|
|
|
/* Append + quantize npos roped K/V rows into the Q4 ring cache, and mirror
|
|
* each row into the fp16 sink/recent-window shadow so greedy argmax stops
|
|
* flipping. k_new/v_new fp32 [npos][n_head_kv][head_dim]; caches
|
|
* [n_head_kv][max_seq][row_bytes]; k/v_sink fp16 [n_head_kv][SINKMAX][head_dim];
|
|
* k/v_win fp16 [n_head_kv][WMAX][head_dim] ring. sink/win = live sizes. */
|
|
void fc_attn_kv_append_q4_ctx(fc_cuda_ctx *ctx, fc_buffer *k_new,
|
|
fc_buffer *v_new, fc_buffer *k_cache,
|
|
fc_buffer *v_cache, fc_buffer *k_sink,
|
|
fc_buffer *v_sink, fc_buffer *k_win,
|
|
fc_buffer *v_win, int n_head_kv, int head_dim,
|
|
int max_seq, int pos0, int npos, int sink,
|
|
int win);
|
|
|
|
/* Causal GQA flash attention over the mixed Q4 + fp16-shadow cache (fp16 for
|
|
* sink/recent keys, in-kernel q4 dequant for the middle). Same contract/returns
|
|
* as fc_attn_flash_prefill_ctx. */
|
|
int fc_attn_flash_prefill_q4_ctx(fc_cuda_ctx *ctx, fc_buffer *q,
|
|
fc_buffer *k_cache, fc_buffer *v_cache,
|
|
fc_buffer *k_sink, fc_buffer *v_sink,
|
|
fc_buffer *k_win, fc_buffer *v_win,
|
|
fc_buffer *out, int n_head, int n_head_kv,
|
|
int head_dim, int max_seq, int pos0, int npos,
|
|
int sink, int win, int attn_sink, int attn_win,
|
|
float scale);
|
|
|
|
/* ================================================================
|
|
* Decode-mode split flash attention (QWEN35_FLASH_DECODE)
|
|
*
|
|
* A single decode query attends the full cached context [0, seq_len). The KV
|
|
* sequence is split across n_splits blocks (grid (n_splits, n_head_kv)) so the
|
|
* die is not left near-idle by the npos=1 prefill launch; each block writes a
|
|
* partial {m, l, acc} to scratch and a combine pass merges them per head. The
|
|
* combine is fused into the same ctx-stream launch, so ..._ctx returns once
|
|
* both kernels are queued.
|
|
*
|
|
* Scratch sizing (caller-owned, indexed by global head qh = kvh*kv_groups+g):
|
|
* m_scr, l_scr : >= n_head * n_splits floats
|
|
* acc_scr : >= n_head * n_splits * head_dim floats
|
|
* out: [n_head][head_dim]. Returns 0 on success, -1 on a contract violation /
|
|
* launch failure. */
|
|
int fc_attn_flash_decode_available_ctx(fc_cuda_ctx *ctx);
|
|
int fc_attn_flash_decode_q4_available_ctx(fc_cuda_ctx *ctx);
|
|
|
|
int fc_attn_flash_decode_ctx(fc_cuda_ctx *ctx, fc_buffer *q,
|
|
fc_buffer *k_cache, fc_buffer *v_cache,
|
|
fc_buffer *m_scr, fc_buffer *l_scr,
|
|
fc_buffer *acc_scr, fc_buffer *out, int n_head,
|
|
int n_head_kv, int head_dim, int max_seq,
|
|
int seq_len, int n_splits, int split_len,
|
|
int attn_sink, int attn_win, float scale);
|
|
|
|
int fc_attn_flash_decode_q4_ctx(fc_cuda_ctx *ctx, fc_buffer *q,
|
|
fc_buffer *k_cache, fc_buffer *v_cache,
|
|
fc_buffer *k_sink, fc_buffer *v_sink,
|
|
fc_buffer *k_win, fc_buffer *v_win,
|
|
fc_buffer *m_scr, fc_buffer *l_scr,
|
|
fc_buffer *acc_scr, fc_buffer *out, int n_head,
|
|
int n_head_kv, int head_dim, int max_seq,
|
|
int seq_len, int n_splits, int split_len,
|
|
int sink, int win, int attn_sink, int attn_win,
|
|
float scale);
|
|
|
|
/* ---- Device-position decode variants (CUDA-graph capture) ---------------
|
|
* Same math as the host-arg append/decode calls above, but the token position
|
|
* is read from `pos` (an int32 DEVICE buffer, pos[0] = current position;
|
|
* seq_len = pos[0] + 1) and the split plan (n_splits/split_len) is recomputed
|
|
* ON-DEVICE from kps/splits_cap with the identical formula the caller uses —
|
|
* so every launch argument, buffer address and grid dim is position-invariant
|
|
* and the whole decode attention block can live inside a captured CUDA graph:
|
|
* the host updates pos[0] once per token (outside the graph) and replays.
|
|
* Bit-exact per token to the host-arg variants. The decode launch uses a
|
|
* fixed grid of splits_cap blocks; dead blocks exit immediately. Scratch must
|
|
* be sized for splits_cap. Caller guarantees no ring wrap
|
|
* (pos[0] + 1 <= max_seq). Kernels are OPTIONAL in the PTX blob (older
|
|
* fc_cuda_ptx.h): probe with fc_attn_decode_graph_available_ctx before use. */
|
|
|
|
/* 1 if every kernel the graph-captured decode-attention block needs is
|
|
* loaded (qz split, QK norm, RoPE-from-device-pos, sigmoid/mul glue, the
|
|
* *_dev append + split + combine for the given cache mode q4 = 0/1). */
|
|
int fc_attn_decode_graph_available_ctx(fc_cuda_ctx *ctx, int q4);
|
|
|
|
void fc_attn_kv_append_dev_ctx(fc_cuda_ctx *ctx, fc_buffer *k_new,
|
|
fc_buffer *v_new, fc_buffer *k_cache,
|
|
fc_buffer *v_cache, int n_head_kv,
|
|
int head_dim, int max_seq, fc_buffer *pos);
|
|
|
|
void fc_attn_kv_append_q4_dev_ctx(fc_cuda_ctx *ctx, fc_buffer *k_new,
|
|
fc_buffer *v_new, fc_buffer *k_cache,
|
|
fc_buffer *v_cache, fc_buffer *k_sink,
|
|
fc_buffer *v_sink, fc_buffer *k_win,
|
|
fc_buffer *v_win, int n_head_kv,
|
|
int head_dim, int max_seq, fc_buffer *pos,
|
|
int sink, int win);
|
|
|
|
int fc_attn_flash_decode_dev_ctx(fc_cuda_ctx *ctx, fc_buffer *q,
|
|
fc_buffer *k_cache, fc_buffer *v_cache,
|
|
fc_buffer *m_scr, fc_buffer *l_scr,
|
|
fc_buffer *acc_scr, fc_buffer *out,
|
|
int n_head, int n_head_kv, int head_dim,
|
|
int max_seq, fc_buffer *pos, int kps,
|
|
int splits_cap, int attn_sink, int attn_win,
|
|
float scale);
|
|
|
|
int fc_attn_flash_decode_q4_dev_ctx(fc_cuda_ctx *ctx, fc_buffer *q,
|
|
fc_buffer *k_cache, fc_buffer *v_cache,
|
|
fc_buffer *k_sink, fc_buffer *v_sink,
|
|
fc_buffer *k_win, fc_buffer *v_win,
|
|
fc_buffer *m_scr, fc_buffer *l_scr,
|
|
fc_buffer *acc_scr, fc_buffer *out,
|
|
int n_head, int n_head_kv, int head_dim,
|
|
int max_seq, fc_buffer *pos, int kps,
|
|
int splits_cap, int sink, int win,
|
|
int attn_sink, int attn_win, float scale);
|
|
|
|
/* ---- Multi-user decode slot attention (L7) ------------------------------
|
|
* K users, one decode token each at its OWN position, against PER-SLOT KV
|
|
* rings laid out [slot][n_head_kv][max_seq][row] (each slot slice
|
|
* byte-identical to the single-user ring layout). `pos` is the int32 DEVICE
|
|
* position array (the RoPE position buffer, pos[u] = slot u's position);
|
|
* every user's split plan is recomputed on-device from its own position with
|
|
* the single-user formula (kps/splits_cap), so each user's attention output
|
|
* is bit-identical to fc_attn_flash_decode(_q4) at the same position.
|
|
* launch_splits must be >= every user's plan (the caller takes the max of
|
|
* the per-user host plans); partial scratch is strided by launch_splits:
|
|
* m_scr, l_scr : >= nslots * n_head * launch_splits floats
|
|
* acc_scr : >= nslots * n_head * launch_splits * head_dim floats
|
|
* q: [nslots][n_head][head_dim]; out: [nslots][n_head][head_dim].
|
|
* Caller guarantees no ring wrap (pos[u] + 1 <= max_seq for every slot).
|
|
* Kernels are OPTIONAL in the PTX blob (older fc_cuda_ptx.h): probe with the
|
|
* availability calls below. */
|
|
int fc_attn_flash_decode_slots_available_ctx(fc_cuda_ctx *ctx);
|
|
int fc_attn_flash_decode_slots_q4_available_ctx(fc_cuda_ctx *ctx);
|
|
|
|
/* Append each slot's single roped K/V row ([nslots][n_head_kv][head_dim]) to
|
|
* its own ring at pos[u] (q4: quantized in-kernel + per-slot fp16 sink /
|
|
* recent-window shadow mirror, matching the host pn_kvq4_store_row bytes). */
|
|
void fc_attn_kv_append_slots_ctx(fc_cuda_ctx *ctx, fc_buffer *k_new,
|
|
fc_buffer *v_new, fc_buffer *k_cache,
|
|
fc_buffer *v_cache, int n_head_kv,
|
|
int head_dim, int max_seq, fc_buffer *pos,
|
|
int nslots);
|
|
|
|
void fc_attn_kv_append_slots_q4_ctx(fc_cuda_ctx *ctx, fc_buffer *k_new,
|
|
fc_buffer *v_new, fc_buffer *k_cache,
|
|
fc_buffer *v_cache, fc_buffer *k_sink,
|
|
fc_buffer *v_sink, fc_buffer *k_win,
|
|
fc_buffer *v_win, int n_head_kv,
|
|
int head_dim, int max_seq, fc_buffer *pos,
|
|
int nslots, int sink, int win);
|
|
|
|
int fc_attn_flash_decode_slots_ctx(fc_cuda_ctx *ctx, fc_buffer *q,
|
|
fc_buffer *k_cache, fc_buffer *v_cache,
|
|
fc_buffer *m_scr, fc_buffer *l_scr,
|
|
fc_buffer *acc_scr, fc_buffer *out,
|
|
int n_head, int n_head_kv, int head_dim,
|
|
int max_seq, fc_buffer *pos, int nslots,
|
|
int launch_splits, int kps, int splits_cap,
|
|
int attn_sink, int attn_win, float scale);
|
|
|
|
int fc_attn_flash_decode_slots_q4_ctx(fc_cuda_ctx *ctx, fc_buffer *q,
|
|
fc_buffer *k_cache, fc_buffer *v_cache,
|
|
fc_buffer *k_sink, fc_buffer *v_sink,
|
|
fc_buffer *k_win, fc_buffer *v_win,
|
|
fc_buffer *m_scr, fc_buffer *l_scr,
|
|
fc_buffer *acc_scr, fc_buffer *out,
|
|
int n_head, int n_head_kv, int head_dim,
|
|
int max_seq, fc_buffer *pos, int nslots,
|
|
int launch_splits, int kps,
|
|
int splits_cap, int sink, int win,
|
|
int attn_sink, int attn_win, float scale);
|
|
|
|
/* ================================================================
|
|
* Multi-user decode (slot) kernels — K independent sequences, ONE
|
|
* token each, per launch
|
|
*
|
|
* The batched-decode counterparts of the _seq scans: slot u owns its
|
|
* OWN state image (slot-major state buffers) and advances exactly one
|
|
* step, with per-slot math bit-exact to the single-token kernels —
|
|
* batching K users changes throughput, not any user's output.
|
|
* Activations are slot-major [K][dim], matching the batch layout.
|
|
* ================================================================ */
|
|
|
|
/* Conv1d + SiLU, one step per slot: qkv/out [nslots][conv_dim],
|
|
* conv_state [nslots][conv_dim*(conv_kernel-1)] (each slot's slice
|
|
* updated in place). */
|
|
void fc_ssm_conv1d_silu_slots_ctx(fc_cuda_ctx *ctx, fc_buffer *qkv,
|
|
fc_buffer *weight, fc_buffer *conv_state,
|
|
fc_buffer *out, int conv_dim,
|
|
int conv_kernel, int nslots);
|
|
|
|
/* Delta-rule recurrence, one step per slot: s [nslots][n_v_heads*S*S]
|
|
* (each slot's state updated in place), q/k/v/y [nslots][n_v_heads*S],
|
|
* g/beta [nslots][n_v_heads]. */
|
|
void fc_ssm_recurrence_slots_ctx(fc_cuda_ctx *ctx, fc_buffer *s, fc_buffer *q,
|
|
fc_buffer *k, fc_buffer *v, fc_buffer *g,
|
|
fc_buffer *beta, fc_buffer *y,
|
|
int n_v_heads, int state_size, int nslots,
|
|
float scale);
|
|
|
|
/* Partial NEOX RoPE with a PER-ROW position: data [nrows][n_heads*head_dim]
|
|
* rotated in place, pos an int32 DEVICE buffer of nrows entries (row t at
|
|
* position pos[t]) — each user decodes at its own context length. */
|
|
void fc_rope_neox_partial_pos_ctx(fc_cuda_ctx *ctx, fc_buffer *data,
|
|
int n_heads, int head_dim, int rope_dim,
|
|
fc_buffer *pos, float theta_base,
|
|
int nrows);
|
|
|
|
/* ================================================================
|
|
* CUDA graph capture / replay
|
|
*
|
|
* Decode on the K80 is launch-bound: each cuLaunchKernel costs far more
|
|
* host time than the kernel it starts. A captured graph replays a whole
|
|
* recorded launch sequence with ONE cuGraphLaunch. Bit-exact by
|
|
* construction — the graph re-issues the exact kernels, grids and args
|
|
* that were recorded, so the device does precisely the same work.
|
|
*
|
|
* Contract: between begin and end, issue ONLY stream-ordered ops on this
|
|
* ctx's stream (kernel launches, async copies). Synchronous ops
|
|
* (fc_sync_ctx, fc_buffer_to_cpu/from_cpu) invalidate the capture. The
|
|
* recorded launches DO NOT EXECUTE during capture; on a NULL return from
|
|
* fc_graph_end_ctx the caller must re-issue the sequence directly.
|
|
* Replay requires every recorded buffer address and grid dim to still be
|
|
* valid — capture only position-invariant sequences over persistent
|
|
* buffers.
|
|
*
|
|
* Thread scope: capture is THREAD_LOCAL, so the contract above binds the
|
|
* CAPTURING THREAD only. Other threads may allocate, free, sync and run
|
|
* their own dies normally while a capture is open — and may hold their own
|
|
* captures concurrently. (This was not true when capture used GLOBAL mode:
|
|
* any thread's unsafe call anywhere in the process killed the capture.)
|
|
* ================================================================ */
|
|
|
|
typedef struct fc_graph fc_graph;
|
|
|
|
/* Begin capturing ctx's stream. Returns 0 on success, -1 if graphs are
|
|
* unavailable (old driver / missing symbols) — caller keeps its direct
|
|
* launch path. */
|
|
int fc_graph_begin_ctx(fc_cuda_ctx *ctx);
|
|
|
|
/* End capture and instantiate the executable graph. NULL on failure (the
|
|
* recorded launches never ran — re-issue them directly). */
|
|
fc_graph *fc_graph_end_ctx(fc_cuda_ctx *ctx);
|
|
|
|
/* Replay a captured graph on its owning ctx's stream (async, stream-
|
|
* ordered like any launch). Returns 0 on success. */
|
|
int fc_graph_launch(fc_graph *g);
|
|
|
|
void fc_graph_free(fc_graph *g);
|
|
|
|
/* ================================================================
|
|
* CUDA events — non-blocking completion
|
|
*
|
|
* fc_sync_ctx blocks the calling host thread (the driver spins or
|
|
* yields) until the stream drains. The async serve scheduler instead
|
|
* records an event after launching a card's step and POLLS it, so one
|
|
* 2-core host keeps 8 cards' streams in flight simultaneously: the
|
|
* waiter sleeps between polls instead of burning a core per card.
|
|
* ================================================================ */
|
|
|
|
typedef struct fc_event fc_event;
|
|
|
|
/* Create a (timing-disabled) event owned by ctx. NULL if events are
|
|
* unavailable (missing driver symbols / ctx not ready) — caller falls
|
|
* back to fc_sync_ctx. */
|
|
fc_event *fc_event_create_ctx(fc_cuda_ctx *ctx);
|
|
|
|
/* Record ev on ctx's stream: the event completes once all work queued
|
|
* on the stream BEFORE this call has finished. ev must have been
|
|
* created on the same ctx. Returns 0 on success. */
|
|
int fc_event_record_ctx(fc_cuda_ctx *ctx, fc_event *ev);
|
|
|
|
/* Non-blocking completion probe. Returns 0 = the recorded point has
|
|
* completed, 1 = still running, -1 = error/unavailable. Never blocks. */
|
|
int fc_event_query(fc_event *ev);
|
|
|
|
/* Block the calling host thread until ev's recorded point completes
|
|
* (cuEventSynchronize). Narrower than fc_sync_ctx, which drains the WHOLE
|
|
* stream: this waits only for work queued before the record. Returns 0 on
|
|
* success, -1 if events are unavailable.
|
|
*
|
|
* Thread-safe: yes (enters/leaves the owning context)
|
|
* Reentrant: no
|
|
* Signal-safe: no */
|
|
int fc_event_sync(fc_event *ev);
|
|
|
|
/* CROSS-DIE ORDERING WITHOUT THE HOST. Make `waiter`'s stream block until ev
|
|
* completes (cuStreamWaitEvent). ev may have been recorded on a DIFFERENT
|
|
* device's context — that is the point: every other ordering primitive here
|
|
* (fc_sync_ctx, fc_event_query polling) routes a die-to-die dependency through
|
|
* a host round-trip, and this one does not. The call itself returns
|
|
* immediately; it enqueues a wait node, it does not block the host.
|
|
*
|
|
* Pair with fc_buffer_copy_peer_async to build a die-to-die producer/consumer
|
|
* hand-off that costs no host synchronisation:
|
|
* fc_buffer_copy_peer_async(dst_on_b, src_on_a, n, ctx_b);
|
|
* fc_event_record_ctx(ctx_b, ev_b);
|
|
* fc_event_wait_ctx(ctx_a, ev_b); // die A's stream waits on die B
|
|
*
|
|
* Returns 0 on success, -1 if cuStreamWaitEvent is unavailable (old driver) or
|
|
* either handle is unusable.
|
|
*
|
|
* Thread-safe: yes (enters/leaves the waiter's context)
|
|
* Reentrant: no
|
|
* Signal-safe: no */
|
|
int fc_event_wait_ctx(fc_cuda_ctx *waiter, fc_event *ev);
|
|
|
|
void fc_event_free(fc_event *ev);
|
|
|
|
/* ================================================================
|
|
* Synchronization
|
|
*
|
|
* CUDA operations are asynchronous by default. Call fc_sync()
|
|
* when CPU needs GPU results (e.g., before fc_buffer_to_cpu).
|
|
* In practice, cuMemcpyDtoH already synchronizes, so fc_sync()
|
|
* is mainly for explicit fence points.
|
|
* ================================================================ */
|
|
|
|
/* Wait for all pending GPU operations on ctx's stream to complete. */
|
|
void fc_sync_ctx(fc_cuda_ctx *ctx);
|
|
|
|
/* Wait for all pending GPU operations on the process-default context. */
|
|
void fc_sync(void);
|
|
|
|
#ifdef __cplusplus
|
|
}
|
|
#endif
|
|
|
|
#endif /* FOUNDRY_CUDA_H */
|