Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
6c7b75f
Add: planar-transposed activation layout for the PTQ1_0 mat-vec path
sudoingX Sep 19, 2026
a564404
Add: dedicated PTQ1_0 mat-vec kernel with full lane utilization
sudoingX Sep 19, 2026
d6eef0e
Test: add Bonsai 2 projection shapes to the mul_mat perf cases
sudoingX Sep 19, 2026
3db707e
Add: GGML_CUDA_BATCH_INVARIANT for batch-invariant small-batch kernels
sudoingX Sep 19, 2026
145a3a1
Fix: use the mat-vec kernel for bf16 matrices under 64 rows at 2 to 8…
sudoingX Sep 19, 2026
4381a8a
cuda: fold the recurrent-state gather into the GATED_DELTA_NET kernel…
professorpalmer Sep 19, 2026
f437ff8
cuda: PTQ1_0 decode on consumer Ampere/Ada: SoA q8 activations, warp-…
professorpalmer Sep 19, 2026
128c2e6
cuda: PTQ1_0 hybrid dispatch: template y_soa, MMQ from 5 columns
professorpalmer Sep 19, 2026
d5a9e88
cuda: flash attention MMA reads q4_0/q8_0 K/V in place (no F16 scratc…
professorpalmer Sep 19, 2026
5300cd1
cuda: PTQ1_0 multi-column mat-vec: raw digits with exact activation s…
professorpalmer Sep 20, 2026
795ce56
qwen35: MTP graph publishes only the output rows of h_nextn when embe…
professorpalmer Sep 20, 2026
3ce69d2
speculative: draft-mtp decodes the catch-up rows together with the fi…
professorpalmer Sep 20, 2026
d38e711
cuda: Hadamard transform quantizes its own output when every consumer…
professorpalmer Sep 20, 2026
e4bee1e
sampling: reject out-of-vocab token ids from the backend sampler and …
professorpalmer Sep 20, 2026
a038c7f
speculative: draft-mtp drops stale deferred rows when a new task star…
professorpalmer Sep 20, 2026
7716dfc
cuda: release the FWHT-q8 held pool blocks LIFO and before the pools …
professorpalmer Sep 20, 2026
319bb14
server: --spec-draft-depth-max stops drafting once the sequence is deep
professorpalmer Sep 20, 2026
633d12c
cuda: restrict off PTQ1_0 kernel params; Ampere uses the PT 1-col path.
professorpalmer Sep 21, 2026
46f7d0a
cuda: address Copilot review on native q4/q8 FA, MTP catch-up, and BOM.
professorpalmer Sep 21, 2026
14643e5
cuda: share the PTQ1_0 mat-vec smem budget between guard and launch
professorpalmer Sep 21, 2026
dff1e63
spec: flush MTP catch-up when it plus anchors would overflow n_batch
professorpalmer Sep 21, 2026
ad51f87
cuda: restore the scalar PTQ1_0 vec-dot on MUSA
professorpalmer Sep 21, 2026
558e98a
spec: keep MTP catch-up across a failed draft decode
professorpalmer Sep 23, 2026
32842f6
cuda: define the PTQ1_0 layout helper after ggml_cuda_info
professorpalmer Sep 23, 2026
75b0e43
cuda: land remaining #221 review notes: pass q8 layout down, skip HIP…
professorpalmer Sep 25, 2026
09b6cce
cuda: wait on the PDL dependency in the PTQ1_0 planar mat-vec (Hopper…
professorpalmer Sep 25, 2026
f4be987
cuda: restore warp-reduce epilogue under BATCH_INVARIANT so MTP-on ma…
professorpalmer Sep 26, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions common/arg.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -4118,6 +4118,16 @@ common_params_context common_params_parser_init(common_params & params, llama_ex
params.speculative.draft.n_min = value;
}
).set_spec().set_examples({LLAMA_EXAMPLE_SPECULATIVE, LLAMA_EXAMPLE_LOOKUP, LLAMA_EXAMPLE_SERVER, LLAMA_EXAMPLE_CLI}).set_env("LLAMA_ARG_SPEC_DRAFT_N_MIN"));
add_opt(common_arg(
{"--spec-draft-depth-max"}, "N",
string_format("stop drafting once the sequence is longer than N tokens, 0 = never (default: %d)", params.speculative.draft.n_depth_max),
[](common_params & params, int value) {
if (value < 0) {
throw std::invalid_argument("invalid value");
}
params.speculative.draft.n_depth_max = value;
}
).set_spec().set_examples({LLAMA_EXAMPLE_SERVER}).set_env("LLAMA_ARG_SPEC_DRAFT_DEPTH_MAX"));

add_opt(common_arg(
{"--spec-draft-p-split", "--draft-p-split"}, "P",
Expand Down
6 changes: 6 additions & 0 deletions common/common.h
Original file line number Diff line number Diff line change
Expand Up @@ -329,6 +329,12 @@ struct common_params_speculative_draft {
float p_split = 0.1f; // speculative decoding split probability
float p_min = 0.0f; // minimum speculative decoding probability (greedy)

// stop drafting once the sequence is this long (0 = never). Deep in the context a step is bound
// by reading the KV cache, and the draft passes plus the multi-column verify add to that read
// without shortening it: on a 4070 with Bonsai 2 27B the draft is +85% at zero depth, breaks
// even near 24k tokens and costs 30% at 64k. Past the cutoff the slot decodes one token per step.
int32_t n_depth_max = 0;

bool backend_sampling = true; // offload draft sampling to the backend (default: on)

common_params_model mparams;
Expand Down
24 changes: 24 additions & 0 deletions common/sampling.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@

#include <algorithm>
#include <cctype>
#include <cinttypes>
#include <climits>
#include <cmath>
#include <cstring>
Expand Down Expand Up @@ -614,6 +615,21 @@ llama_token common_sampler_sample(struct common_sampler * gsmpl, struct llama_co
if (id != LLAMA_TOKEN_NULL) {
LOG_DBG("%s: Backend sampler selected token: '%d'. Will not run any CPU samplers\n", __func__, id);

{
const int32_t n_vocab = llama_vocab_n_tokens(llama_model_get_vocab(llama_get_model(ctx)));
if (id < 0 || id >= n_vocab) {
// diagnostic: a backend-sampled id outside the vocab would otherwise surface much later as a
// std::vector::at() throw in the tokenizer ("invalid vector subscript"). log the origin and
// fall back to the CPU chain on the (already fetched) logits/candidates.
LOG_ERR("%s: backend sampler returned out-of-vocab token id %d (idx=%d, n_vocab=%d, cur_p.size=%zu, cur_p[0].id=%d) - falling back to CPU sampling\n",
__func__, id, idx, n_vocab, cur_p.size, cur_p.size > 0 ? cur_p.data[0].id : -1);
id = LLAMA_TOKEN_NULL;
}
}
}

if (id != LLAMA_TOKEN_NULL) {

GGML_ASSERT(!gsmpl->grmr && "using grammar in combination with backend sampling is not supported");
GGML_ASSERT(!gsmpl->rbudget && "using reasoning budget in combination with backend sampling is not supported");

Expand All @@ -639,6 +655,14 @@ llama_token common_sampler_sample(struct common_sampler * gsmpl, struct llama_co

id = cur_p.data[cur_p.selected].id;

{
const int32_t n_vocab = llama_vocab_n_tokens(llama_model_get_vocab(llama_get_model(ctx)));
if (id < 0 || id >= n_vocab) {
LOG_ERR("%s: CPU chain selected out-of-vocab token id %d (idx=%d, n_vocab=%d, selected=%" PRId64 ", cur_p.size=%zu) - candidate ids from the backend are corrupt\n",
__func__, id, idx, n_vocab, cur_p.selected, cur_p.size);
}
}

if (grammar_first || !grammar_should_apply(gsmpl)) {
return id;
}
Expand Down
239 changes: 237 additions & 2 deletions common/speculative.cpp

Large diffs are not rendered by default.

4 changes: 4 additions & 0 deletions common/speculative.h
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,10 @@ struct common_speculative_output_limits {
common_speculative_output_limits common_speculative_get_output_limits(
int32_t n_batch, int32_t n_parallel, int32_t n_draft);

// True if deferred catch-up rows plus one first-draft anchor per sequence fit in one llama_decode.
// Used by draft-mtp so a full-prefill stash (n_tokens == n_batch) does not add a 33rd row.
bool common_speculative_mtp_first_decode_fits(int32_t n_batch, int32_t catchup_rows, int32_t n_anchors);

common_speculative * common_speculative_init(common_params_speculative & params, uint32_t n_seq);

void common_speculative_free(common_speculative * spec);
Expand Down
173 changes: 173 additions & 0 deletions ggml/src/ggml-cuda/common.cuh
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,14 @@ static int ggml_cuda_highest_compiled_arch(const int arch) {

// ---------------------------------------------------------------------------------------------------------

// GGML_CUDA_BATCH_INVARIANT=1: prefer kernels whose per-column arithmetic does not depend on the
// number of columns in the batch (1 to 8), so that a token decoded alone and a token verified
// inside a speculative batch see the same logits bit for bit. Costs some throughput at 2 to 8 columns.
static inline bool ggml_cuda_batch_invariant() {
static const bool enabled = getenv("GGML_CUDA_BATCH_INVARIANT") != nullptr;
return enabled;
}

#define MATRIX_ROW_PADDING 512 // last row of quant. matrices is a multiple of this to avoid out-of-bounds memory accesses

#define GGML_CUDA_MAX_STREAMS 8
Expand Down Expand Up @@ -741,6 +749,20 @@ static __device__ __forceinline__ int ggml_cuda_dp4a(const int a, const int b, i
#endif // defined(GGML_USE_HIP)
}

// c += dot(a as 4 unsigned bytes, b as 4 signed bytes). Used by the ternary paths that keep the raw
// digits {0,1,2} and subtract the exact integer activation sum once per block instead of biasing
// every word (two SIMD ops per 4 weights). PTX dp4a takes mixed .u32.s32 operand types directly.
static __device__ __forceinline__ int ggml_cuda_dp4a_us(const unsigned int a, const int b, int c) {
#if !defined(GGML_USE_HIP) && !defined(GGML_USE_MUSA) && __CUDA_ARCH__ >= GGML_CUDA_CC_DP4A
asm("dp4a.u32.s32 %0, %1, %2, %3;" : "=r"(c) : "r"(a), "r"(b), "r"(c));
return c;
#else
const uint8_t * a8 = (const uint8_t *) &a;
const int8_t * b8 = (const int8_t *) &b;
return c + (int) a8[0]*b8[0] + (int) a8[1]*b8[1] + (int) a8[2]*b8[2] + (int) a8[3]*b8[3];
#endif
}

static __device__ __forceinline__ void ggml_cuda_mad(float & acc, const float v, const float u) {
acc += v*u;
}
Expand Down Expand Up @@ -1026,6 +1048,58 @@ struct ggml_cuda_type_traits<GGML_TYPE_PTQ1_0> {
static constexpr int qi = QI_PTQ1_0;
};

// Activation (src1) q8_1 layouts produced by quantize_row_q8_1_cuda and consumed by the MMVQ kernels.
// Every layout keeps block_q8_1's bytes per row, so launcher strides in block_q8_1 units are valid
// for all three; only the byte order inside a column differs.
enum ggml_cuda_q8_1_layout : int {
GGML_CUDA_Q8_1_AOS = 0, // plain block_q8_1 array (every type except the PTQ1_0 cases below)
GGML_CUDA_Q8_1_SOA_ISUM = 1, // PTQ1_0, one column: warp-transposed, exact int sums (ggml_cuda_ptq1_q8_word)
GGML_CUDA_Q8_1_PT = 2, // PTQ1_0, 2-8 columns or MoE ids: planar-transposed (mmvq-ptq1_0.cuh)
};

// Column-count helper, not the layout decision. 2-8 columns and MoE (ids) take the planar
// kernel. One column returns the Ada default (SOA_ISUM). Ampere and GGML_CUDA_BATCH_INVARIANT
// override that to PT in ggml_cuda_q8_1_layout_host, which is what the quantizer and the kernel
// switch both call. This helper does not consult the compute capability.
static constexpr __host__ __device__ ggml_cuda_q8_1_layout ggml_cuda_q8_1_layout_for(ggml_type type_src0, int ncols_dst, bool has_ids) {
#if defined(GGML_USE_HIP)
GGML_UNUSED(type_src0); GGML_UNUSED(ncols_dst); GGML_UNUSED(has_ids);
return GGML_CUDA_Q8_1_AOS;
#else
if (type_src0 != GGML_TYPE_PTQ1_0) {
return GGML_CUDA_Q8_1_AOS;
}
return (ncols_dst == 1 && !has_ids) ? GGML_CUDA_Q8_1_SOA_ISUM : GGML_CUDA_Q8_1_PT;
#endif
}

// ggml_cuda_q8_1_layout_host is defined below ggml_cuda_info(); it reads the device
// compute capability, which is not declared yet at this point in the header.

// Warp-transposed (SoA) q8_1 activation layout for the ternary MMVQ.
//
// One PTQ1_0 K-block (128 weights) consumes 4 block_q8_1 = 36 words (32 qs + 4 ds). In the
// small-K MMVQ geometry each lane owns one K-block, so with the plain AoS layout a warp-wide
// load of "word w" touches 32 lines 144 B apart: ~36 L1 wavefronts per instruction, ~1300 per
// K-iteration against ~250 for the weights themselves. That LSU traffic, not GDDR, capped the
// PTQ1 GEMV near 370 GB/s on Ada. Here K-blocks are grouped by 32 and word w of the group is
// stored contiguously, so the same load is 32 consecutive words = 1 wavefront.
// Bytes per column are unchanged when K is padded to a multiple of 32*128 = 4096.
#define GGML_CUDA_PTQ1_Q8_GROUP_KB 32
#define GGML_CUDA_PTQ1_Q8_WORDS_PER_KB 36
#define GGML_CUDA_PTQ1_Q8_GROUP_WORDS (GGML_CUDA_PTQ1_Q8_GROUP_KB * GGML_CUDA_PTQ1_Q8_WORDS_PER_KB)
#define GGML_CUDA_PTQ1_K_PAD (GGML_CUDA_PTQ1_Q8_GROUP_KB * QK_PTQ1_0)

// Word offset (within one activation column) of word w (0..7 = qs words, 8 = ds) of block_q8_1 ib.
static constexpr __host__ __device__ int ggml_cuda_ptq1_q8_word(int ib, int w) {
const int kb = ib >> 2;
const int sub = ib & 3;
const int g = kb >> 5;
const int lane = kb & 31;
const int ww = w < 8 ? sub * 8 + w : 32 + sub;
return g * GGML_CUDA_PTQ1_Q8_GROUP_WORDS + ww * GGML_CUDA_PTQ1_Q8_GROUP_KB + lane;
}

template<>
struct ggml_cuda_type_traits<GGML_TYPE_Q4_0> {
static constexpr int qk = QK4_0;
Expand Down Expand Up @@ -1205,6 +1279,26 @@ const ggml_cuda_device_info & ggml_cuda_info();
void ggml_cuda_set_device(int device);
int ggml_cuda_get_device();

// Host-side wrapper used by both the quantizer call and the kernel switch.
// Under GGML_CUDA_BATCH_INVARIANT the one-column case must run the same arithmetic as 2-8
// columns, so it takes the planar layout (the SoA vec-dot sums in a different order).
// Ampere (sm_80/86, including the 3060/3090/170HX): the #218 PT kernel wins at one column
// too (+5.9% tg128 vs SoA on a 3060). Ada and newer keep SOA_ISUM at one column (4070 win).
// Lives here, after ggml_cuda_info(), because the body reads the current device's cc.
static inline ggml_cuda_q8_1_layout ggml_cuda_q8_1_layout_host(ggml_type type_src0, int ncols_dst, bool has_ids) {
const ggml_cuda_q8_1_layout l = ggml_cuda_q8_1_layout_for(type_src0, ncols_dst, has_ids);
if (l == GGML_CUDA_Q8_1_SOA_ISUM && ggml_cuda_batch_invariant()) {
return GGML_CUDA_Q8_1_PT;
}
if (l == GGML_CUDA_Q8_1_SOA_ISUM) {
const int cc = ggml_cuda_info().devices[ggml_cuda_get_device()].cc;
if (GGML_CUDA_CC_IS_NVIDIA(cc) && cc >= GGML_CUDA_CC_AMPERE && cc < GGML_CUDA_CC_ADA_LOVELACE) {
return GGML_CUDA_Q8_1_PT;
}
}
return l;
}

struct ggml_cuda_pool {
virtual ~ggml_cuda_pool() = default;

Expand Down Expand Up @@ -1453,6 +1547,79 @@ struct ggml_cuda_stream_context {
}
};

// Fused recurrent-state gather for GATED_DELTA_NET. build_rs materialises GET_ROWS(cache, s_copy)
// into a temp per layer that only the GDN kernel reads; when the graph evaluator can prove that, it
// skips the GET_ROWS and records the gather here so the kernel reads cache row ids[seq] directly.
struct ggml_cuda_gated_delta_net_gather {
const float * base = nullptr; // cache rows [row_stride floats each]
const int32_t * ids = nullptr; // per-seq row index
int64_t row_stride = 0; // in floats
};

// Owned by the backend context that evaluates the graph: registrations are keyed by node pointer,
// so they are only meaningful for the evaluation that made them. Reset at the start of every
// graph evaluation/capture; never shared between contexts or threads.
struct ggml_cuda_gdn_gather_context {
std::unordered_map<const ggml_tensor *, ggml_cuda_gated_delta_net_gather> gathers;

void reset() {
gathers.clear();
}

void set(const ggml_tensor * gdn, const ggml_cuda_gated_delta_net_gather & gather) {
gathers[gdn] = gather;
}

const ggml_cuda_gated_delta_net_gather * find(const ggml_tensor * gdn) const {
const auto it = gathers.find(gdn);
return it == gathers.end() ? nullptr : &it->second;
}
};

// A Hadamard transform (MUL_MAT with GGML_HINT_SRC0_IS_HADAMARD, optionally preceded by the sign
// flip) whose only consumers are PTQ1_0 mat-vecs writes the q8_1-quantized activation into its own
// output buffer instead of the F32 result (the quantized rows are 9/8 bytes per element, so they
// fit in the F32 allocation), and the mat-vecs skip their quantize launch. The output buffer's
// lifetime is exactly the consumers' lifetime, so nothing about allocation changes. Keyed by tensor
// identity (the transform output and every reshape view of it that a mat-vec consumes), never by
// data pointer: ggml-alloc recycles a dead output's block for later tensors in the same graph, and a
// later PTQ1_0 mat-vec whose src1 landed there must not mistake its F32 rows for q8_1. Valid for one
// graph evaluation.
struct ggml_cuda_fwht_q8 {
ggml_cuda_q8_1_layout layout = GGML_CUDA_Q8_1_AOS;
int64_t ne0 = 0; // padded row width the quantizer wrote (what the mat-vec expects)
int64_t ncols = 0; // rows quantized (src1->ne[1] of every consumer)
const void * data = nullptr; // the q8_1 rows: the transform's own output buffer, or a pool block
// held for the rest of the graph when that buffer aliases the input
};

struct ggml_cuda_fwht_q8_context {
std::unordered_map<const ggml_tensor *, ggml_cuda_fwht_q8> entries;
std::vector<std::unique_ptr<ggml_cuda_pool_alloc<char>>> held; // released at reset(), newest first (VMM pool is LIFO)

// the implicit destructor would release `held` oldest first, which the VMM pool asserts on. The
// owning context also calls reset() before its pools go away (this member is declared before them).
~ggml_cuda_fwht_q8_context() {
reset();
}

void reset() {
entries.clear();
while (!held.empty()) {
held.pop_back();
}
}

void set(const ggml_tensor * out, const ggml_cuda_fwht_q8 & e) {
entries[out] = e;
}

const ggml_cuda_fwht_q8 * find(const ggml_tensor * out) const {
const auto it = entries.find(out);
return it == entries.end() ? nullptr : &it->second;
}
};

struct ggml_backend_cuda_context {
int device;
std::string name;
Expand Down Expand Up @@ -1523,6 +1690,8 @@ struct ggml_backend_cuda_context {
}

ggml_cuda_stream_context concurrent_stream_context;
ggml_cuda_gdn_gather_context gdn_gather_context;
ggml_cuda_fwht_q8_context fwht_q8_context;

~ggml_backend_cuda_context();

Expand All @@ -1538,6 +1707,10 @@ struct ggml_backend_cuda_context {

ggml_cuda_stream_context & stream_context() { return concurrent_stream_context; }

ggml_cuda_gdn_gather_context & gdn_gathers() { return gdn_gather_context; }

ggml_cuda_fwht_q8_context & fwht_q8() { return fwht_q8_context; }

cublasHandle_t cublas_handle() {
if (cublas_handles[device][curr_stream_no] == nullptr) {
ggml_cuda_set_device(device);
Expand Down
5 changes: 4 additions & 1 deletion ggml/src/ggml-cuda/fattn-common.cuh
Original file line number Diff line number Diff line change
Expand Up @@ -1154,11 +1154,14 @@ void launch_fattn(

// If ntiles_total % blocks_per_wave != 0 then some efficiency is lost due to tail effects.
// Test whether parallel_blocks can be set to a higher value for better efficiency.
// Batch-invariant mode: size the KV split as for a single query tile, so the order in which the
// partial softmax results are combined does not depend on how many queries are in the batch.
const int ntiles_dst_eff = ggml_cuda_batch_invariant() ? ntiles_dst / ntiles_x : ntiles_dst;
const int blocks_per_wave = nsm * max_blocks_per_sm;
int nwaves_best = 0;
int efficiency_percent_best = 0;
for (int parallel_blocks_test = parallel_blocks; parallel_blocks_test <= ntiles_KV; ++parallel_blocks_test) {
const int nblocks_total = ntiles_dst * parallel_blocks_test;
const int nblocks_total = ntiles_dst_eff * parallel_blocks_test;
const int nwaves = (nblocks_total + blocks_per_wave - 1) / blocks_per_wave;
const int efficiency_percent = 100 * nblocks_total / (nwaves*blocks_per_wave);

Expand Down
Loading
Loading