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
1 change: 1 addition & 0 deletions conversion/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -286,6 +286,7 @@
"CogVLMForCausalLM": "cogvlm",
"DeepseekOCR2ForCausalLM": "deepseek",
"DeepseekOCRForCausalLM": "deepseek",
"DeepseekV4ForCausalLM": "deepseek",
"Dots3NoteForCausalLM": "dots3",
"Dots3NoteForConditionalGeneration": "dots3",
"DotsOCRForCausalLM": "dotsocr",
Expand Down
73 changes: 73 additions & 0 deletions conversion/deepseek.py
Original file line number Diff line number Diff line change
Expand Up @@ -578,6 +578,9 @@ def index_tensors(self, remote_hf_model_id: str | None = None) -> dict[str, Call
@classmethod
def filter_tensors(cls, item: tuple[str, Callable[[], Tensor]]) -> tuple[str, Callable[[], Tensor]] | None:
name, gen = item
if (name.startswith(("aligner.", "image_"))
or name.endswith(".ffn.gate.bias_vl")):
return None
if name.startswith("mtp."):
if not cls.mtp_only:
cls._skipped_mtp_tensors += 1
Expand Down Expand Up @@ -1018,3 +1021,73 @@ def set_gguf_parameters(self):

self.gguf_writer.add_block_size(self.hparams["dspark_block_size"])
self.gguf_writer.add_target_layers([layer + 1 for layer in self.hparams["dspark_target_layer_ids"]])


@ModelBase.register("DeepseekV4ForCausalLM")
@ModelBase.example("deepseek-ai/DeepSeek-V4-Flash-Vision-Exp")
class DeepseekV4FlashVisionModel(MmprojModel):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
assert self.hparams_vision is not None
# no preprocessor_config.json in the repo; normalization is (x/255 - 0.5) / 0.5
# ref: inference/image_processor.py (load_image)
self.preprocessor_config = {
"image_mean": [0.5, 0.5, 0.5],
"image_std": [0.5, 0.5, 0.5],
**self.preprocessor_config,
}

def get_vision_config(self) -> dict[str, Any] | None:
cfg = self.global_config
if cfg.get("vision_n_layers", 0) == 0:
raise ValueError("DeepseekV4FlashVisionModel requires vision_n_layers > 0 in the model config")
return {
"num_hidden_layers": cfg["vision_n_layers"],
"hidden_size": cfg["vision_dim"],
"num_attention_heads": cfg["vision_n_heads"],
"intermediate_size": cfg["vision_inter_dim"],
"patch_size": cfg["vision_patch_size"],
# dynamic resolution; only used for compat / warmup
"image_size": cfg["vision_patch_size"] * cfg["vision_downsample_ratio"] * 16,
"rope_theta": cfg.get("vision_rope_theta", 10000.0),
"downsample_ratio": cfg["vision_downsample_ratio"],
"min_pixels": cfg["vision_min_pixels"],
}

def set_gguf_parameters(self):
super().set_gguf_parameters()
assert self.hparams_vision is not None
self.gguf_writer.add_clip_projector_type(gguf.VisionProjectorType.DEEPSEEK4V)
# vision RMSNorm eps is the pytorch default, NOT the LLM's rms_norm_eps (1e-20)
# ref: inference/vision.py (RMSNorm)
self.gguf_writer.add_vision_attention_layernorm_eps(1e-6)
self.gguf_writer.add_vision_use_silu(True) # SwiGLU MLP
self.gguf_writer.add_vision_projector_scale_factor(self.hparams_vision["downsample_ratio"])
self.gguf_writer.add_vision_min_pixels(self.hparams_vision["min_pixels"])
# hardcoded on the C++ side (see PROJECTOR_TYPE_DEEPSEEK4V in clip.cpp)
# if future models use different values, add GGUF keys for those
assert self.global_config["vision_max_n_token"] == 384
assert self.global_config["vision_max_wh_ratio"] == 8

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

def modify_tensors(self, data_torch: Tensor, name: str, bid: int | None) -> Iterable[tuple[str, Tensor]]:
assert self.hparams_vision is not None
if name == "vision.patch_embed.proj.weight":
# nn.Linear over flattened (3, p, p) patches == conv2d weight
p = self.hparams_vision["patch_size"]
data_torch = data_torch.reshape(data_torch.shape[0], 3, p, p)

if ".mlp.w1." in name:
# fused SwiGLU gate+up
gate, up = data_torch.chunk(2, dim=0)
yield from super().modify_tensors(gate, name.replace("w1", "w1_gate"), bid)
yield from super().modify_tensors(up, name.replace("w1", "w1_up"), bid)
return

yield from super().modify_tensors(data_torch, name, bid)
10 changes: 10 additions & 0 deletions gguf-py/gguf/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -950,6 +950,9 @@ class MODEL_TENSOR(IntEnum):
V_RESMPL_PROJ = auto() # minicpmv
V_RESMPL_QUERY = auto() # minicpmv
V_TOK_EMBD_IMG_BREAK = auto() # pixtral
V_TOK_EMBD_IMG_START = auto() # deepseek4v
V_TOK_EMBD_IMG_END = auto() # deepseek4v
V_TOK_EMBD_IMG_PAD = auto() # deepseek4v
V_MM_PATCH_MERGER = auto() # mistral small 3.1
V_DS_NORM = auto() # qwen3vl
V_DS_FC1 = auto() # qwen3vl
Expand Down Expand Up @@ -1696,6 +1699,9 @@ class MODEL_TENSOR(IntEnum):
MODEL_TENSOR.V_RESMPL_PROJ: "resampler.proj",
MODEL_TENSOR.V_RESMPL_QUERY: "resampler.query",
MODEL_TENSOR.V_TOK_EMBD_IMG_BREAK: "v.token_embd.img_break", # pixtral
MODEL_TENSOR.V_TOK_EMBD_IMG_START: "v.token_embd.img_start", # deepseek4v
MODEL_TENSOR.V_TOK_EMBD_IMG_END: "v.token_embd.img_end", # deepseek4v
MODEL_TENSOR.V_TOK_EMBD_IMG_PAD: "v.token_embd.img_pad", # deepseek4v
MODEL_TENSOR.V_MM_PATCH_MERGER: "mm.patch_merger", # mistral small 3.1
MODEL_TENSOR.V_DS_NORM: "v.deepstack.{bid}.norm",
MODEL_TENSOR.V_DS_FC1: "v.deepstack.{bid}.fc1",
Expand Down Expand Up @@ -2030,6 +2036,9 @@ class MODEL_TENSOR(IntEnum):
MODEL_TENSOR.V_RESMPL_PROJ,
MODEL_TENSOR.V_RESMPL_QUERY,
MODEL_TENSOR.V_TOK_EMBD_IMG_BREAK,
MODEL_TENSOR.V_TOK_EMBD_IMG_START,
MODEL_TENSOR.V_TOK_EMBD_IMG_END,
MODEL_TENSOR.V_TOK_EMBD_IMG_PAD,
MODEL_TENSOR.V_MM_PATCH_MERGER,
MODEL_TENSOR.V_MM_MERGER_FC1,
MODEL_TENSOR.V_MM_MERGER_FC2,
Expand Down Expand Up @@ -5645,6 +5654,7 @@ class VisionProjectorType:
DOTS3NOTE_A = "dots3note_a" # audio
DEEPSEEKOCR = "deepseekocr"
DEEPSEEKOCR2 = "deepseekocr2"
DEEPSEEK4V = "deepseek4v"
LFM2A = "lfm2a" # audio
MUSIC_FLAMINGO = "musicflamingo" # audio
GLM4V = "glm4v"
Expand Down
23 changes: 23 additions & 0 deletions gguf-py/gguf/tensor_mapping.py
Original file line number Diff line number Diff line change
Expand Up @@ -1476,6 +1476,7 @@ class TensorNameMap:
## Vision encoder

MODEL_TENSOR.V_MMPROJ: (
"aligner.w{bid}", # deepseek4v (w1 -> mm.1, w2 -> mm.2)
"multi_modal_projector.linear_{bid}",
"mm_projector.proj.linear_{bid}", # Kimi-K2.5
"visual.merger.mlp.{bid}", # qwen2vl
Expand Down Expand Up @@ -1515,6 +1516,7 @@ class TensorNameMap:
),

MODEL_TENSOR.V_ENC_EMBD_PATCH: (
"vision.patch_embed.proj", # deepseek4v
"model.vision_tower.vision_model.embeddings.patch_embedding", # Granite4Vision
"vision_tower.vision_model.embeddings.patch_embedding",
"model.vision_tower.embeddings.patch_embedding", # minicpmv4_6
Expand Down Expand Up @@ -1570,6 +1572,7 @@ class TensorNameMap:

# TODO: I think these should all be moved to mapping_cfg?
MODEL_TENSOR.V_ENC_EMBD_IMGNL: (
"image_newline", # deepseek4v
"model.image_newline", # Deepseek-OCR, Granite4Vision
"vit.perceive.image_newline", # HunyuanVL
),
Expand All @@ -1580,6 +1583,7 @@ class TensorNameMap:
),

MODEL_TENSOR.V_ENC_ATTN_QKV: (
"vision.blocks.{bid}.attn.wqkv", # deepseek4v
"visual.blocks.{bid}.attn.qkv", # qwen3vl
"vision_tower.blocks.{bid}.attn.qkv", # dots.ocr
"vision_encoder.blocks.{bid}.attn.qkv", # dots3note
Expand Down Expand Up @@ -1667,6 +1671,7 @@ class TensorNameMap:
),

MODEL_TENSOR.V_ENC_INPUT_NORM: (
"vision.blocks.{bid}.norm1", # deepseek4v
"model.vision_tower.vision_model.encoder.layers.{bid}.layer_norm1", # Granite4Vision
"vision_tower.vision_model.encoder.layers.{bid}.layer_norm1",
"model.vision_tower.encoder.layers.{bid}.layer_norm1", # minicpmv4_6
Expand All @@ -1692,6 +1697,7 @@ class TensorNameMap:
),

MODEL_TENSOR.V_ENC_ATTN_O: (
"vision.blocks.{bid}.attn.wo", # deepseek4v
"model.vision_tower.vision_model.encoder.layers.{bid}.self_attn.out_proj", # Granite4Vision
"vision_tower.vision_model.encoder.layers.{bid}.self_attn.out_proj",
"model.vision_tower.encoder.layers.{bid}.self_attn.out_proj", # minicpmv4_6
Expand Down Expand Up @@ -1723,6 +1729,7 @@ class TensorNameMap:
),

MODEL_TENSOR.V_ENC_POST_ATTN_NORM: (
"vision.blocks.{bid}.norm2", # deepseek4v
"model.vision_tower.vision_model.encoder.layers.{bid}.layer_norm2", # Granite4Vision
"vision_tower.vision_model.encoder.layers.{bid}.layer_norm2",
"model.vision_tower.encoder.layers.{bid}.layer_norm2", # minicpmv4_6
Expand All @@ -1749,6 +1756,7 @@ class TensorNameMap:
),

MODEL_TENSOR.V_ENC_FFN_UP: (
"vision.blocks.{bid}.mlp.w1_up", # deepseek4v (split from fused w1)
"vision_encoder.blocks.{bid}.mlp.fc3", # dots3note
"model.vision_tower.vision_model.encoder.layers.{bid}.mlp.fc1", # Granite4Vision
"vision_tower.vision_model.encoder.layers.{bid}.mlp.fc1",
Expand All @@ -1775,6 +1783,7 @@ class TensorNameMap:
),

MODEL_TENSOR.V_ENC_FFN_GATE: (
"vision.blocks.{bid}.mlp.w1_gate", # deepseek4v (split from fused w1)
"vision_encoder.blocks.{bid}.mlp.fc1", # dots3note
"vision_tower.transformer.layers.{bid}.feed_forward.gate_proj", # pixtral-hf
"vision_encoder.transformer.layers.{bid}.feed_forward.w1", # pixtral
Expand All @@ -1784,6 +1793,7 @@ class TensorNameMap:
),

MODEL_TENSOR.V_ENC_FFN_DOWN: (
"vision.blocks.{bid}.mlp.w2", # deepseek4v
"vision_encoder.blocks.{bid}.mlp.fc2", # dots3note
"model.vision_tower.vision_model.encoder.layers.{bid}.mlp.fc2", # Granite4Vision
"vision_tower.vision_model.encoder.layers.{bid}.mlp.fc2",
Expand Down Expand Up @@ -1869,6 +1879,7 @@ class TensorNameMap:
),

MODEL_TENSOR.V_POST_NORM: (
"vision.norm", # deepseek4v
"model.vision_tower.vision_model.post_layernorm", # Granite4Vision
"vision_tower.vision_model.post_layernorm",
"model.vision_tower.post_layernorm", # minicpmv4_6
Expand Down Expand Up @@ -1960,6 +1971,18 @@ class TensorNameMap:
"v.token_embd.img_break", # for pixtral, this is a generated vector
),

MODEL_TENSOR.V_TOK_EMBD_IMG_START: (
"image_start", # deepseek4v
),

MODEL_TENSOR.V_TOK_EMBD_IMG_END: (
"image_end", # deepseek4v
),

MODEL_TENSOR.V_TOK_EMBD_IMG_PAD: (
"image_pad", # deepseek4v
),

MODEL_TENSOR.V_MM_PATCH_MERGER: (
"multi_modal_projector.patch_merger.merging_layer", # mistral small 3.1 - hf
"patch_merger.merging_layer", # mistral
Expand Down
1 change: 1 addition & 0 deletions tools/mtmd/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ add_library(mtmd
models/models.h
models/cogvlm.cpp
models/conformer.cpp
models/deepseek4v.cpp
models/dots3note.cpp
models/dotsocr.cpp
models/exaone4_5.cpp
Expand Down
28 changes: 26 additions & 2 deletions tools/mtmd/clip-impl.h
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,9 @@
#define TN_MM_MERGER_FC1 "mm.merger.fc1.%s" // minimax-m3 patch-merge MLP
#define TN_MM_MERGER_FC2 "mm.merger.fc2.%s"
#define TN_TOK_IMG_BREAK "v.token_embd.img_break" // pixtral
#define TN_TOK_IMG_START "v.token_embd.img_start" // deepseek4v
#define TN_TOK_IMG_END "v.token_embd.img_end" // deepseek4v
#define TN_TOK_IMG_PAD "v.token_embd.img_pad" // deepseek4v
#define TN_TOK_GLM_BOI "adapter.boi" // glm-edge (these embeddings are not in text model)
#define TN_TOK_GLM_EOI "adapter.eoi" // glm-edge (these embeddings are not in text model)
#define TN_DEEPSTACK_NORM "v.deepstack.%d.norm.%s" // qwen3vl deepstack
Expand Down Expand Up @@ -296,8 +299,8 @@

// hunyuanvl (shared GGUF tensor names)
#define TN_MM_PRE_NORM "mm.pre_norm.%s"
#define TN_TOK_IMG_BEGIN "mm.image_begin"
#define TN_TOK_IMG_END "mm.image_end"
#define TN_MM_IMG_BEGIN "mm.image_begin" // note: legacy name, new models should use v.token_embd.*
#define TN_MM_IMG_END "mm.image_end" // note: legacy name, new models should use v.token_embd.*

// deepseek-ocr
#define TN_SAM_POS_EMBD "v.sam.pos_embd.%s"
Expand Down Expand Up @@ -480,6 +483,7 @@ enum projector_type {
PROJECTOR_TYPE_DOTS3NOTE_A,
PROJECTOR_TYPE_DEEPSEEKOCR,
PROJECTOR_TYPE_DEEPSEEKOCR2,
PROJECTOR_TYPE_DEEPSEEK4V,
PROJECTOR_TYPE_LFM2A,
PROJECTOR_TYPE_GLM4V,
PROJECTOR_TYPE_YOUTUVL,
Expand Down Expand Up @@ -544,6 +548,7 @@ static std::map<projector_type, std::string> PROJECTOR_TYPE_NAMES = {
{ PROJECTOR_TYPE_DOTS3NOTE_A, "dots3note_a"},
{ PROJECTOR_TYPE_DEEPSEEKOCR, "deepseekocr"},
{ PROJECTOR_TYPE_DEEPSEEKOCR2, "deepseekocr2"},
{ PROJECTOR_TYPE_DEEPSEEK4V, "deepseek4v"},
{ PROJECTOR_TYPE_LFM2A, "lfm2a"},
{ PROJECTOR_TYPE_GLM4V, "glm4v"},
{ PROJECTOR_TYPE_YOUTUVL, "youtuvl"},
Expand Down Expand Up @@ -655,6 +660,9 @@ struct clip_image_f32 {
// appends a learned newline (or EOI) token after the image
// no model uses it now (Granite4 Vision moved to anyres), kept for future models
bool add_newline = false;
// deepseek4v: number of leading IMAGE_PAD embeddings, aligns IMAGE_START to the LLM compressor ratio
// depends on the chunk position, set at tokenize time (see mtmd_tokenizer::add_media)
int32_t lead_pad = 0;

// llava-next "anyres" tiling, used by Granite4 Vision
// the whole grid is encoded and assembled in a single graph
Expand Down Expand Up @@ -771,6 +779,22 @@ static inline void clip_anyres_unpad(int cur_w, int cur_h, int orig_w, int orig_
}
}

// deepseek4v: layout of the LLM token block built from the aligner grid
struct dsv4_block_layout {
int rows; // grid rows, padded to an even count
int row_len; // grid width + 1 newline
int pad_last; // trailing pads
int n_out; // total block size, including lead pads and the start/end sentinels
};
static inline dsv4_block_layout dsv4_get_block_layout(int n_llm_w, int n_llm_h, int lead_pad) {
dsv4_block_layout bl;
bl.rows = n_llm_h + (n_llm_h % 2);
bl.row_len = n_llm_w + 1;
bl.pad_last = (bl.rows / 2 * bl.row_len) % 2 * 2;
bl.n_out = lead_pad + 1 + bl.rows * bl.row_len + bl.pad_last + 1;
return bl;
}

//
// logging
//
Expand Down
9 changes: 9 additions & 0 deletions tools/mtmd/clip-model.h
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,10 @@ struct clip_hparams {
std::unordered_set<int32_t> wa_layer_indexes; // explicit layer indexes that use full attention (for irregular patterns like YoutuVL)
std::vector<int32_t> wa_pattern_mode; // mimovl: per-layer window-attention mode

// deepseek4v: resize solver caps the LLM token count of the aligner grid
int32_t dsv4_max_n_token = 0;
int32_t dsv4_max_wh_ratio = 0;

// deepseek-ocr (sam)
int32_t sam_n_layer = 0;
int32_t sam_n_head = 0;
Expand Down Expand Up @@ -724,6 +728,11 @@ struct clip_model {

// pixtral, glm4v
ggml_tensor * token_embd_img_break = nullptr;

// deepseek4v sentinel embeddings (image_newline is reused for IMAGE_NEW_LINE)
ggml_tensor * token_embd_img_start = nullptr;
ggml_tensor * token_embd_img_end = nullptr;
ggml_tensor * token_embd_img_pad = nullptr;
ggml_tensor * mm_patch_merger_w = nullptr;
ggml_tensor * mm_patch_merger_b = nullptr;

Expand Down
Loading
Loading