Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
75 changes: 70 additions & 5 deletions common/speculative.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@

#include <algorithm>
#include <cassert>
#include <cmath>
#include <cstring>
#include <iomanip>
#include <map>
Expand Down Expand Up @@ -923,6 +924,10 @@ struct common_speculative_impl_draft_dflash : public common_speculative_impl {
int32_t block_size = 0;
llama_token mask_token_id = 0;

bool is_dflash2 = false;
bool is_mrope = false;
int32_t selector_top_k = 0;

// draft-dspark: the draft carries a Markov head and uses an anchor-first block layout
const bool is_dspark;

Expand Down Expand Up @@ -967,6 +972,9 @@ struct common_speculative_impl_draft_dflash : public common_speculative_impl {
sample_from_anchor = std::strcmp(buf, "true") == 0;
}
}

selector_top_k = llama_model_dflash_selector_top_k(model_dft);
is_dflash2 = selector_top_k > 0;
mask_token_id = llama_vocab_mask(llama_model_get_vocab(model_dft));

LOG_INF("%s: adding speculative implementation '%s'\n", __func__, common_speculative_type_to_str(type).c_str());
Expand All @@ -987,6 +995,13 @@ struct common_speculative_impl_draft_dflash : public common_speculative_impl {
batch = llama_batch_init(llama_n_batch(ctx_dft), 0, n_seq);
batch_inject = llama_batch_init(llama_n_batch(ctx_dft), n_embd_dec, n_seq);

// embd batches on an M-RoPE draft need 4 position rows per token
is_mrope = llama_model_rope_type(model_dft) == LLAMA_ROPE_TYPE_MROPE;
if (is_mrope) {
free(batch_inject.pos);
batch_inject.pos = (llama_pos *) malloc(sizeof(llama_pos) * 4 * llama_n_batch(ctx_dft));
}

smpls.resize(n_seq);
for (auto & s : smpls) {
common_params_sampling sparams;
Expand All @@ -998,7 +1013,7 @@ struct common_speculative_impl_draft_dflash : public common_speculative_impl {

// offload draft sampling to the backend
backend_chains.assign(n_seq, nullptr);
if (this->params.backend_sampling) {
if (this->params.backend_sampling && !is_dflash2) {
for (llama_seq_id seq_id = 0; seq_id < (llama_seq_id) n_seq; ++seq_id) {
llama_sampler * chain = llama_sampler_chain_init(llama_sampler_chain_default_params());
llama_sampler_chain_add(chain, llama_sampler_init_top_k(10));
Expand All @@ -1017,7 +1032,8 @@ struct common_speculative_impl_draft_dflash : public common_speculative_impl {
llama_set_embeddings_layer_inp(ctx_tgt, (uint32_t) target_layer_ids[k], true);
}

llama_set_embeddings_nextn(ctx_dft, true, /*masked*/ true);
// DFlash2 reads its selector lattice from h_nextn and never consumes raw logits.
llama_set_embeddings_nextn(ctx_dft, true, /*masked*/ !is_dflash2);
llama_set_causal_attn(ctx_dft, false); // DFlash needs non-causal attention
}

Expand Down Expand Up @@ -1118,11 +1134,24 @@ struct common_speculative_impl_draft_dflash : public common_speculative_impl {
}

// fuse extracted features through DFlash encoder
// M-RoPE drafts read 4 position rows per token from embd batches, so pass them explicitly
std::vector<llama_pos> enc_pos;
if (is_mrope) {
enc_pos.resize((size_t) 4 * n_chunk);
for (int32_t i = 0; i < n_chunk; ++i) {
const llama_pos p = batch_in.pos[i_batch_beg[seq_id] + offset + i];
enc_pos[0 * n_chunk + i] = p;
enc_pos[1 * n_chunk + i] = p;
enc_pos[2 * n_chunk + i] = p;
enc_pos[3 * n_chunk + i] = 0;
}
}

llama_batch enc_batch = {
/*.n_tokens =*/ n_chunk,
/*.token =*/ nullptr,
/*.embd =*/ features_buf.data(),
/*.pos =*/ nullptr,
/*.pos =*/ is_mrope ? enc_pos.data() : nullptr,
/*.n_seq_id =*/ nullptr,
/*.seq_id =*/ nullptr,
/*.logits =*/ nullptr,
Expand All @@ -1143,7 +1172,13 @@ struct common_speculative_impl_draft_dflash : public common_speculative_impl {
std::memcpy(batch_inject.embd, inp_g, (size_t) n_chunk * n_embd_dec * sizeof(float));

for (int32_t i = 0; i < n_chunk; ++i) {
batch_inject.pos[i] = batch_in.pos[i_batch_beg[seq_id] + offset + i];
const llama_pos p = batch_in.pos[i_batch_beg[seq_id] + offset + i];
batch_inject.pos[i] = p;
if (is_mrope) {
batch_inject.pos[1 * n_chunk + i] = p;
batch_inject.pos[2 * n_chunk + i] = p;
batch_inject.pos[3 * n_chunk + i] = 0;
}
batch_inject.n_seq_id[i] = 1;
batch_inject.seq_id[i][0] = seq_id;
batch_inject.logits[i] = false;
Expand Down Expand Up @@ -1186,7 +1221,7 @@ struct common_speculative_impl_draft_dflash : public common_speculative_impl {
i_block_beg[seq_id] = batch.n_tokens;
n_block [seq_id] = n_block_tokens;
for (int32_t i = 0; i < n_block_tokens; ++i) {
common_batch_add(batch, i == 0 ? dp.id_last : mask_token_id, n + i, { seq_id }, true);
common_batch_add(batch, i == 0 ? dp.id_last : mask_token_id, n + i, { seq_id }, !is_dflash2);
}
}

Expand Down Expand Up @@ -1214,6 +1249,36 @@ struct common_speculative_impl_draft_dflash : public common_speculative_impl {

auto & result = *dp.result;

if (is_dflash2) {
const float * lattice = llama_get_embeddings_nextn(ctx_dft);
GGML_ASSERT(lattice && "DFlash2 selector produced no lattice");

int32_t predecessor = 0;
for (int32_t i = 1; i < n_block_tokens; ++i) {
const float * row = lattice + (size_t) (beg + i) * n_embd_dec;
const float * scores = row + selector_top_k + (size_t) predecessor * selector_top_k;

predecessor = (int32_t) std::distance(scores,
std::max_element(scores, scores + selector_top_k));
if (params.p_min > 0.0f) {
// softmax(scores) at the argmax, i.e. 1 / sum(exp(s_k - s_max))
float sum = 0.0f;
for (int32_t k = 0; k < selector_top_k; ++k) {
sum += std::exp(scores[k] - scores[predecessor]);
}
if (1.0f / sum < params.p_min) {
break;
}
}
result.push_back((llama_token) row[predecessor]);
}

if (result.size() < (size_t) params.n_min) {
result.clear();
}
continue;
}

if (is_dspark) {
// DSpark: read from the first draft slot, truncate below the confidence threshold
const float * conf = params.p_min > 0.0f ? llama_get_embeddings_nextn(ctx_dft) : nullptr;
Expand Down
1 change: 1 addition & 0 deletions conversion/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@
"DeepseekV3ForCausalLM": "deepseek",
"DeepseekV32ForCausalLM": "deepseek",
"DFlashDraftModel": "qwen",
"DFlash2DraftModel": "qwen",
"Qwen3DSparkModel": "qwen",
"DSparkDraftModel": "qwen",
"DSparkSpeculator": "qwen",
Expand Down
62 changes: 59 additions & 3 deletions conversion/qwen.py
Original file line number Diff line number Diff line change
Expand Up @@ -639,7 +639,7 @@ class Qwen3_5MoeTextModel(_Qwen35MRopeMixin, _LinearAttentionVReorderBase):
model_arch = gguf.MODEL_ARCH.QWEN35MOE


@ModelBase.register("DFlashDraftModel")
@ModelBase.register("DFlashDraftModel", "DFlash2DraftModel")
@ModelBase.example("z-lab/Qwen3.5-9B-DFlash")
class DFlashModel(Qwen3Model):
model_arch = gguf.MODEL_ARCH.DFLASH
Expand Down Expand Up @@ -678,9 +678,31 @@ def set_vocab(self):
def set_gguf_parameters(self):
super().set_gguf_parameters()

block_size = self.hparams.get("block_size", 16)
self.gguf_writer.add_block_size(block_size)
dflash_config = self.hparams.get("dflash_config", {})
block_size = dflash_config.get("block_size", self.hparams.get("block_size", 16))
self.gguf_writer.add_block_size(block_size)

if "conv_kernel_size" in dflash_config:
self.gguf_writer.add_conv_kernel_size(int(dflash_config["conv_kernel_size"]))
self.gguf_writer.add_conv_group_size(int(dflash_config["conv_group_size"]))
self.gguf_writer.add_selector_rank(int(dflash_config["selector_rank"]))
self.gguf_writer.add_selector_top_k(int(dflash_config["selector_top_k"]))

output_multiplier = dflash_config.get(
"output_multiplier", self.hparams.get("output_multiplier")
)
if output_multiplier is not None:
self.gguf_writer.add_logit_scale(float(output_multiplier))
softcap = dflash_config.get(
"final_logit_softcapping", self.hparams.get("final_logit_softcapping")
)
if softcap is not None and float(softcap) > 0:
self.gguf_writer.add_final_logit_softcapping(float(softcap))
embedding_scale = dflash_config.get(
"input_embedding_scale", self.hparams.get("input_embedding_scale")
)
if embedding_scale is not None:
self.gguf_writer.add_embedding_scale(float(embedding_scale))

target_layer_ids = dflash_config.get("target_layer_ids", [])
if target_layer_ids:
Expand All @@ -695,17 +717,51 @@ def set_gguf_parameters(self):
self.gguf_writer.add_sliding_window(sliding_window)
self.gguf_writer.add_sliding_window_pattern(is_swa)

# M-RoPE target: the draft ropes on the temporal dim only, so write
# degenerate sections [n_rot/2, 0, 0, 0]
if self._target_uses_mrope():
head_dim = self.hparams.get("head_dim") or self.hparams["hidden_size"] // self.hparams["num_attention_heads"]
self.gguf_writer.add_rope_dimension_sections([head_dim // 2, 0, 0, 0])

def _target_uses_mrope(self) -> bool:
if self.target_model_dir is None:
return False
with open(self.target_model_dir / "config.json", "r", encoding="utf-8") as f:
cfg = json.load(f)
cfg = cfg.get("text_config", cfg)
rope = cfg.get("rope_parameters") or cfg.get("rope_scaling") or {}
return "mrope_section" in rope

@classmethod
def filter_tensors(cls, item: tuple[str, Callable[[], Tensor]]) -> tuple[str, Callable[[], Tensor]] | None:
name, gen = item
if not name.startswith("model."):
name = "model." + name
return super().filter_tensors((name, gen))

_ROPE_PERMUTE_SUFFIXES = (
"self_attn.q_proj.weight",
"self_attn.k_proj.weight",
"self_attn.q_norm.weight",
"self_attn.k_norm.weight",
)

def modify_tensors(self, data_torch: Tensor, name: str, bid: int | None) -> Iterable[tuple[str, Tensor]]:
if name == "model.embed_tokens.weight" and not self.hparams.get("has_embed_tokens", True):
return

# interleaved-rope checkpoints (rope_is_neox_style = false) -> NeoX layout: per head, even dims first then odd
if not self.hparams.get("rope_is_neox_style", True) and name.endswith(self._ROPE_PERMUTE_SUFFIXES):
head_dim = self.hparams["head_dim"]
shape = data_torch.shape
data_torch = data_torch.reshape(-1, head_dim // 2, 2, *shape[1:]).transpose(1, 2).reshape(shape)

if name in (
"model.candidate_selector.predecessor_codebook",
"model.candidate_selector.successor_codebook",
):
name += ".weight"

yield from super().modify_tensors(data_torch, name, bid)


Expand Down
100 changes: 100 additions & 0 deletions ggml/src/ggml-cuda/top-k.cu

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

normally we would require CUDA changes to be in a dedicated PR, unless strictly necessary. but just asking if @ruixiang63 @am17an are ok or you want CUDA in a dedicated one?

also, please tell your agents to clean up code comments per agents.md requirements

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The comments are cleaned up in d1a522f.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it makes sense to include the CUDA kernel changes in this PR, as they are a key selling point of DFlash2. We should also have the CUDA maintainers review the PR.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I could not repro consistent perf improvements on my E2E runs, using speed-bench as the target (cells show avg pred_t/s)

c1 OFF c1 ON c4 OFF c4 ON
DGX Spark 34.53 34.92 (+1.1%) 17.16 17.51 (+2.0%)
B4500 82.53 82.76 (+0.3%) 38.75 37.70 (-2.7%)
Details

Server command (same for all runs, -np set to 1 or 4):

./build_topk_changes/bin/llama-server \
  -m /mnt/share/gguf/unsloth/Qwen3.8-27B-GGUF/Qwen3.8-27B-UD-Q4_K_M.gguf \
  --spec-type draft-dflash \
  -md /mnt/share/gguf/incoai/Qwen3.8-27B-DFlash2-GGUF/Qwen3.8-27B-DFlash2-Q4_K_M.gguf \
  --spec-draft-n-max 7 \
  -bs \
  --load-mode dio \
  -np 1|4

Bench command (same for all runs):

python tools/server/bench/speed-bench/speed_bench.py \
  --url localhost:8080 --bench qualitative --osl 256 --concurrency 1|4 --limit 8

88 samples total, 8 per category. OFF/ON = topk fusion change disabled/enabled in the server build.

DGX Spark

Cells: avg pred_t/s / avg latency.

category c1 OFF c1 ON c4 OFF c4 ON
coding 34.74 / 10.099s 34.90 / 10.154s 16.59 / 21.958s 17.42 / 20.930s
humanities 33.30 / 9.276s 33.26 / 9.276s 16.05 / 19.511s 16.24 / 19.355s
math 36.45 / 7.443s 36.31 / 7.468s 18.06 / 15.602s 17.96 / 16.026s
qa 32.04 / 7.901s 32.40 / 7.828s 15.23 / 16.724s 15.00 / 16.658s
rag 37.23 / 7.303s 37.84 / 7.197s 16.96 / 15.318s 17.99 / 15.302s
reasoning 34.36 / 7.629s 34.73 / 7.548s 17.02 / 16.106s 17.31 / 16.118s
stem 33.95 / 7.708s 34.77 / 7.529s 16.12 / 16.851s 17.41 / 15.607s
writing 34.06 / 9.686s 34.80 / 9.499s 17.72 / 19.242s 17.81 / 19.456s
multilingual 39.82 / 6.787s 40.46 / 6.688s 21.61 / 13.162s 20.47 / 13.268s
summarization 32.05 / 7.637s 32.55 / 7.521s 16.86 / 11.983s 17.41 / 12.812s
roleplay 31.85 / 16.352s 32.13 / 16.247s 16.50 / 31.989s 17.55 / 30.468s
overall 34.53 / 8.893s 34.92 / 8.814s 17.16 / 18.041s 17.51 / 17.818s

B4500

Cells: avg pred_t/s / avg latency.

category c1 OFF c1 ON c4 OFF c4 ON
coding 77.21 / 5.030s 78.12 / 5.132s 43.59 / 9.658s 41.56 / 9.950s
humanities 83.67 / 4.121s 84.01 / 4.098s 36.58 / 8.938s 35.45 / 9.196s
math 90.15 / 3.235s 90.20 / 3.238s 40.14 / 7.365s 40.63 / 7.315s
qa 69.73 / 3.864s 69.82 / 3.873s 36.98 / 7.805s 32.70 / 7.769s
rag 83.57 / 4.008s 83.66 / 4.022s 33.08 / 8.409s 35.96 / 8.573s
reasoning 87.23 / 3.175s 87.50 / 3.166s 39.35 / 7.640s 36.22 / 7.904s
stem 87.19 / 3.139s 87.39 / 3.129s 38.96 / 7.329s 38.36 / 7.468s
writing 84.46 / 4.630s 84.74 / 4.619s 39.44 / 9.654s 36.30 / 9.760s
multilingual 94.65 / 3.320s 94.66 / 3.316s 42.73 / 6.913s 42.36 / 7.049s
summarization 76.87 / 2.961s 77.11 / 2.872s 36.99 / 6.597s 35.95 / 6.662s
roleplay 73.07 / 8.074s 73.16 / 8.038s 38.38 / 15.239s 39.22 / 14.844s
overall 82.53 / 4.141s 82.76 / 4.137s 38.75 / 8.686s 37.70 / 8.772s

Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,101 @@ static int next_power_of_2(int x) {

#endif // CUB_TOP_K_AVAILABLE


// Two-stage top-k for wide rows: a global top-k element has at most k-1 larger
// elements, so at most k-1 inside its own tile and tiling cannot drop a winner.
#define TOPK_CAND 1024 // argsort_f32_i32_cuda_bitonic's row limit

// measured on H200 and A10G
#define TOPK_BLOCK 256
#define TOPK_TILE_WIDE 8192
#define TOPK_TILE 4096

template <int TILE, int BLOCK>
static __global__ void topk_tile(const float * src, float * cand_val, int * cand_idx,
const int ncols, const int ntiles, const int k) {
__shared__ uint64_t smem[BLOCK];

const int row = blockIdx.x / ntiles;
const int tile = blockIdx.x % ntiles;
const float * row_ptr = src + (size_t) row * ncols;

uint64_t keys[TILE / BLOCK];
#pragma unroll
for (int i = 0; i < TILE / BLOCK; ++i) {
const int col = tile * TILE + threadIdx.x + i * BLOCK;
uint32_t b = col < ncols ? __float_as_uint(row_ptr[col]) : 0;
b = (b & 0x80000000u) ? ~b : (b | 0x80000000u);
keys[i] = col < ncols ? (((uint64_t) b << 32) | (uint32_t) (ncols - 1 - col)) : 0;
}

const size_t out = ((size_t) row * ntiles + tile) * k;
for (int j = 0; j < k; ++j) {
uint64_t local = 0;
#pragma unroll
for (int i = 0; i < TILE / BLOCK; ++i) {
local = max(local, keys[i]);
}
smem[threadIdx.x] = local;
__syncthreads();
for (int s = BLOCK / 2; s > 0; s >>= 1) {
if (threadIdx.x < s) {
smem[threadIdx.x] = max(smem[threadIdx.x], smem[threadIdx.x + s]);
}
__syncthreads();
}
const uint64_t best = smem[0];
if (threadIdx.x == 0) {
const int col = ncols - 1 - (int) (best & 0xFFFFFFFFu);
cand_val[out + j] = best ? row_ptr[col] : -INFINITY;
cand_idx[out + j] = best ? col : 0;
}
#pragma unroll
for (int i = 0; i < TILE / BLOCK; ++i) {
if (keys[i] == best) {
keys[i] = 0;
}
}
__syncthreads();
}
}

// The argsort ranks candidates; turn its positions back into columns.
static __global__ void topk_unmap(const int * cand_idx, const int * order, int * dst,
const int ncand, const int k) {
for (int i = threadIdx.x; i < k; i += blockDim.x) {
dst[(size_t) blockIdx.x * k + i] = cand_idx[(size_t) blockIdx.x * ncand + order[(size_t) blockIdx.x * ncand + i]];
}
}

static bool ggml_cuda_top_k_tiled(ggml_cuda_pool & pool, const float * src, int * dst,
const int ncols, const int nrows, const int k,
cudaStream_t stream) {
// Narrow rows are already handled whole by the bitonic sort below.
const int tile = ncols >= 65536 ? TOPK_TILE_WIDE : TOPK_TILE;
const int ntiles = (ncols + tile - 1) / tile;
const int ncand = ntiles * k;
if (ncols <= TOPK_CAND || ncand > TOPK_CAND) {
return false;
}

ggml_cuda_pool_alloc<float> cand_val(pool, (size_t) nrows * ncand);
ggml_cuda_pool_alloc<int> cand_idx(pool, (size_t) nrows * ncand);
ggml_cuda_pool_alloc<int> order (pool, (size_t) nrows * ncand);

if (tile == TOPK_TILE_WIDE) {
topk_tile<TOPK_TILE_WIDE, TOPK_BLOCK><<<nrows * ntiles, TOPK_BLOCK, 0, stream>>>(
src, cand_val.get(), cand_idx.get(), ncols, ntiles, k);
} else {
topk_tile<TOPK_TILE, TOPK_BLOCK><<<nrows * ntiles, TOPK_BLOCK, 0, stream>>>(
src, cand_val.get(), cand_idx.get(), ncols, ntiles, k);
}
argsort_f32_i32_cuda_bitonic(cand_val.get(), order.get(), ncand, nrows,
GGML_SORT_ORDER_DESC, stream);
topk_unmap<<<nrows, TOPK_BLOCK, 0, stream>>>(cand_idx.get(), order.get(), dst, ncand, k);
return true;
}

void ggml_cuda_op_top_k(ggml_backend_cuda_context & ctx, ggml_tensor * dst) {
const ggml_tensor * src0 = dst->src[0];
const float * src0_d = (const float *) src0->data;
Expand All @@ -63,6 +158,11 @@ void ggml_cuda_op_top_k(ggml_backend_cuda_context & ctx, ggml_tensor * dst) {
const int64_t nrows = ggml_nrows(src0);
const int64_t k = dst->ne[0];
ggml_cuda_pool & pool = ctx.pool();

if (ggml_cuda_top_k_tiled(pool, src0_d, dst_d, ncols, nrows, k, stream)) {
return;
}

#ifdef CUB_TOP_K_AVAILABLE
// TODO: Switch to `DeviceSegmentedTopK` for multi-row TopK once implemented
// https://github.com/NVIDIA/cccl/issues/6391
Expand Down
Loading
Loading