Skip to content
Open
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
1 change: 1 addition & 0 deletions conversion/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@
"Gemma3nForCausalLM": "gemma",
"Gemma3nForConditionalGeneration": "gemma",
"Gemma4AssistantForCausalLM": "gemma",
"Gemma4DSparkModel": "gemma",
"Gemma4ForConditionalGeneration": "gemma",
"Gemma4ForCausalLM": "gemma",
"Gemma4UnifiedForConditionalGeneration": "gemma",
Expand Down
61 changes: 61 additions & 0 deletions conversion/gemma.py
Original file line number Diff line number Diff line change
Expand Up @@ -851,6 +851,67 @@ def set_gguf_parameters(self):
self.gguf_writer.add_nextn_predict_layers(self.block_count)


@ModelBase.register("Gemma4DSparkModel")
class Gemma4DSparkModel(Gemma4Model):
# DSpark draft with a Gemma4 backbone
model_arch = gguf.MODEL_ARCH.DFLASH

def set_vocab(self):
if self.target_model_dir is None:
raise ValueError("Gemma4 DSpark requires --target-model-dir with the target tokenizer")

original_dir = self.dir_model
try:
self.dir_model = self.target_model_dir
super().set_vocab()
finally:
self.dir_model = original_dir

mask_token_id = self.hparams.get("mask_token_id")
if mask_token_id is not None:
self.gguf_writer.add_mask_token_id(mask_token_id)

def set_gguf_parameters(self):
# inject Gemma3Model's pattern == 1 sentinel so the sliding_window inherited from the target config is not written
if all(lt == "full_attention" for lt in self.hparams["layer_types"]):
self.hparams["sliding_window_pattern"] = 1

super().set_gguf_parameters()

self.gguf_writer.add_block_size(self.hparams.get("block_size", 7))

# flat DeepSpec schema; mirror DFlash's +1 extract-layer convention
target_layer_ids = self.hparams.get("target_layer_ids", [])
if target_layer_ids:
extract_layer_ids = [i + 1 for i in target_layer_ids]
self.gguf_writer.add_target_layers(extract_layer_ids)

# Gemma4TextScaledWordEmbedding scales the shared token embeddings by sqrt(hidden_size)
self.gguf_writer.add_embedding_scale(self.hparams["hidden_size"] ** 0.5)
# Gemma4DSparkAttention uses scaling = 1.0
self.gguf_writer.add_attention_scale(1.0)

@classmethod
def filter_tensors(cls, item: tuple[str, Callable[[], Tensor]]) -> tuple[str, Callable[[], Tensor]] | None:
name, gen = item
# embed_tokens / lm_head are byte-identical to the target and shared at runtime -- drop them
if name.endswith(("embed_tokens.weight", "lm_head.weight")):
return None
if not name.startswith("model."):
name = "model." + name
return super().filter_tensors((name, gen))

def modify_tensors(self, data_torch: Tensor, name: str, bid: int | None) -> Iterable[tuple[str, Tensor]]:
# the shared DFlash tensor map resolves these norm names Qwen-style -- remap them to their Gemma4 meaning
if name.endswith(".post_attention_layernorm.weight"):
yield (self.format_tensor_name(gguf.MODEL_TENSOR.ATTN_POST_NORM, bid), data_torch)
return
if name.endswith(".pre_feedforward_layernorm.weight"):
yield (self.format_tensor_name(gguf.MODEL_TENSOR.FFN_NORM, bid), data_torch)
return
yield from super().modify_tensors(data_torch, name, bid)


@ModelBase.register("Gemma4ForConditionalGeneration")
@ModelBase.example("google/gemma-4-31B-it", "google/gemma-4-26B-A4B-it", "google/gemma-4-E2B-it")
class Gemma4VisionAudioModel(MmprojModel):
Expand Down
7 changes: 3 additions & 4 deletions docs/speculative.md
Original file line number Diff line number Diff line change
Expand Up @@ -100,11 +100,10 @@ llama-server -m Qwen3-4B.gguf -md Qwen3-4B-DSpark.gguf \

`--spec-draft-n-max` is clamped to the draft model's trained block size.

`--spec-draft-conf-min P` truncates each drafted block at the first position whose predicted
acceptance (from the draft's confidence head, if present) falls below `P` (default 0 = disabled).
`--spec-draft-p-min P` also gates the DSpark confidence head: each drafted block is truncated at
the first position whose predicted acceptance falls below `P` (default 0 = disabled).

Currently only drafts with a Qwen3 backbone are supported; support for other backbones
(e.g. Gemma4) is planned.
DSpark drafts support multiple backbones; the backbone is detected from the checkpoint, and all of them convert and run the same way.

DSpark drafts exported in the [speculators](https://github.com/vllm-project/speculators) format
(for example [`RedHatAI/gemma-4-31B-it-speculator.dspark`](https://huggingface.co/RedHatAI/gemma-4-31B-it-speculator.dspark))
Expand Down
4 changes: 4 additions & 0 deletions gguf-py/gguf/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -4822,13 +4822,15 @@ class MODEL_TENSOR(IntEnum):
MODEL_TENSOR.TOKEN_EMBD,
MODEL_TENSOR.OUTPUT,
MODEL_TENSOR.OUTPUT_NORM,
MODEL_TENSOR.ROPE_FREQS,
MODEL_TENSOR.ATTN_NORM,
MODEL_TENSOR.ATTN_Q,
MODEL_TENSOR.ATTN_K,
MODEL_TENSOR.ATTN_V,
MODEL_TENSOR.ATTN_OUT,
MODEL_TENSOR.ATTN_Q_NORM,
MODEL_TENSOR.ATTN_K_NORM,
MODEL_TENSOR.ATTN_POST_NORM,
MODEL_TENSOR.ATTN_SINKS,
MODEL_TENSOR.ATTN_Q_A,
MODEL_TENSOR.ATTN_Q_B,
Expand All @@ -4850,6 +4852,7 @@ class MODEL_TENSOR(IntEnum):
MODEL_TENSOR.FFN_GATE,
MODEL_TENSOR.FFN_DOWN,
MODEL_TENSOR.FFN_UP,
MODEL_TENSOR.FFN_POST_NORM,
MODEL_TENSOR.FFN_GATE_INP,
MODEL_TENSOR.FFN_EXP_PROBS_B,
MODEL_TENSOR.FFN_GATE_EXP,
Expand All @@ -4858,6 +4861,7 @@ class MODEL_TENSOR(IntEnum):
MODEL_TENSOR.FFN_GATE_SHEXP,
MODEL_TENSOR.FFN_DOWN_SHEXP,
MODEL_TENSOR.FFN_UP_SHEXP,
MODEL_TENSOR.LAYER_OUT_SCALE,
MODEL_TENSOR.FC,
MODEL_TENSOR.ENC_OUTPUT_NORM,
MODEL_TENSOR.D2T,
Expand Down
83 changes: 73 additions & 10 deletions src/models/dflash.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,12 @@ void llama_model_dflash::load_arch_hparams(llama_model_loader & ml) {
hparams.rope_freq_scale_train_swa = hparams.rope_freq_scale_train;
}

// Gemma4 backbone: scaled token embeddings, kq_scale override and final-logit softcapping
hparams.f_final_logit_softcapping = 0.0f;
ml.get_key(LLM_KV_EMBEDDING_SCALE, hparams.f_embedding_scale, false);
ml.get_key(LLM_KV_ATTENTION_SCALE, hparams.f_attention_scale, false);
ml.get_key(LLM_KV_FINAL_LOGIT_SOFTCAPPING, hparams.f_final_logit_softcapping, false);

type = LLM_TYPE_UNKNOWN;
}

Expand All @@ -96,9 +102,6 @@ void llama_model_dflash::load_arch_tensors(llama_model_loader &) {
}

// DSpark = DFlash + a semi-autoregressive Markov head and Confidence head
//
// TODO: only Qwen3-style backbones are supported for now; other backbones (e.g. Gemma4)
// need their own conversion path and graph tweaks
const struct ggml_tensor * markov_meta = ml->get_tensor_meta("markov_w1.weight");
if (markov_meta) {
const int64_t dspark_markov_rank = markov_meta->ne[0];
Expand Down Expand Up @@ -170,6 +173,35 @@ void llama_model_dflash::load_arch_tensors(llama_model_loader &) {
// optional: reduced-vocab drafts ship their own, full-vocab drafts share the target's via ctx_other
output = create_tensor(tn(LLM_TENSOR_OUTPUT, "weight"), { n_embd, n_vocab_draft }, TENSOR_NOT_REQUIRED);

// Gemma4 backbone: marked by its scaled token embeddings (always written by the Gemma4 converter)
if (hparams.f_embedding_scale != 0.0f) {
for (int i = 0; i < n_layer; ++i) {
auto & layer = layers[i];

layer.attn_norm = create_tensor(tn(LLM_TENSOR_ATTN_NORM, "weight", i), { n_embd }, 0);

// no V projection: V = the K projection
layer.wq = create_tensor(tn(LLM_TENSOR_ATTN_Q, "weight", i), { n_embd, n_embd_head_k * n_head }, 0);
layer.wk = create_tensor(tn(LLM_TENSOR_ATTN_K, "weight", i), { n_embd, n_embd_k_gqa }, 0);
layer.wo = create_tensor(tn(LLM_TENSOR_ATTN_OUT, "weight", i), { n_embd_head_k * n_head, n_embd }, 0);

layer.attn_q_norm = create_tensor(tn(LLM_TENSOR_ATTN_Q_NORM, "weight", i), { n_embd_head_k }, 0);
layer.attn_k_norm = create_tensor(tn(LLM_TENSOR_ATTN_K_NORM, "weight", i), { n_embd_head_k }, 0);
layer.attn_post_norm = create_tensor(tn(LLM_TENSOR_ATTN_POST_NORM, "weight", i), { n_embd }, 0);

layer.ffn_norm = create_tensor(tn(LLM_TENSOR_FFN_NORM, "weight", i), { n_embd }, 0);
layer.ffn_gate = create_tensor(tn(LLM_TENSOR_FFN_GATE, "weight", i), { n_embd, n_ff }, 0);
layer.ffn_down = create_tensor(tn(LLM_TENSOR_FFN_DOWN, "weight", i), { n_ff, n_embd }, 0);
layer.ffn_up = create_tensor(tn(LLM_TENSOR_FFN_UP, "weight", i), { n_embd, n_ff }, 0);
layer.ffn_post_norm = create_tensor(tn(LLM_TENSOR_FFN_POST_NORM, "weight", i), { n_embd }, 0);

layer.out_scale = create_tensor(tn(LLM_TENSOR_LAYER_OUT_SCALE, "weight", i), { 1u }, TENSOR_NOT_REQUIRED);
layer.rope_freqs = create_tensor(tn(LLM_TENSOR_ROPE_FREQS, "weight", i), { n_embd_head_k/2 },
TENSOR_NOT_REQUIRED | (i != 0 ? TENSOR_DUPLICATED : 0));
}
return;
}

for (int i = 0; i < n_layer; ++i) {
auto & layer = layers[i];

Expand Down Expand Up @@ -350,6 +382,9 @@ static void build_dspark_markov_head(llm_graph_context & g, const llama_model &
// * token batch -> noise-block diffusion: attend over [committed, MASK...] to generate draft tokens
template <>
llama_model_dflash::graph<false>::graph(const llama_model & model, const llm_graph_params & params) : llm_graph_context(params) {
// Gemma4 backbone: marked by its scaled token embeddings (always written by the Gemma4 converter)
const bool is_gemma4 = hparams.f_embedding_scale != 0.0f;

const int64_t n_embd_head = hparams.n_embd_head_v();

GGML_ASSERT(n_embd_head == hparams.n_embd_head_k());
Expand All @@ -367,7 +402,10 @@ llama_model_dflash::graph<false>::graph(const llama_model & model, const llm_gra
inp_attn = build_attn_inp_kv();
}

const float kq_scale = 1.0f/sqrtf(float(n_embd_head));
const float kq_scale = hparams.f_attention_scale == 0.0f ? 1.0f/sqrtf(float(n_embd_head)) : hparams.f_attention_scale;

// the Gemma4 backbone uses GELU instead of SiLU
const llm_ffn_op_type ffn_act = is_gemma4 ? LLM_FFN_GELU : LLM_FFN_SILU;

// KV cache injection
if (ubatch.embd) {
Expand All @@ -385,14 +423,18 @@ llama_model_dflash::graph<false>::graph(const llama_model & model, const llm_gra
const auto & layer = model.layers[il];

ggml_tensor * Kcur = build_lora_mm(layer.wk, inp_g);
ggml_tensor * Vcur = build_lora_mm(layer.wv, inp_g);
// Gemma4 has no V projection: V = the K projection through a scale-less RMS norm
ggml_tensor * Vcur = is_gemma4 ? Kcur : build_lora_mm(layer.wv, inp_g);

Kcur = ggml_reshape_3d(ctx0, Kcur, n_embd_head, n_head_kv, n_tokens);
Vcur = ggml_reshape_3d(ctx0, Vcur, n_embd_head, n_head_kv, n_tokens);

Kcur = build_norm(Kcur, layer.attn_k_norm, NULL, LLM_NORM_RMS, il);
if (is_gemma4) {
Vcur = ggml_rms_norm(ctx0, Vcur, hparams.f_norm_rms_eps);
}
Kcur = ggml_rope_ext(
ctx0, Kcur, inp_pos, nullptr,
ctx0, Kcur, inp_pos, layer.rope_freqs,
n_rot, rope_type, n_ctx_orig, freq_base, freq_scale,
ext_factor, attn_factor, beta_fast, beta_slow
);
Expand Down Expand Up @@ -453,6 +495,9 @@ llama_model_dflash::graph<false>::graph(const llama_model & model, const llm_gra
ggml_tensor * inp_tokens = inp->tokens;

ggml_tensor * inpL = ggml_get_rows(ctx0, tok_embd, inp->tokens);
if (hparams.f_embedding_scale != 0.0f) {
inpL = ggml_scale(ctx0, inpL, hparams.f_embedding_scale);
}
cb(inpL, "inp_noise_embd", -1);

res->add_input(std::move(inp));
Expand All @@ -465,22 +510,26 @@ llama_model_dflash::graph<false>::graph(const llama_model & model, const llm_gra

ggml_tensor * Qcur = build_lora_mm(layer.wq, noise_norm);
ggml_tensor * Kcur = build_lora_mm(layer.wk, noise_norm);
ggml_tensor * Vcur = build_lora_mm(layer.wv, noise_norm);
// Gemma4 has no V projection: V = the K projection through a scale-less RMS norm
ggml_tensor * Vcur = is_gemma4 ? Kcur : build_lora_mm(layer.wv, noise_norm);

Qcur = ggml_reshape_3d(ctx0, Qcur, n_embd_head, n_head, n_tokens);
Kcur = ggml_reshape_3d(ctx0, Kcur, n_embd_head, n_head_kv, n_tokens);
Vcur = ggml_reshape_3d(ctx0, Vcur, n_embd_head, n_head_kv, n_tokens);

Qcur = build_norm(Qcur, layer.attn_q_norm, NULL, LLM_NORM_RMS, il);
Kcur = build_norm(Kcur, layer.attn_k_norm, NULL, LLM_NORM_RMS, il);
if (is_gemma4) {
Vcur = ggml_rms_norm(ctx0, Vcur, hparams.f_norm_rms_eps);
}

Qcur = ggml_rope_ext(
ctx0, Qcur, inp_pos, nullptr,
ctx0, Qcur, inp_pos, layer.rope_freqs,
n_rot, rope_type, n_ctx_orig, freq_base, freq_scale,
ext_factor, attn_factor, beta_fast, beta_slow
);
Kcur = ggml_rope_ext(
ctx0, Kcur, inp_pos, nullptr,
ctx0, Kcur, inp_pos, layer.rope_freqs,
n_rot, rope_type, n_ctx_orig, freq_base, freq_scale,
ext_factor, attn_factor, beta_fast, beta_slow
);
Expand All @@ -493,6 +542,11 @@ llama_model_dflash::graph<false>::graph(const llama_model & model, const llm_gra
? build_attn(inp_attn_iswa, layer.wo, NULL, NULL, Qcur, Kcur, Vcur, nullptr, nullptr, nullptr, kq_scale, il)
: build_attn(inp_attn, layer.wo, NULL, NULL, Qcur, Kcur, Vcur, nullptr, nullptr, nullptr, kq_scale, il);

if (layer.attn_post_norm) {
cur = build_norm(cur, layer.attn_post_norm, NULL, LLM_NORM_RMS, il);
cb(cur, "attn_post_norm", il);
}

ggml_tensor * ffn_inp = ggml_add(ctx0, cur, inpL);
cb(ffn_inp, "ffn_inp", il);

Expand All @@ -504,10 +558,19 @@ llama_model_dflash::graph<false>::graph(const llama_model & model, const llm_gra
layer.ffn_gate, NULL, layer.ffn_gate_s,
layer.ffn_down, NULL, layer.ffn_down_s,
NULL,
LLM_FFN_SILU, LLM_FFN_PAR, il);
ffn_act, LLM_FFN_PAR, il);
cb(cur, "ffn_out", il);

if (layer.ffn_post_norm) {
cur = build_norm(cur, layer.ffn_post_norm, NULL, LLM_NORM_RMS, il);
cb(cur, "ffn_post_norm", il);
}

cur = ggml_add(ctx0, cur, ffn_inp);

if (layer.out_scale) {
cur = ggml_mul(ctx0, cur, layer.out_scale);
}
cb(cur, "l_out", il);

inpL = cur;
Expand Down