Skip to content

Commit ffc0a38

Browse files
forforever73CISC
andauthored
model : support step3-vl-10b (#21287)
* feat: support step3-vl-10b * use fused QKV && mapping tensor in tensor_mapping.py * guard hardcoded params and drop crop metadata * get understand_projector_stride from global config * img_u8_resize_bilinear_to_f32 move in step3vl class * Apply suggestions from code review Co-authored-by: Sigbjørn Skjæret <sigbjorn.skjaeret@scala.com> * fix the \r\n mess * add width and heads to MmprojModel.set_gguf_parameters --------- Co-authored-by: Sigbjørn Skjæret <sigbjorn.skjaeret@scala.com>
1 parent 508fc31 commit ffc0a38

12 files changed

Lines changed: 537 additions & 4 deletions

File tree

convert_hf_to_gguf.py

Lines changed: 85 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2219,10 +2219,10 @@ def set_gguf_parameters(self):
22192219
self.image_size = self.find_vparam(["image_size"])
22202220
self.gguf_writer.add_vision_image_size(self.image_size)
22212221
self.gguf_writer.add_vision_patch_size(self.find_vparam(["patch_size"]))
2222-
self.gguf_writer.add_vision_embedding_length(self.find_vparam(["hidden_size", "vt_hidden_size"]))
2222+
self.gguf_writer.add_vision_embedding_length(self.find_vparam(["hidden_size", "width", "vt_hidden_size"]))
22232223
self.gguf_writer.add_vision_feed_forward_length(self.find_vparam(["intermediate_size", "vt_intermediate_size"]))
22242224
self.gguf_writer.add_vision_block_count(self.find_vparam(self.n_block_keys))
2225-
self.gguf_writer.add_vision_head_count(self.find_vparam(["num_attention_heads", "num_heads", "vt_num_attention_heads"]))
2225+
self.gguf_writer.add_vision_head_count(self.find_vparam(["num_attention_heads", "num_heads", "heads", "vt_num_attention_heads"]))
22262226

22272227
# preprocessor config
22282228
image_mean = _MISTRAL_COMMON_DATASET_MEAN if self.is_mistral_format else self.preprocessor_config["image_mean"]
@@ -4949,6 +4949,73 @@ def modify_tensors(self, data_torch: Tensor, name: str, bid: int | None) -> Iter
49494949
yield from super().modify_tensors(data_torch, name, bid)
49504950

49514951

4952+
@ModelBase.register("StepVLForConditionalGeneration")
4953+
class Step3VLVisionModel(MmprojModel):
4954+
def __init__(self, *args, **kwargs):
4955+
super().__init__(*args, **kwargs)
4956+
assert self.hparams_vision is not None
4957+
4958+
if not self.hparams_vision.get("intermediate_size"):
4959+
hidden_size = self.hparams_vision.get("hidden_size") or self.hparams_vision.get("width") or 0
4960+
assert hidden_size > 0
4961+
mlp_ratio = float(self.hparams_vision.get("mlp_ratio", 8960 / 1536))
4962+
self.hparams_vision["intermediate_size"] = int(round(hidden_size * mlp_ratio))
4963+
4964+
self.preprocessor_config.setdefault("image_mean", list(_MISTRAL_COMMON_DATASET_MEAN))
4965+
self.preprocessor_config.setdefault("image_std", list(_MISTRAL_COMMON_DATASET_STD))
4966+
4967+
def set_gguf_parameters(self):
4968+
super().set_gguf_parameters()
4969+
assert self.hparams_vision is not None
4970+
4971+
projector_stride = int(self.global_config.get("understand_projector_stride", -1))
4972+
hidden_size = int(self.hparams_vision.get("hidden_size", self.hparams_vision.get("width", -1)))
4973+
num_layers = int(self.hparams_vision.get("num_hidden_layers", self.hparams_vision.get("layers", -1)))
4974+
assert (projector_stride, int(self.hparams_vision.get("image_size", -1)), hidden_size, num_layers) == (2, 728, 1536, 47), (
4975+
"current Step3-VL conversion path is only validated for Step3-VL-10B"
4976+
)
4977+
4978+
self.gguf_writer.add_clip_projector_type(gguf.VisionProjectorType.STEP3VL)
4979+
self.gguf_writer.add_vision_attention_layernorm_eps(float(self.hparams_vision.get("layer_norm_eps", 1e-5)))
4980+
self.gguf_writer.add_vision_projector_scale_factor(projector_stride ** 2)
4981+
# 3024 max resize comes from step3-vl-10b processing_step3.py.
4982+
self.gguf_writer.add_vision_preproc_image_size(3024)
4983+
4984+
def tensor_force_quant(self, name, new_name, bid, n_dims):
4985+
if ".position_embd." in new_name:
4986+
return gguf.GGMLQuantizationType.F32
4987+
return super().tensor_force_quant(name, new_name, bid, n_dims)
4988+
4989+
def modify_tensors(self, data_torch: Tensor, name: str, bid: int | None) -> Iterable[tuple[str, Tensor]]:
4990+
if name.startswith("model.") or name.startswith("lm_head."):
4991+
return
4992+
4993+
if name.startswith("vision_model.vit_downsampler"):
4994+
match = re.match(r"vision_model\.vit_downsampler(\d+)\.(weight|bias)", name)
4995+
if match is None:
4996+
raise ValueError(f"Unexpected Step3-VL projector tensor {name!r}")
4997+
4998+
proj_id = int(match.group(1)) - 1
4999+
suffix = f".{match.group(2)}"
5000+
yield (self.format_tensor_name(gguf.MODEL_TENSOR.V_MMPROJ, proj_id, suffix=suffix), data_torch)
5001+
return
5002+
5003+
if name == "vit_large_projector.weight":
5004+
yield (self.format_tensor_name(gguf.MODEL_TENSOR.V_MMPROJ_FC), data_torch)
5005+
return
5006+
5007+
if name.startswith("vision_model."):
5008+
if name == "vision_model.positional_embedding":
5009+
name += ".weight"
5010+
elif name.endswith(".gamma") and ".ls_" in name:
5011+
name = name.removesuffix(".gamma") + ".weight"
5012+
5013+
name = name.replace("attn.in_proj_weight", "attn.in_proj.weight")
5014+
name = name.replace("attn.in_proj_bias", "attn.in_proj.bias")
5015+
5016+
yield from super().modify_tensors(data_torch, name, bid)
5017+
5018+
49525019
@ModelBase.register("Qwen3VLForConditionalGeneration")
49535020
class Qwen3VLTextModel(Qwen3Model):
49545021
model_arch = gguf.MODEL_ARCH.QWEN3VL
@@ -4969,6 +5036,16 @@ def modify_tensors(self, data_torch: Tensor, name: str, bid: int | None) -> Iter
49695036
yield from super().modify_tensors(data_torch, name, bid)
49705037

49715038

5039+
@ModelBase.register("StepVLForConditionalGeneration")
5040+
class Step3VLTextModel(Qwen3Model):
5041+
model_arch = gguf.MODEL_ARCH.QWEN3
5042+
5043+
def modify_tensors(self, data_torch: Tensor, name: str, bid: int | None) -> Iterable[tuple[str, Tensor]]:
5044+
if name.startswith("vision_model.") or name.startswith("model.vision_model.") or name.startswith("vit_large_projector."):
5045+
return
5046+
yield from super().modify_tensors(data_torch, name, bid)
5047+
5048+
49725049
@ModelBase.register("Qwen3VLMoeForConditionalGeneration")
49735050
class Qwen3VLMoeTextModel(Qwen3MoeModel):
49745051
model_arch = gguf.MODEL_ARCH.QWEN3VLMOE
@@ -12994,6 +13071,12 @@ def get_model_architecture(hparams: dict[str, Any], model_type: ModelType) -> st
1299413071
# For non-hf Mamba and Mamba2 models
1299513072
arch = hparams["ssm_cfg"].get("layer", "Mamba") + "ForCausalLM"
1299613073

13074+
# Step3-VL keeps text config under text_config but uses a custom top-level architecture.
13075+
# For text conversion we route to a dedicated text-only class.
13076+
# TODO: refactor this later to avoid adding exception here
13077+
if model_type == ModelType.TEXT and arch == "StepVLForConditionalGeneration":
13078+
return arch
13079+
1299713080
# if "architectures" is found in the sub-config, use that instead
1299813081
if model_type == ModelType.TEXT and text_config.get("architectures") is not None:
1299913082
arch = text_config["architectures"][0]

gguf-py/gguf/constants.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -506,6 +506,7 @@ class VISION_PROJECTOR_TYPE(IntEnum):
506506
GEMMA3N = auto()
507507
GEMMA3 = auto()
508508
QWEN3VL = auto()
509+
STEP3VL = auto()
509510
COGVLM = auto()
510511

511512

@@ -987,6 +988,8 @@ class MODEL_TENSOR(IntEnum):
987988
VISION_PROJECTOR_TYPE.GLM_EDGE: "adapter",
988989
VISION_PROJECTOR_TYPE.MERGER: "qwen2vl_merger",
989990
VISION_PROJECTOR_TYPE.GEMMA3: "gemma3",
991+
VISION_PROJECTOR_TYPE.QWEN3VL: "qwen3vl_merger",
992+
VISION_PROJECTOR_TYPE.STEP3VL: "step3vl",
990993
}
991994

992995
TENSOR_NAMES: dict[MODEL_TENSOR, str] = {
@@ -4105,6 +4108,7 @@ class VisionProjectorType:
41054108
QWEN2VL = "qwen2vl_merger"
41064109
QWEN25VL = "qwen2.5vl_merger"
41074110
QWEN3VL = "qwen3vl_merger"
4111+
STEP3VL = "step3vl"
41084112
ULTRAVOX = "ultravox"
41094113
INTERNVL = "internvl"
41104114
QWEN2A = "qwen2a" # audio

gguf-py/gguf/tensor_mapping.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1406,6 +1406,7 @@ class TensorNameMap:
14061406
"siglip2.vision_model.embeddings.patch_embedding",
14071407
"vision_model.radio_model.model.patch_generator.embedder", # Nemotron Nano v2 VL
14081408
"model.vision_tower.patch_embedder.input_proj", # gemma4
1409+
"vision_model.conv1", # Step3-VL
14091410
),
14101411

14111412
MODEL_TENSOR.V_ENC_EMBD_NORM: (
@@ -1425,6 +1426,7 @@ class TensorNameMap:
14251426
"visual.embeddings.position_embedding", # glm4v
14261427
"vision_model.radio_model.model.patch_generator.pos_embed", # Nemotron Nano v2 VL
14271428
"model.vision_tower.patch_embedder.position_embedding_table", # gemma4
1429+
"vision_model.positional_embedding", # Step3-VL
14281430
),
14291431

14301432
MODEL_TENSOR.V_ENC_EMBD_IMGNL: (
@@ -1443,6 +1445,7 @@ class TensorNameMap:
14431445
"model.vision_model.transformer.layers.{bid}.self_attn.qkv_proj", # Deepseek-OCR CLIP
14441446
"vision_tower.encoder.blocks.{bid}.wqkv", # Kimi-K2.5
14451447
"vision_model.radio_model.model.blocks.{bid}.attn.qkv", # Nemotron Nano v2 VL
1448+
"vision_model.transformer.resblocks.{bid}.attn.in_proj", # Step3-VL
14461449
),
14471450

14481451
MODEL_TENSOR.V_ENC_ATTN_Q: (
@@ -1523,6 +1526,7 @@ class TensorNameMap:
15231526
"model.vision_model.transformer.layers.{bid}.layer_norm1", # Deepseek-OCR CLIP
15241527
"siglip2.vision_model.encoder.layers.{bid}.layer_norm1",
15251528
"vision_model.radio_model.model.blocks.{bid}.norm1", # Nemotron Nano v2 VL
1529+
"vision_model.transformer.resblocks.{bid}.ln_1", # Step3-VL
15261530
),
15271531

15281532
MODEL_TENSOR.V_ENC_ATTN_O: (
@@ -1543,6 +1547,7 @@ class TensorNameMap:
15431547
"siglip2.vision_model.encoder.layers.{bid}.self_attn.out_proj", # youtuvl
15441548
"vision_model.radio_model.model.blocks.{bid}.attn.proj", # Nemotron Nano v2 VL
15451549
"vision_model.model.layers.{bid}.self_attn.o_proj.linear", # gemma4
1550+
"vision_model.transformer.resblocks.{bid}.attn.out_proj", # Step3-VL
15461551
),
15471552

15481553
MODEL_TENSOR.V_ENC_POST_ATTN_NORM: (
@@ -1562,6 +1567,7 @@ class TensorNameMap:
15621567
"siglip2.vision_model.encoder.layers.{bid}.layer_norm2",
15631568
"vision_model.radio_model.model.blocks.{bid}.norm2", # Nemotron Nano v2 VL
15641569
"vision_model.model.layers.{bid}.pre_feedforward_layernorm", # gemma4
1570+
"vision_model.transformer.resblocks.{bid}.ln_2", # Step3-VL
15651571
),
15661572

15671573
MODEL_TENSOR.V_ENC_FFN_UP: (
@@ -1582,6 +1588,7 @@ class TensorNameMap:
15821588
"siglip2.vision_model.encoder.layers.{bid}.mlp.fc1",
15831589
"vision_model.radio_model.model.blocks.{bid}.mlp.fc1", # Nemotron Nano v2 VL
15841590
"vision_model.model.layers.{bid}.mlp.up_proj", # gemma4
1591+
"vision_model.transformer.resblocks.{bid}.mlp.c_fc", # Step3-VL
15851592
),
15861593

15871594
MODEL_TENSOR.V_ENC_FFN_GATE: (
@@ -1609,6 +1616,7 @@ class TensorNameMap:
16091616
"siglip2.vision_model.encoder.layers.{bid}.mlp.fc2",
16101617
"vision_model.radio_model.model.blocks.{bid}.mlp.fc2", # Nemotron Nano v2 VL
16111618
"vision_model.model.layers.{bid}.mlp.down_proj", # gemma4
1619+
"vision_model.transformer.resblocks.{bid}.mlp.c_proj", # Step3-VL
16121620
),
16131621

16141622
MODEL_TENSOR.V_ENC_ATTN_POST_NORM: (
@@ -1622,11 +1630,13 @@ class TensorNameMap:
16221630
MODEL_TENSOR.V_LAYER_SCALE_1: (
16231631
"vision_tower.vision_model.encoder.layers.{bid}.ls1", # InternVL
16241632
"model.vision_tower.encoder.layer.{bid}.lambda_1", # Intern-S1
1633+
"vision_model.transformer.resblocks.{bid}.ls_1", # Step3-VL
16251634
),
16261635

16271636
MODEL_TENSOR.V_LAYER_SCALE_2: (
16281637
"vision_tower.vision_model.encoder.layers.{bid}.ls2", # InternVL
16291638
"model.vision_tower.encoder.layer.{bid}.lambda_2", # Intern-S1
1639+
"vision_model.transformer.resblocks.{bid}.ls_2", # Step3-VL
16301640
),
16311641

16321642
MODEL_TENSOR.V_LAYER_OUT_SCALE: (
@@ -1639,6 +1649,7 @@ class TensorNameMap:
16391649
"vision_encoder.ln_pre", # pixtral
16401650
"vision_model.layernorm_pre", # llama4
16411651
"model.vision_model.pre_layrnorm", # Deepseek-OCR CLIP
1652+
"vision_model.ln_pre", # Step3-VL
16421653
),
16431654

16441655
MODEL_TENSOR.V_POST_NORM: (

tools/mtmd/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ add_library(mtmd
3131
models/pixtral.cpp
3232
models/qwen2vl.cpp
3333
models/qwen3vl.cpp
34+
models/step3vl.cpp
3435
models/siglip.cpp
3536
models/whisper-enc.cpp
3637
models/deepseekocr.cpp

tools/mtmd/clip-impl.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -242,6 +242,7 @@ enum projector_type {
242242
PROJECTOR_TYPE_GLM_EDGE,
243243
PROJECTOR_TYPE_QWEN2VL,
244244
PROJECTOR_TYPE_QWEN3VL,
245+
PROJECTOR_TYPE_STEP3VL,
245246
PROJECTOR_TYPE_GEMMA3,
246247
PROJECTOR_TYPE_GEMMA3NV,
247248
PROJECTOR_TYPE_GEMMA3NA,
@@ -284,6 +285,7 @@ static std::map<projector_type, std::string> PROJECTOR_TYPE_NAMES = {
284285
{ PROJECTOR_TYPE_QWEN2VL, "qwen2vl_merger"},
285286
{ PROJECTOR_TYPE_QWEN25VL, "qwen2.5vl_merger"},
286287
{ PROJECTOR_TYPE_QWEN3VL, "qwen3vl_merger"},
288+
{ PROJECTOR_TYPE_STEP3VL, "step3vl"},
287289
{ PROJECTOR_TYPE_GEMMA3, "gemma3"},
288290
{ PROJECTOR_TYPE_GEMMA3NV, "gemma3nv"},
289291
{ PROJECTOR_TYPE_GEMMA3NA, "gemma3na"},

tools/mtmd/clip-model.h

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,6 @@ struct clip_hparams {
7979

8080
float eps = 1e-6;
8181
float rope_theta = 0.0;
82-
8382
std::unordered_set<int32_t> vision_feature_layer;
8483
int32_t attn_window_size = 0;
8584
int32_t n_wa_pattern = 0;

tools/mtmd/clip.cpp

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -862,6 +862,10 @@ static ggml_cgraph * clip_image_build_graph(clip_ctx * ctx, const clip_image_f32
862862
{
863863
builder = std::make_unique<clip_graph_qwen3vl>(ctx, img);
864864
} break;
865+
case PROJECTOR_TYPE_STEP3VL:
866+
{
867+
builder = std::make_unique<clip_graph_step3vl>(ctx, img);
868+
} break;
865869
case PROJECTOR_TYPE_MINICPMV:
866870
{
867871
builder = std::make_unique<clip_graph_minicpmv>(ctx, img);
@@ -1337,6 +1341,17 @@ struct clip_model_loader {
13371341
LOG_WRN("%s: more info: https://github.com/ggml-org/llama.cpp/issues/16842\n\n", __func__);
13381342
}
13391343
} break;
1344+
case PROJECTOR_TYPE_STEP3VL:
1345+
{
1346+
hparams.n_merge = 4; // two stride-2 downsamplers after patching
1347+
get_u32(KEY_PROJ_SCALE_FACTOR, hparams.n_merge, false);
1348+
hparams.rope_theta = 10000.0f;
1349+
get_u32(KEY_PREPROC_IMAGE_SIZE, hparams.image_longest_edge, false);
1350+
if (hparams.image_longest_edge == 0) {
1351+
hparams.image_longest_edge = 3024;
1352+
}
1353+
hparams.warmup_image_size = hparams.image_size;
1354+
} break;
13401355
case PROJECTOR_TYPE_YOUTUVL:
13411356
{
13421357
hparams.n_merge = 2;
@@ -1769,6 +1784,14 @@ struct clip_model_loader {
17691784
model.mm_1_w = get_tensor(string_format(TN_LLAVA_PROJ, 2, "weight"));
17701785
model.mm_1_b = get_tensor(string_format(TN_LLAVA_PROJ, 2, "bias"));
17711786
} break;
1787+
case PROJECTOR_TYPE_STEP3VL:
1788+
{
1789+
model.mm_0_w = get_tensor(string_format(TN_LLAVA_PROJ, 0, "weight"));
1790+
model.mm_0_b = get_tensor(string_format(TN_LLAVA_PROJ, 0, "bias"), false);
1791+
model.mm_1_w = get_tensor(string_format(TN_LLAVA_PROJ, 1, "weight"));
1792+
model.mm_1_b = get_tensor(string_format(TN_LLAVA_PROJ, 1, "bias"), false);
1793+
model.mm_model_proj = get_tensor(string_format(TN_MM_PROJECTOR, "weight"));
1794+
} break;
17721795
case PROJECTOR_TYPE_YOUTUVL:
17731796
{
17741797
model.mm_input_norm_w = get_tensor(TN_MM_INP_NORM); // merger.ln_q (RMS norm)
@@ -2615,6 +2638,8 @@ int clip_n_output_tokens_x(const struct clip_ctx * ctx, struct clip_image_f32 *
26152638
case PROJECTOR_TYPE_HUNYUANOCR:
26162639
case PROJECTOR_TYPE_YOUTUVL:
26172640
return (img->nx / params.patch_size) / 2;
2641+
case PROJECTOR_TYPE_STEP3VL:
2642+
return img->nx / (params.patch_size * params.n_merge);
26182643
default:
26192644
break;
26202645
}
@@ -2632,6 +2657,8 @@ int clip_n_output_tokens_y(const struct clip_ctx * ctx, struct clip_image_f32 *
26322657
case PROJECTOR_TYPE_PADDLEOCR:
26332658
case PROJECTOR_TYPE_YOUTUVL:
26342659
return (img->ny / params.patch_size) / 2;
2660+
case PROJECTOR_TYPE_STEP3VL:
2661+
return img->ny / (params.patch_size * params.n_merge);
26352662
default:
26362663
break;
26372664
}
@@ -2702,6 +2729,12 @@ int clip_n_output_tokens(const struct clip_ctx * ctx, struct clip_image_f32 * im
27022729
int y_patch = img->ny / (params.patch_size * 2);
27032730
n_patches = x_patch * y_patch;
27042731
} break;
2732+
case PROJECTOR_TYPE_STEP3VL:
2733+
{
2734+
int x_patch = img->nx / (params.patch_size * params.n_merge);
2735+
int y_patch = img->ny / (params.patch_size * params.n_merge);
2736+
n_patches = x_patch * y_patch;
2737+
} break;
27052738
case PROJECTOR_TYPE_GEMMA3:
27062739
case PROJECTOR_TYPE_GEMMA4V:
27072740
case PROJECTOR_TYPE_IDEFICS3:
@@ -3004,6 +3037,18 @@ bool clip_image_batch_encode(clip_ctx * ctx, const int n_threads, const clip_ima
30043037

30053038
set_input_i32("positions", positions);
30063039
} break;
3040+
case PROJECTOR_TYPE_STEP3VL:
3041+
{
3042+
std::vector<int32_t> pos_data(n_pos);
3043+
for (int i = 0; i < n_pos; i++) {
3044+
pos_data[i] = i / pos_w;
3045+
}
3046+
set_input_i32("pos_h", pos_data);
3047+
for (int i = 0; i < n_pos; i++) {
3048+
pos_data[i] = i % pos_w;
3049+
}
3050+
set_input_i32("pos_w", pos_data);
3051+
} break;
30073052
case PROJECTOR_TYPE_PADDLEOCR:
30083053
{
30093054
const int merge_ratio = hparams.n_merge;
@@ -3358,6 +3403,8 @@ int clip_n_mmproj_embd(const struct clip_ctx * ctx) {
33583403
case PROJECTOR_TYPE_QWEN3VL:
33593404
// main path + deepstack paths
33603405
return ctx->model.mm_1_b->ne[0] * (1 + ctx->model.n_deepstack_layers);
3406+
case PROJECTOR_TYPE_STEP3VL:
3407+
return ctx->model.mm_model_proj->ne[1];
33613408
case PROJECTOR_TYPE_GEMMA3:
33623409
case PROJECTOR_TYPE_GEMMA3NV:
33633410
return ctx->model.mm_input_proj_w->ne[0];

tools/mtmd/models/models.h

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,11 @@ struct clip_graph_qwen3vl : clip_graph {
3333
ggml_cgraph * build() override;
3434
};
3535

36+
struct clip_graph_step3vl : clip_graph {
37+
clip_graph_step3vl(clip_ctx * ctx, const clip_image_f32 & img) : clip_graph(ctx, img) {}
38+
ggml_cgraph * build() override;
39+
};
40+
3641
struct clip_graph_youtuvl : clip_graph {
3742
clip_graph_youtuvl(clip_ctx * ctx, const clip_image_f32 & img) : clip_graph(ctx, img) {}
3843
ggml_cgraph * build() override;

0 commit comments

Comments
 (0)