Spangler3000 commited on
Commit
21532ed
·
verified ·
1 Parent(s): 8483c02

Add files using upload-large-folder tool

Browse files
This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. .eval_results/lhtb.yaml +9 -0
  2. .eval_results/minimax-m3.yaml +94 -0
  3. .gitattributes +2 -0
  4. README.md +144 -0
  5. added_tokens.json +63 -0
  6. chat_template.jinja +247 -0
  7. chat_template.json +3 -0
  8. config.json +0 -0
  9. configuration_minimax_m3_vl.py +111 -0
  10. figures/benchmark.jpeg +3 -0
  11. figures/efficiency_gqa_vs_msa.png +0 -0
  12. figures/logo.svg +16 -0
  13. generation_config.json +8 -0
  14. image_processor.py +223 -0
  15. model-00001-of-00059.safetensors +3 -0
  16. model-00003-of-00059.safetensors +3 -0
  17. model-00005-of-00059.safetensors +3 -0
  18. model-00006-of-00059.safetensors +3 -0
  19. model-00009-of-00059.safetensors +3 -0
  20. model-00011-of-00059.safetensors +3 -0
  21. model-00012-of-00059.safetensors +3 -0
  22. model-00017-of-00059.safetensors +3 -0
  23. model-00018-of-00059.safetensors +3 -0
  24. model-00020-of-00059.safetensors +3 -0
  25. model-00024-of-00059.safetensors +3 -0
  26. model-00025-of-00059.safetensors +3 -0
  27. model-00029-of-00059.safetensors +3 -0
  28. model-00032-of-00059.safetensors +3 -0
  29. model-00034-of-00059.safetensors +3 -0
  30. model-00037-of-00059.safetensors +3 -0
  31. model-00038-of-00059.safetensors +3 -0
  32. model-00041-of-00059.safetensors +3 -0
  33. model-00042-of-00059.safetensors +3 -0
  34. model-00044-of-00059.safetensors +3 -0
  35. model-00047-of-00059.safetensors +3 -0
  36. model-00048-of-00059.safetensors +3 -0
  37. model-00050-of-00059.safetensors +3 -0
  38. model-00053-of-00059.safetensors +3 -0
  39. model-00056-of-00059.safetensors +3 -0
  40. model-00057-of-00059.safetensors +3 -0
  41. model-00059-of-00059.safetensors +3 -0
  42. model.safetensors.index.json +0 -0
  43. preprocessor_config.json +27 -0
  44. processing_minimax.py +254 -0
  45. processor_config.json +63 -0
  46. special_tokens_map.json +16 -0
  47. tokenizer.json +3 -0
  48. tokenizer_config.json +13 -0
  49. video_preprocessor_config.json +32 -0
  50. video_processor.py +208 -0
.eval_results/lhtb.yaml ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
 
1
+ - dataset:
2
+ id: IntelligenceLab/Long-Horizon-Terminal-Bench
3
+ task_id: lhtb
4
+ value: 38.5
5
+ date: '2026-07-16'
6
+ source:
7
+ url: https://zli12321.github.io/LHTB/leaderboard.html
8
+ name: LHTB leaderboard
9
+ notes: "mean reward x100 over 46 tasks (partial credit); [email protected]=3/46; official LHTB Harbor harness"
.eval_results/minimax-m3.yaml ADDED
@@ -0,0 +1,94 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Evaluation results for MiniMaxAI/MiniMax-M3
2
+ # Extracted from the model card benchmark graph (figures/benchmark.jpeg)
3
+ # https://huggingface.co/MiniMaxAI/MiniMax-M3
4
+ # Paper: https://arxiv.org/abs/2606.13392
5
+
6
+ # ---------------------------------------------------------------------------
7
+ # Coding
8
+ # ---------------------------------------------------------------------------
9
+
10
+ # SWE-Bench Verified - 80.5
11
+ - dataset:
12
+ id: SWE-bench/SWE-bench_Verified
13
+ task_id: swe_bench_%_resolved
14
+ value: 80.5
15
+ source:
16
+ url: https://huggingface.co/MiniMaxAI/MiniMax-M3
17
+ name: MiniMax-M3 model card
18
+ notes: "Evaluated on internal infrastructure using Claude Code as the scaffolding. Each test was run 4 times and the average was taken."
19
+
20
+ # SWE-Bench Pro - 59.0
21
+ - dataset:
22
+ id: ScaleAI/SWE-bench_Pro
23
+ task_id: SWE_Bench_Pro
24
+ value: 59.0
25
+ source:
26
+ url: https://huggingface.co/MiniMaxAI/MiniMax-M3
27
+ name: MiniMax-M3 model card
28
+ notes: "Evaluated on internal infrastructure using Claude Code as the scaffolding. Testing logic is aligned with the official evaluation."
29
+
30
+
31
+ # ---------------------------------------------------------------------------
32
+ # Multimodal
33
+ # ---------------------------------------------------------------------------
34
+
35
+ # MMMU-Pro - 78.1
36
+ # MMMU-Pro defines three tasks: mmmu_pro_vision, mmmu_pro_standard_4_options,
37
+ # mmmu_pro_standard_10_options. The model card reports a single "MMMU-Pro"
38
+ # score without specifying the exact variant. We map it to the standard
39
+ # 10-options task as the most common updated benchmark configuration.
40
+ - dataset:
41
+ id: MMMU/MMMU_Pro
42
+ task_id: mmmu_pro_standard_10_options
43
+ value: 78.1
44
+ source:
45
+ url: https://huggingface.co/MiniMaxAI/MiniMax-M3
46
+ name: MiniMax-M3 model card
47
+ notes: "MMMU-Pro score extracted from the model card benchmark graph. The exact task variant (vision, standard 4-options, or standard 10-options) is not explicitly stated."
48
+
49
+ # Video-MME (w/ sub) - 85.4
50
+ # Mapped to Video-MME-v2, the registered successor benchmark on the Hub.
51
+ - dataset:
52
+ id: MME-Benchmarks/Video-MME-v2
53
+ task_id: video-mme-v2
54
+ value: 85.4
55
+ source:
56
+ url: https://huggingface.co/MiniMaxAI/MiniMax-M3
57
+ name: MiniMax-M3 model card
58
+ notes: "Model card reports 'VideoMME (w/ sub)'. Mapped to the closest registered benchmark on the Hub, Video-MME-v2."
59
+
60
+ # ---------------------------------------------------------------------------
61
+ # Cowork
62
+ # ---------------------------------------------------------------------------
63
+
64
+ # Claw-Eval - 74.5
65
+ # Claw-Eval defines three tasks: general, multimodal, multi_turn. The model card
66
+ # reports a single overall score, so it is mapped to the 'general' task.
67
+ - dataset:
68
+ id: claw-eval/Claw-Eval
69
+ task_id: general
70
+ value: 74.5
71
+ source:
72
+ url: https://huggingface.co/MiniMaxAI/MiniMax-M3
73
+ name: MiniMax-M3 model card
74
+ notes: "Model card reports a single 'Claw-Eval' score. Mapped to the 'general' task (overall); the exact task split is not specified."
75
+
76
+ # Apex-Agents - 27.7
77
+ - dataset:
78
+ id: mercor/apex-agents
79
+ task_id: apex-agents
80
+ value: 27.7
81
+ source:
82
+ url: https://huggingface.co/MiniMaxAI/MiniMax-M3
83
+ name: MiniMax-M3 model card
84
+ notes: "Evaluated on the apex-agents benchmark."
85
+
86
+ # YC-Bench - 2.1M (final assets in fund, monetary metric)
87
+ - dataset:
88
+ id: collinear-ai/yc-bench
89
+ task_id: medium
90
+ value: 2100000
91
+ source:
92
+ url: https://huggingface.co/MiniMaxAI/MiniMax-M3
93
+ name: MiniMax-M3 model card
94
+ notes: "Model card reports 2.1M (monetary value, final assets fund). The benchmark's 'medium' task is used as the overall evaluation. Metric is monetary, not percentage-based."
.gitattributes CHANGED
@@ -33,3 +33,5 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
 
 
 
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
36
+ figures/benchmark.jpeg filter=lfs diff=lfs merge=lfs -text
37
+ tokenizer.json filter=lfs diff=lfs merge=lfs -text
README.md ADDED
@@ -0,0 +1,144 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ base_model: MiniMaxAI/MiniMax-M3
3
+ library_name: mlx
4
+ pipeline_tag: text-generation
5
+ tags:
6
+ - mlx
7
+ - minimax
8
+ - mixture-of-experts
9
+ - mixed-precision
10
+ - quantization
11
+ - thundermlx
12
+ license: other
13
+ license_name: minimax
14
+ ---
15
+
16
+ # MiniMax-M3 Mixed-4.5bit MLX — the anti-overthinking quant
17
+
18
+ A **mixed-precision MLX quantization of MiniMax-M3** (428B parameters, 23B
19
+ active) that puts precision where decisions are made instead of spreading it
20
+ evenly. Built for and served by
21
+ [ThunderMLX](https://github.com/jonathan308/ThunderMLX), a 2-Mac pipeline
22
+ serving stack for Apple Silicon.
23
+
24
+ **TL;DR:** at +45 GB over the standard flat 4-bit (270 vs 225 GB), this quant
25
+ closes ~28% of the entire fidelity gap to the bf16 model, cuts reasoning-loop
26
+ "doom spirals" by 42–60%, eliminates 92% of hesitation markers, ships complete
27
+ agentic artifacts instead of drafting them inside thinking — and finishes real
28
+ tasks **15% faster in wall time** despite ~12% slower raw decode, because it
29
+ stops second-guessing itself.
30
+
31
+ ## Why: flat 4-bit quantization causes overthinking
32
+
33
+ Running MiniMax-M3 4-bit in agentic use, we kept hitting a failure family:
34
+ thinking spirals that re-analyze the same paragraph with mutating wording,
35
+ hesitation cascades ("wait… actually… let me reconsider"), and a stubborn
36
+ habit of drafting entire code artifacts inside the thinking block while
37
+ ignoring steering. Following arXiv 2606.00206 (quantization inflates
38
+ hesitation-marker probabilities at high-entropy positions), we first shipped a
39
+ runtime logit-penalty guard — it helped, but treated the symptom.
40
+
41
+ The cause turned out to be *where* flat quantization spends its error budget.
42
+ Rounding noise in a handful of small, decision-critical modules flips discrete
43
+ choices: which experts fire, which KV blocks sparse attention reads, and which
44
+ token wins the final logit race. This quant fixes those modules directly.
45
+
46
+ ## The recipe
47
+
48
+ | Tier | Modules | Precision | Rationale |
49
+ |---|---|---|---|
50
+ | Decision | lm_head, all 57 MoE router gates, sparse-attention indexer projections | **8-bit / g64** | rounding noise here flips discrete choices — the literal overthinking mechanism |
51
+ | Every-token | embeddings, all attention projections, dense-MLP layers | **6-bit / g64** | error compounds across all 60 layers with no routing dilution |
52
+ | Bulk | all 129-expert fused MoE tensors | **4-bit / g32** | halved group size halves in-group rounding error; the cheapest quality lever on 96% of the weights |
53
+ | Native | vision tower, norms (bf16), e_score_correction_bias (f32) | untouched | matches upstream |
54
+
55
+ Effective average: ~4.8 bits/weight. Identical tensor names and MLX affine
56
+ format to the standard 4-bit conversion — **loads anywhere the flat 4-bit
57
+ loads**, no code changes.
58
+
59
+ ## Benchmarks
60
+
61
+ ### Distribution fidelity (teacher-forced EAR vs a bf16-grade reference, ~10k positions)
62
+
63
+ EAR = per-position overlap between the quant's and the reference model's
64
+ next-token distributions (metric from arXiv 2605.02404), normalized, higher
65
+ is better. Reference = the bf16 checkpoint itself (experts at lossless 8-bit),
66
+ evaluated with a layer-streaming pass.
67
+
68
+ | Quant | Size | EAR mean | Worst-5% positions |
69
+ |---|---|---|---|
70
+ | flat 4-bit / g64 | 225 GB | 0.8747 | 0.5236 |
71
+ | same-budget control (extra bits spread across bulk experts) | 268 GB | 0.8806 | 0.5493 |
72
+ | **this quant** | 270 GB | **0.9103** | **0.6656** |
73
+
74
+ The control experiment is the point: an equal-size quant that spends its extra
75
+ bits on bulk experts recovers ~5% of the gap to bf16. Spending the same bits
76
+ on the decision path recovers **~28%** — and **~30% at the hard-position tail**
77
+ where reasoning behavior lives. Where the bits go matters far more than how
78
+ many.
79
+
80
+ ### Behavior (identical prompts and seeds vs flat 4-bit, guard disabled)
81
+
82
+ | Suite | flat 4-bit | this quant |
83
+ |---|---|---|
84
+ | Graded tasks — accuracy | 100% | **100%** |
85
+ | Graded — avg thinking tokens | 176 | **121 (−31%)** |
86
+ | Graded — hesitation markers/run | 0.60 | **0.05 (−92%)** |
87
+ | Graded — avg wall time | 8.0 s | **6.8 s (−15%)** |
88
+ | Loop probes (3 seeds) — avg thinking tokens | 1992 | **1159 (−42%)** |
89
+ | Loop probes — hesitation markers | 28.9 | **7.7 (−73%)** |
90
+
91
+ Ungoverned, this quant out-behaves the flat 4-bit running its most aggressive
92
+ anti-overthinking logit penalty. On the flagship two-turn agentic test (build
93
+ a complete single-file game, then steer), it plans in ~1k characters of
94
+ thinking and ships a complete 46.8k-character working artifact in the answer —
95
+ the flat 4-bit drafted the entire artifact inside its thinking block and
96
+ resisted steering. Long thinking is preserved where it's warranted: hard
97
+ constraint-solving still gets ~4k tokens of *forward-moving* reasoning
98
+ (2.3% repeated-phrase churn vs >10% in true spirals).
99
+
100
+ ### Speed (2-Mac ThunderMLX pipeline, Thunderbolt RDMA, 38/22 layer split)
101
+
102
+ | Metric | flat 4-bit | this quant |
103
+ |---|---|---|
104
+ | Decode, short context | ~28 tok/s | 23–26 tok/s |
105
+ | Decode @ 70k context | ~27–29 tok/s | 23.8 tok/s (no depth collapse) |
106
+ | Prefill @ 70k | — | 342 tok/s |
107
+ | TTFT (warm) | ~1.4 s | ~1.4 s (unchanged) |
108
+
109
+ The ~12% decode tax is repaid with interest on real tasks by shorter,
110
+ non-redundant thinking (see wall times above).
111
+
112
+ ## Serving
113
+
114
+ Built for [ThunderMLX](https://github.com/jonathan308/ThunderMLX) across two
115
+ Apple Silicon Macs (tested: Mac Studio + MacBook Pro, 38/22 pipeline split,
116
+ ~187 GB + ~96 GB wired). Any MLX stack that serves the standard 4-bit
117
+ conversion can load this model unchanged — same tensor names, same config
118
+ schema, per-path quantization overrides declared in `config.json`.
119
+
120
+ ## Reproduce / adapt
121
+
122
+ The converter, verification suite, and EAR evaluator are open source in the
123
+ ThunderMLX repo (`ops/quant/`):
124
+
125
+ - `m3_mixed_quant.py` — streaming mixed-precision converter: plan pass with a
126
+ name-set parity gate, per-expert rebuild of fused MoE tensors, incremental
127
+ 5 GB shards, ~15 GB peak memory while converting an 854 GB checkpoint.
128
+ - `ear_eval.py` / `ear_compare.py` — layer-streaming EAR evaluator: exact
129
+ next-token distributions from models far larger than RAM, including the
130
+ bf16 reference itself.
131
+
132
+ Two upstream findings the tooling works around, relevant to anyone quantizing
133
+ very large MoE models with MLX: (1) kernels evaluated on tensors above ~2³¹
134
+ elements can silently corrupt output — fused MoE expert tensors are exactly
135
+ that size, so the converter rebuilds them per-expert; (2) GPU kernels fed
136
+ directly from memory-mapped files on slow external drives stall past the
137
+ Metal watchdog — the converter materializes on the CPU stream first.
138
+
139
+ ## Acknowledgements
140
+
141
+ - MiniMax for MiniMax-M3.
142
+ - arXiv 2606.00206 (quantization-induced overthinking) for the mechanism, and
143
+ arXiv 2605.02404 (statistically-lossless quantization) for the EAR metric.
144
+ - The MLX team — this entire pipeline runs on MLX.
added_tokens.json ADDED
@@ -0,0 +1,63 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "]!p~[": 200000,
3
+ "<fim_prefix>": 200001,
4
+ "<fim_middle>": 200002,
5
+ "<fim_suffix>": 200003,
6
+ "<fim_pad>": 200004,
7
+ "<reponame>": 200005,
8
+ "<filename>": 200006,
9
+ "<gh_stars>": 200007,
10
+ "<issue_start>": 200008,
11
+ "<issue_comment>": 200009,
12
+ "<issue_closed>": 200010,
13
+ "<jupyter_start>": 200011,
14
+ "<jupyter_text>": 200012,
15
+ "<jupyter_code>": 200013,
16
+ "<jupyter_output>": 200014,
17
+ "<empty_output>": 200015,
18
+ "<commit_before>": 200016,
19
+ "<commit_msg>": 200017,
20
+ "<commit_after>": 200018,
21
+ "]~b]": 200019,
22
+ "[e~[": 200020,
23
+ "]!d~[": 200021,
24
+ "<function_call>": 200022,
25
+ "<code_interpreter>": 200023,
26
+ "]<]speech[>[": 200024,
27
+ "]<]image[>[": 200025,
28
+ "]<]video[>[": 200026,
29
+ "]<]start of speech[>[": 200027,
30
+ "]<]end of speech[>[": 200028,
31
+ "]<]start of image[>[": 200029,
32
+ "]<]end of image[>[": 200030,
33
+ "]<]start of video[>[": 200031,
34
+ "]<]end of video[>[": 200032,
35
+ "]<]vision pad[>[": 200033,
36
+ "]~!b[": 200034,
37
+ "<jupyter_error>": 200035,
38
+ "<add_file>": 200036,
39
+ "<delete_file>": 200037,
40
+ "<rename_file>": 200038,
41
+ "<edit_file>": 200039,
42
+ "<commit_message>": 200040,
43
+ "<empty_source_file>": 200041,
44
+ "<repo_struct>": 200042,
45
+ "<code_context>": 200043,
46
+ "<file_content>": 200044,
47
+ "<source_files>": 200045,
48
+ "<pr_start>": 200046,
49
+ "<review_comment>": 200047,
50
+ "<filepath>": 200048,
51
+ "<file_sep>": 200049,
52
+ "<think>": 200050,
53
+ "</think>": 200051,
54
+ "<tool_call>": 200052,
55
+ "</tool_call>": 200053,
56
+ "]<]frame[>[": 200054,
57
+ "]<]start of frame[>[": 200055,
58
+ "]<]end of frame[>[": 200056,
59
+ "<|content_altered_placeholder|>": 200057,
60
+ "]<]minimax[>[": 200058,
61
+ "<mm:think>": 200059,
62
+ "</mm:think>": 200060
63
+ }
chat_template.jinja ADDED
@@ -0,0 +1,247 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {# ---------- special token variables ---------- #}
2
+ {%- set ns_token = ']<]minimax[>[' -%}
3
+ {%- set bod_token = ']~!b[' -%}
4
+ {%- set bos_token = ']~b]' -%}
5
+ {%- set eos_token = '[e~[' -%}
6
+ {%- set toolcall_begin_token = ns_token ~ '<tool_call>' -%}
7
+ {%- set toolcall_end_token = ns_token ~ '</tool_call>' -%}
8
+ {%- set think_begin_token = '<mm:think>' -%}
9
+ {%- set think_end_token = '</mm:think>' -%}
10
+ {%- set image_token = ']<]image[>[' -%}
11
+ {%- set video_token = ']<]video[>[' -%}
12
+ {#- Thinking mode: "enabled" / "disabled" / "adaptive" / not defined -#}
13
+ {#- Recursive XML renderer for tool_call arguments ======================== -#}
14
+ {#- None values are intentionally skipped in mapping iteration so that
15
+ `<key>null</key>` (which would round-trip to the literal string "null")
16
+ never appears in the rendered tool_call. The convention is: omit the
17
+ field entirely. The top-level `_args` loop applies the same rule.
18
+ The `val is none` branch below is a safety net only — upstream cleaning
19
+ (drop_none_in_tool_arguments) should ensure no None ever reaches here. -#}
20
+ {%- macro to_xml(val, ns) -%}
21
+ {%- if val is mapping -%}
22
+ {%- for k, v in val.items() if v is not none -%}
23
+ {{ ns }}<{{ k }}>{{ to_xml(v, ns) }}{{ ns }}</{{ k }}>
24
+ {%- endfor -%}
25
+ {%- elif val is iterable and val is not string -%}
26
+ {%- for item in val -%}
27
+ {{ ns }}<item>{{ to_xml(item, ns) }}{{ ns }}</item>
28
+ {%- endfor -%}
29
+ {%- elif val is none -%}
30
+ {#- Should be unreachable when upstream cleaning is applied. -#}
31
+ {%- elif val is boolean -%}
32
+ {{ val | tojson }}
33
+ {%- else -%}
34
+ {{ val }}
35
+ {%- endif -%}
36
+ {%- endmacro -%}
37
+ {#- Tool Rendering Functions ============================================== -#}
38
+ {%- macro render_tool_namespace(namespace_name, tool_list) -%}
39
+ {%- for tool in tool_list -%}
40
+ <tool>{{ tool.function | tojson(ensure_ascii=False) }}</tool>
41
+ {% endfor -%}
42
+ {%- endmacro -%}
43
+ {%- macro visible_text(content) -%}
44
+ {%- if content is string -%}
45
+ {{ content }}
46
+ {%- elif content is iterable and content is not mapping -%}
47
+ {%- for item in content -%}
48
+ {%- if item is mapping and item.type == 'text' -%}
49
+ {{- item.text }}
50
+ {%- elif item is mapping and item.type == 'image' -%}
51
+ {{- image_token }}
52
+ {%- elif item is mapping and item.type == 'video' -%}
53
+ {{- video_token}}
54
+ {%- elif item is string -%}
55
+ {{- item }}
56
+ {%- endif -%}
57
+ {%- endfor -%}
58
+ {%- elif content is none -%}
59
+ {{- '' }}
60
+ {%- else -%}
61
+ {{- content }}
62
+ {%- endif -%}
63
+ {%- endmacro -%}
64
+ {#- System Message Construction ============================================ -#}
65
+ {%- macro build_system_message(system_message) -%}
66
+ {%- if system_message and system_message.content -%}
67
+ {{- visible_text(system_message.content) }}
68
+ {%- else -%}
69
+ {{- 'Your model version is MiniMax-M3, developed by MiniMax. Knowledge cutoff: January 2026. Founded in early 2022, MiniMax is a global AI foundation model company committed to advancing the frontiers of AI towards AGI.' }}
70
+ {%- endif -%}
71
+
72
+ {#- Thinking mode instructions -#}
73
+ {{- '\n\n<thinking_instructions>\n' }}
74
+ {{- 'You have a thinking capability that allows you to reason step by step before responding. When thinking is enabled, wrap your reasoning in ' ~ think_begin_token ~ think_end_token ~ ' tags before your response. When thinking is disabled, begin your response directly after the ' ~ think_end_token ~ ' prefix. When thinking is adaptive, decide on your own whether to think for the current turn.\n' }}
75
+ {%- if thinking_mode is defined -%}
76
+ {%- if thinking_mode == "enabled" -%}
77
+ {{- 'Current thinking mode: enabled. You MUST think step by step before every response, including after receiving function/tool results.\n' }}
78
+ {%- elif thinking_mode == "disabled" -%}
79
+ {{- 'Current thinking mode: disabled. Do not output any thinking process.\n' }}
80
+ {%- elif thinking_mode == "adaptive" -%}
81
+ {{- 'Current thinking mode: adaptive. You are encouraged to think for complex decision-making, multi-step reasoning, or when analyzing function/tool results.\n' }}
82
+ {%- endif -%}
83
+ {%- else -%}
84
+ {{- 'Current thinking mode: adaptive. You are encouraged to think for complex decision-making, multi-step reasoning, or when analyzing function/tool results.\n' }}
85
+ {%- endif -%}
86
+ {{- '</thinking_instructions>' }}
87
+ {%- endmacro -%}
88
+ {%- macro build_developer_message(developer_message) -%}
89
+ {%- if developer_message and developer_message.content -%}
90
+ {{- visible_text(developer_message.content) }}
91
+ {%- else -%}
92
+ {%- if model_identity is not defined -%}
93
+ {%- set model_identity = "You are a helpful assistant." -%}
94
+ {%- endif -%}
95
+ {{- model_identity }}
96
+ {%- endif -%}
97
+ {%- endmacro -%}
98
+ {#- Main Template Logic ================================================= -#}
99
+ {#- Role mapping: root -> system sp (high priority), system/developer -> developer sp (low priority) -#}
100
+ {%- set system_message = none -%}
101
+ {%- set developer_message = none -%}
102
+ {%- set conversation_messages = messages -%}
103
+ {%- if messages and messages[0].role == "root" -%}
104
+ {%- set system_message = messages[0] -%}
105
+ {%- set conversation_messages = messages[1:] -%}
106
+ {%- if conversation_messages and conversation_messages[0].role in ["system", "developer"] -%}
107
+ {%- set developer_message = conversation_messages[0] -%}
108
+ {%- set conversation_messages = conversation_messages[1:] -%}
109
+ {%- endif -%}
110
+ {%- elif messages and messages[0].role in ["system", "developer"] -%}
111
+ {%- set developer_message = messages[0] -%}
112
+ {%- set conversation_messages = messages[1:] -%}
113
+ {%- endif -%}
114
+ {#- Render system sp (higher priority, root role only) -#}
115
+ {{- bod_token ~ bos_token ~ 'system' ~ '\n' }}
116
+ {{- build_system_message(system_message) }}
117
+ {{- eos_token ~ '\n' }}
118
+
119
+ {#- Render developer sp (lower priority: system/developer role + tools) -#}
120
+ {{- bos_token ~ 'developer' ~ '\n' }}
121
+ {{- build_developer_message(developer_message) }}
122
+ {%- if tools -%}
123
+ {{- '\n\n' ~ '# Tools' ~ '\n' ~ 'You may call one or more tools to assist with the user query.\nHere are the tools available in JSONSchema format:' ~ '\n' }}
124
+ {{- '\n' ~ '<tools>' ~ '\n' }}
125
+ {{- render_tool_namespace("functions", tools) }}
126
+ {{- '</tools>' ~ '\n\n' }}
127
+ {{- 'To call tools, wrap all invocations in a single ' ~ toolcall_begin_token ~ toolcall_end_token ~ ' block. Parameter values containing nested objects or arrays are recursively expanded into XML elements. Example:\n' }}
128
+ {{- '\n' ~ toolcall_begin_token ~ '\n' }}
129
+ {{- ns_token + '<invoke name="tool-name-1">' }}
130
+ {{- ns_token + '<param-1>value-1' + ns_token + '</param-1>' }}
131
+ {{- ns_token + '<param-2>' }}
132
+ {{- ns_token + '<item>' }}
133
+ {{- ns_token + '<key-a>val-a' + ns_token + '</key-a>' }}
134
+ {{- ns_token + '<key-b>val-b' + ns_token + '</key-b>' }}
135
+ {{- ns_token + '</item>' }}
136
+ {{- ns_token + '</param-2>' }}
137
+ {{- ns_token + '</invoke>\n' }}
138
+ {{- ns_token + '<invoke name="tool-name-2">' }}
139
+ {{- ns_token + '<param-1>value-1' + ns_token + '</param-1>' }}
140
+ {{- ns_token + '</invoke>\n' }}
141
+ {{- toolcall_end_token }}
142
+ {%- endif -%}
143
+ {{- eos_token ~ '\n' }}
144
+
145
+ {#- Render messages -#}
146
+ {%- set last_tool_call = namespace(name=none) -%}
147
+ {%- for message in conversation_messages -%}
148
+ {%- if message.role == 'assistant' -%}
149
+ {{- bos_token ~ 'ai' ~ '\n' }}
150
+
151
+ {%- set reasoning_content = '' %}
152
+ {%- set content = visible_text(message.content) %}
153
+ {%- if message.reasoning_content is string %}
154
+ {%- set reasoning_content = message.reasoning_content %}
155
+ {%- else %}
156
+ {%- if think_end_token in content %}
157
+ {%- set reasoning_content = content.split(think_end_token)[0].strip('\n').split(think_begin_token)[-1].strip('\n') %}
158
+ {%- set content = content.split(think_end_token)[-1].strip('\n') %}
159
+ {%- endif %}
160
+ {%- endif %}
161
+
162
+ {%- if reasoning_content -%}
163
+ {#- Render thinking for every assistant turn (all-turn visible) -#}
164
+ {{- think_begin_token ~ reasoning_content ~ think_end_token }}
165
+ {%- else -%}
166
+ {#- No thinking rendered → prefix with think_end_token -#}
167
+ {{- think_end_token }}
168
+ {%- endif -%}
169
+
170
+ {%- if content -%}
171
+ {{- content }}
172
+ {%- endif -%}
173
+ {%- if message.tool_calls -%}
174
+ {{- toolcall_begin_token ~ '\n' }}
175
+
176
+ {%- for tool_call in message.tool_calls -%}
177
+ {%- if tool_call.function -%}
178
+ {%- set tool_call = tool_call.function -%}
179
+ {%- endif -%}
180
+ {{- ns_token + '<invoke name="' + tool_call.name + '">' }}
181
+ {%- set _args = tool_call.arguments -%}
182
+ {%- for k, v in _args.items() if v is not none %}
183
+ {{- ns_token + '<' + k + '>' -}}
184
+ {{- to_xml(v, ns_token) -}}
185
+ {{- ns_token + '</' + k + '>' }}
186
+ {%- endfor -%}
187
+ {{- ns_token + '</invoke>' ~ '\n' }}
188
+ {%- endfor -%}
189
+
190
+ {{- toolcall_end_token }}
191
+ {%- if message.tool_calls[-1].function -%}
192
+ {%- set last_tool_call.name = message.tool_calls[-1].function.name -%}
193
+ {%- else -%}
194
+ {%- set last_tool_call.name = message.tool_calls[-1].name -%}
195
+ {%- endif -%}
196
+ {%- else -%}
197
+ {%- set last_tool_call.name = none -%}
198
+ {%- endif -%}
199
+ {{- eos_token ~ '\n' }}
200
+
201
+ {%- elif message.role == 'tool' -%}
202
+ {%- if last_tool_call.name is none -%}
203
+ {{- raise_exception("Message has tool role, but there was no previous assistant message with a tool call!") }}
204
+ {%- endif -%}
205
+ {%- if loop.first or (conversation_messages[loop.index0 - 1].role != 'tool') -%}
206
+ {{- bos_token ~ 'tool' }}
207
+ {%- endif -%}
208
+ {{- '\n<response>' }}
209
+ {%- if message.content is string -%}
210
+ {{- message.content }}
211
+ {%- else -%}
212
+ {%- for tr in message.content -%}
213
+ {%- if tr is mapping and tr.type is defined and tr.type == 'image' -%}
214
+ {{- image_token }}
215
+ {%- elif tr is mapping and tr.type is defined and tr.type == 'video' -%}
216
+ {{- video_token }}
217
+ {%- else -%}
218
+ {{- tr.output if tr.output is defined else (tr.text if tr.type == 'text' and tr.text is defined else tr) }}
219
+ {%- endif -%}
220
+ {%- endfor -%}
221
+ {%- endif -%}
222
+ {{- '</response>' }}
223
+ {%- if loop.last or (conversation_messages[loop.index0 + 1].role != 'tool') -%}
224
+ {{- eos_token ~ '\n' -}}
225
+ {%- endif -%}
226
+
227
+ {%- elif message.role == 'user' -%}
228
+ {{- bos_token ~ 'user' ~ '\n' }}
229
+ {{- visible_text(message.content) }}
230
+ {{- eos_token ~ '\n' }}
231
+ {%- endif -%}
232
+ {%- endfor -%}
233
+
234
+ {#- Generation prompt -#}
235
+ {%- if add_generation_prompt -%}
236
+ {{- bos_token ~ 'ai' ~ '\n' }}
237
+ {%- if thinking_mode is defined and thinking_mode == "disabled" -%}
238
+ {{- think_end_token }}
239
+ {%- elif thinking_mode is defined and thinking_mode == "adaptive" -%}
240
+ {#- adaptive: no prefix, let model decide -#}
241
+ {%- elif thinking_mode is defined and thinking_mode == "enabled" -%}
242
+ {#- enabled or not defined: default to think -#}
243
+ {{- think_begin_token }}
244
+ {%- else -%}
245
+ {#- adaptive: no prefix, let model decide -#}
246
+ {%- endif -%}
247
+ {%- endif -%}
chat_template.json ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ {
2
+ "chat_template": "{# ---------- special token variables ---------- #}\n{%- set ns_token = ']<]minimax[>[' -%}\n{%- set bod_token = ']~!b[' -%}\n{%- set bos_token = ']~b]' -%}\n{%- set eos_token = '[e~[' -%}\n{%- set toolcall_begin_token = ns_token ~ '<tool_call>' -%}\n{%- set toolcall_end_token = ns_token ~ '</tool_call>' -%}\n{%- set think_begin_token = '<mm:think>' -%}\n{%- set think_end_token = '</mm:think>' -%}\n{%- set image_token = ']<]image[>[' -%}\n{%- set video_token = ']<]video[>[' -%}\n{#- Thinking mode: \"enabled\" / \"disabled\" / \"adaptive\" / not defined -#}\n{#- Recursive XML renderer for tool_call arguments ======================== -#}\n{#- None values are intentionally skipped in mapping iteration so that\n `<key>null</key>` (which would round-trip to the literal string \"null\")\n never appears in the rendered tool_call. The convention is: omit the\n field entirely. The top-level `_args` loop applies the same rule.\n The `val is none` branch below is a safety net only \u2014 upstream cleaning\n (drop_none_in_tool_arguments) should ensure no None ever reaches here. -#}\n{%- macro to_xml(val, ns) -%}\n{%- if val is mapping -%}\n{%- for k, v in val.items() if v is not none -%}\n{{ ns }}<{{ k }}>{{ to_xml(v, ns) }}{{ ns }}</{{ k }}>\n{%- endfor -%}\n{%- elif val is iterable and val is not string -%}\n{%- for item in val -%}\n{{ ns }}<item>{{ to_xml(item, ns) }}{{ ns }}</item>\n{%- endfor -%}\n{%- elif val is none -%}\n{#- Should be unreachable when upstream cleaning is applied. -#}\n{%- elif val is boolean -%}\n{{ val | tojson }}\n{%- else -%}\n{{ val }}\n{%- endif -%}\n{%- endmacro -%}\n{#- Tool Rendering Functions ============================================== -#}\n{%- macro render_tool_namespace(namespace_name, tool_list) -%}\n{%- for tool in tool_list -%}\n<tool>{{ tool.function | tojson(ensure_ascii=False) }}</tool>\n{% endfor -%}\n{%- endmacro -%}\n{%- macro visible_text(content) -%}\n {%- if content is string -%}\n {{ content }}\n {%- elif content is iterable and content is not mapping -%}\n {%- for item in content -%}\n {%- if item is mapping and item.type == 'text' -%}\n {{- item.text }}\n {%- elif item is mapping and item.type == 'image' -%}\n {{- image_token }}\n {%- elif item is mapping and item.type == 'video' -%}\n {{- video_token}}\n {%- elif item is string -%}\n {{- item }}\n {%- endif -%}\n {%- endfor -%}\n {%- elif content is none -%}\n {{- '' }}\n {%- else -%}\n {{- content }}\n {%- endif -%}\n{%- endmacro -%}\n{#- System Message Construction ============================================ -#}\n{%- macro build_system_message(system_message) -%}\n {%- if system_message and system_message.content -%}\n {{- visible_text(system_message.content) }}\n {%- else -%}\n {{- 'Your model version is MiniMax-M3, developed by MiniMax. Knowledge cutoff: January 2026. Founded in early 2022, MiniMax is a global AI foundation model company committed to advancing the frontiers of AI towards AGI.' }}\n {%- endif -%}\n\n {#- Thinking mode instructions -#}\n {{- '\\n\\n<thinking_instructions>\\n' }}\n {{- 'You have a thinking capability that allows you to reason step by step before responding. When thinking is enabled, wrap your reasoning in ' ~ think_begin_token ~ think_end_token ~ ' tags before your response. When thinking is disabled, begin your response directly after the ' ~ think_end_token ~ ' prefix. When thinking is adaptive, decide on your own whether to think for the current turn.\\n' }}\n {%- if thinking_mode is defined -%}\n {%- if thinking_mode == \"enabled\" -%}\n {{- 'Current thinking mode: enabled. You MUST think step by step before every response, including after receiving function/tool results.\\n' }}\n {%- elif thinking_mode == \"disabled\" -%}\n {{- 'Current thinking mode: disabled. Do not output any thinking process.\\n' }}\n {%- elif thinking_mode == \"adaptive\" -%}\n {{- 'Current thinking mode: adaptive. You are encouraged to think for complex decision-making, multi-step reasoning, or when analyzing function/tool results.\\n' }}\n {%- endif -%}\n {%- else -%}\n {{- 'Current thinking mode: adaptive. You are encouraged to think for complex decision-making, multi-step reasoning, or when analyzing function/tool results.\\n' }}\n {%- endif -%}\n {{- '</thinking_instructions>' }}\n{%- endmacro -%}\n{%- macro build_developer_message(developer_message) -%}\n {%- if developer_message and developer_message.content -%}\n {{- visible_text(developer_message.content) }}\n {%- else -%}\n {%- if model_identity is not defined -%}\n {%- set model_identity = \"You are a helpful assistant.\" -%}\n {%- endif -%}\n {{- model_identity }}\n {%- endif -%}\n{%- endmacro -%}\n{#- Main Template Logic ================================================= -#}\n{#- Role mapping: root -> system sp (high priority), system/developer -> developer sp (low priority) -#}\n{%- set system_message = none -%}\n{%- set developer_message = none -%}\n{%- set conversation_messages = messages -%}\n{%- if messages and messages[0].role == \"root\" -%}\n {%- set system_message = messages[0] -%}\n {%- set conversation_messages = messages[1:] -%}\n {%- if conversation_messages and conversation_messages[0].role in [\"system\", \"developer\"] -%}\n {%- set developer_message = conversation_messages[0] -%}\n {%- set conversation_messages = conversation_messages[1:] -%}\n {%- endif -%}\n{%- elif messages and messages[0].role in [\"system\", \"developer\"] -%}\n {%- set developer_message = messages[0] -%}\n {%- set conversation_messages = messages[1:] -%}\n{%- endif -%}\n{#- Render system sp (higher priority, root role only) -#}\n{{- bod_token ~ bos_token ~ 'system' ~ '\\n' }}\n{{- build_system_message(system_message) }}\n{{- eos_token ~ '\\n' }}\n\n{#- Render developer sp (lower priority: system/developer role + tools) -#}\n{{- bos_token ~ 'developer' ~ '\\n' }}\n{{- build_developer_message(developer_message) }}\n{%- if tools -%}\n {{- '\\n\\n' ~ '# Tools' ~ '\\n' ~ 'You may call one or more tools to assist with the user query.\\nHere are the tools available in JSONSchema format:' ~ '\\n' }}\n {{- '\\n' ~ '<tools>' ~ '\\n' }}\n {{- render_tool_namespace(\"functions\", tools) }}\n {{- '</tools>' ~ '\\n\\n' }}\n {{- 'To call tools, wrap all invocations in a single ' ~ toolcall_begin_token ~ toolcall_end_token ~ ' block. Parameter values containing nested objects or arrays are recursively expanded into XML elements. Example:\\n' }}\n {{- '\\n' ~ toolcall_begin_token ~ '\\n' }}\n {{- ns_token + '<invoke name=\"tool-name-1\">' }}\n {{- ns_token + '<param-1>value-1' + ns_token + '</param-1>' }}\n {{- ns_token + '<param-2>' }}\n {{- ns_token + '<item>' }}\n {{- ns_token + '<key-a>val-a' + ns_token + '</key-a>' }}\n {{- ns_token + '<key-b>val-b' + ns_token + '</key-b>' }}\n {{- ns_token + '</item>' }}\n {{- ns_token + '</param-2>' }}\n {{- ns_token + '</invoke>\\n' }}\n {{- ns_token + '<invoke name=\"tool-name-2\">' }}\n {{- ns_token + '<param-1>value-1' + ns_token + '</param-1>' }}\n {{- ns_token + '</invoke>\\n' }}\n {{- toolcall_end_token }}\n{%- endif -%}\n{{- eos_token ~ '\\n' }}\n\n{#- Render messages -#}\n{%- set last_tool_call = namespace(name=none) -%}\n{%- for message in conversation_messages -%}\n {%- if message.role == 'assistant' -%}\n {{- bos_token ~ 'ai' ~ '\\n' }}\n\n {%- set reasoning_content = '' %}\n {%- set content = visible_text(message.content) %}\n {%- if message.reasoning_content is string %}\n {%- set reasoning_content = message.reasoning_content %}\n {%- else %}\n {%- if think_end_token in content %}\n {%- set reasoning_content = content.split(think_end_token)[0].strip('\\n').split(think_begin_token)[-1].strip('\\n') %}\n {%- set content = content.split(think_end_token)[-1].strip('\\n') %}\n {%- endif %}\n {%- endif %}\n\n {%- if reasoning_content -%}\n {#- Render thinking for every assistant turn (all-turn visible) -#}\n {{- think_begin_token ~ reasoning_content ~ think_end_token }}\n {%- else -%}\n {#- No thinking rendered \u2192 prefix with think_end_token -#}\n {{- think_end_token }}\n {%- endif -%}\n\n {%- if content -%}\n {{- content }}\n {%- endif -%}\n {%- if message.tool_calls -%}\n {{- toolcall_begin_token ~ '\\n' }}\n\n {%- for tool_call in message.tool_calls -%}\n {%- if tool_call.function -%}\n {%- set tool_call = tool_call.function -%}\n {%- endif -%}\n{{- ns_token + '<invoke name=\"' + tool_call.name + '\">' }}\n{%- set _args = tool_call.arguments -%}\n{%- for k, v in _args.items() if v is not none %}\n{{- ns_token + '<' + k + '>' -}}\n{{- to_xml(v, ns_token) -}}\n{{- ns_token + '</' + k + '>' }}\n{%- endfor -%}\n{{- ns_token + '</invoke>' ~ '\\n' }}\n {%- endfor -%}\n\n {{- toolcall_end_token }}\n {%- if message.tool_calls[-1].function -%}\n {%- set last_tool_call.name = message.tool_calls[-1].function.name -%}\n {%- else -%}\n {%- set last_tool_call.name = message.tool_calls[-1].name -%}\n {%- endif -%}\n {%- else -%}\n {%- set last_tool_call.name = none -%}\n {%- endif -%}\n {{- eos_token ~ '\\n' }}\n\n {%- elif message.role == 'tool' -%}\n {%- if last_tool_call.name is none -%}\n {{- raise_exception(\"Message has tool role, but there was no previous assistant message with a tool call!\") }}\n {%- endif -%}\n {%- if loop.first or (conversation_messages[loop.index0 - 1].role != 'tool') -%}\n {{- bos_token ~ 'tool' }}\n {%- endif -%}\n {{- '\\n<response>' }}\n {%- if message.content is string -%}\n {{- message.content }}\n {%- else -%}\n {%- for tr in message.content -%}\n {%- if tr is mapping and tr.type is defined and tr.type == 'image' -%}\n {{- image_token }}\n {%- elif tr is mapping and tr.type is defined and tr.type == 'video' -%}\n {{- video_token }}\n {%- else -%}\n {{- tr.output if tr.output is defined else (tr.text if tr.type == 'text' and tr.text is defined else tr) }}\n {%- endif -%}\n {%- endfor -%}\n {%- endif -%}\n {{- '</response>' }}\n {%- if loop.last or (conversation_messages[loop.index0 + 1].role != 'tool') -%}\n {{- eos_token ~ '\\n' -}}\n {%- endif -%}\n\n {%- elif message.role == 'user' -%}\n {{- bos_token ~ 'user' ~ '\\n' }}\n {{- visible_text(message.content) }}\n {{- eos_token ~ '\\n' }}\n {%- endif -%}\n{%- endfor -%}\n\n{#- Generation prompt -#}\n{%- if add_generation_prompt -%}\n{{- bos_token ~ 'ai' ~ '\\n' }}\n{%- if thinking_mode is defined and thinking_mode == \"disabled\" -%}\n {{- think_end_token }}\n{%- elif thinking_mode is defined and thinking_mode == \"adaptive\" -%}\n {#- adaptive: no prefix, let model decide -#}\n{%- elif thinking_mode is defined and thinking_mode == \"enabled\" -%}\n {#- enabled or not defined: default to think -#}\n {{- think_begin_token }}\n{%- else -%}\n {#- adaptive: no prefix, let model decide -#}\n{%- endif -%}\n{%- endif -%}\n"
3
+ }
config.json ADDED
The diff for this file is too large to render. See raw diff
 
configuration_minimax_m3_vl.py ADDED
@@ -0,0 +1,111 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """HuggingFace configs for the MiniMax VL family (M2 VL / M3 VL).
2
+
3
+ This file is bundled into every converted HF checkpoint so that loading via
4
+ ``AutoConfig.from_pretrained(..., trust_remote_code=True)`` works without any
5
+ runtime dependency on sglang or other internal packages — only stock
6
+ ``transformers`` is required.
7
+
8
+ The class definitions intentionally mirror
9
+ ``sglang.srt.configs.minimax_vl``; if either side changes, keep them in sync.
10
+
11
+ The file is named ``configuration_minimax_m3_vl.py`` (matching the legacy
12
+ ``model_type="minimax_m3_vl"`` and the converter's ``auto_map`` entry) so
13
+ that ckpts produced by this converter remain loadable by older sglang versions
14
+ that only know the ``MiniMaxM3VL*`` names. The canonical class is
15
+ ``MiniMaxM3VLConfig``; ``MiniMaxM3VLConfig`` is a thin BC alias whose only
16
+ purpose is to be referenced from ``auto_map``.
17
+ """
18
+
19
+ from typing import Optional
20
+
21
+ from transformers.configuration_utils import PretrainedConfig
22
+ from transformers.models.auto import CONFIG_MAPPING
23
+
24
+
25
+ def _coerce_sub_config(
26
+ sub_config: Optional[dict], default_model_type: str
27
+ ) -> Optional[PretrainedConfig]:
28
+ """Convert a config dict to a ``PretrainedConfig`` instance.
29
+
30
+ If ``model_type`` is registered in HF ``CONFIG_MAPPING`` the corresponding
31
+ config class is used; otherwise we fall back to a generic
32
+ ``PretrainedConfig`` so all dict keys still become real attributes (M3's
33
+ text backbone uses ``model_type="minimax_m2"`` which is not in
34
+ ``CONFIG_MAPPING``).
35
+ """
36
+ if not isinstance(sub_config, dict):
37
+ return sub_config
38
+ model_type = sub_config.get("model_type", default_model_type)
39
+ cls = CONFIG_MAPPING.get(model_type, PretrainedConfig)
40
+ return cls(**sub_config)
41
+
42
+
43
+ class MiniMaxVLBaseConfig(PretrainedConfig):
44
+ """Base config shared by every MiniMax VL variant.
45
+
46
+ Handles vision/text sub-config coercion. Concrete subclasses only need to
47
+ declare a unique ``model_type`` string.
48
+ """
49
+
50
+ def __init__(
51
+ self,
52
+ vision_config: Optional[dict] = None,
53
+ text_config: Optional[dict] = None,
54
+ image_token_index: int = 200025,
55
+ video_token_index: int = 200026,
56
+ image_seq_length: int = 576,
57
+ process_image_mode: str = "dynamic_res",
58
+ projector_hidden_act: str = "gelu",
59
+ multimodal_projector_bias: bool = True,
60
+ vision_feature_layer: int = -1,
61
+ vision_feature_select_strategy: str = "full",
62
+ img_token_compression_config: Optional[dict] = None,
63
+ image_grid_pinpoints: Optional[str] = None,
64
+ **kwargs,
65
+ ):
66
+ self.vision_config = _coerce_sub_config(vision_config, "clip_vision_model")
67
+ self.text_config = _coerce_sub_config(text_config, "mixtral")
68
+
69
+ self.image_token_index = image_token_index
70
+ self.video_token_index = video_token_index
71
+ self.image_seq_length = image_seq_length
72
+ self.process_image_mode = process_image_mode
73
+ self.projector_hidden_act = projector_hidden_act
74
+ self.multimodal_projector_bias = multimodal_projector_bias
75
+ self.vision_feature_layer = vision_feature_layer
76
+ self.vision_feature_select_strategy = vision_feature_select_strategy
77
+ self.img_token_compression_config = img_token_compression_config or {}
78
+ self.image_grid_pinpoints = image_grid_pinpoints
79
+
80
+ super().__init__(**kwargs)
81
+
82
+ def __post_init__(self, **kwargs):
83
+ super().__post_init__(**kwargs)
84
+ if hasattr(self, "vision_config"):
85
+ self.vision_config = _coerce_sub_config(self.vision_config, "clip_vision_model")
86
+ if hasattr(self, "text_config"):
87
+ self.text_config = _coerce_sub_config(self.text_config, "mixtral")
88
+
89
+
90
+ class MiniMaxM2VLConfig(MiniMaxVLBaseConfig):
91
+ """MiniMax M2 VL: vision tower + M2 (Mixtral-style MoE) text backbone."""
92
+
93
+ model_type = "minimax_m2_vl"
94
+
95
+
96
+ class MiniMaxM3VLConfig(MiniMaxVLBaseConfig):
97
+ """MiniMax M3 VL: vision tower + M3 (mixed sparse/dense MoE) text backbone."""
98
+
99
+ model_type = "minimax_m3_vl"
100
+
101
+
102
+ class MiniMaxM2MiniVLConfig(MiniMaxM2VLConfig):
103
+ """Legacy alias kept so old ``model_type="minimax_m2_mini_vl"`` ckpts load."""
104
+
105
+ model_type = "minimax_m2_mini_vl"
106
+
107
+
108
+ class MiniMaxM3VLConfig(MiniMaxM3VLConfig):
109
+ """Legacy alias kept so old ``model_type="minimax_m3_vl"`` ckpts load."""
110
+
111
+ model_type = "minimax_m3_vl"
figures/benchmark.jpeg ADDED

Git LFS Details

  • SHA256: b4bc02e54f508f540e71a9286905477c780934bb79c0b17cd5892b6338313e57
  • Pointer size: 132 Bytes
  • Size of remote file: 4.42 MB
figures/efficiency_gqa_vs_msa.png ADDED
figures/logo.svg ADDED
generation_config.json ADDED
@@ -0,0 +1,8 @@
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "bos_token_id": 200019,
3
+ "do_sample": true,
4
+ "eos_token_id": 200020,
5
+ "temperature": 1.0,
6
+ "top_p": 0.95,
7
+ "transformers_version": "4.46.1"
8
+ }
image_processor.py ADDED
@@ -0,0 +1,223 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright 2023-2024 SGLang Team
2
+ # Licensed under the Apache License, Version 2.0 (the "License");
3
+ """
4
+ MiniMax VL family HuggingFace-compatible Processor, ImageProcessor, VideoProcessor.
5
+ """
6
+ import math
7
+ from typing import List, Tuple
8
+
9
+ import torch
10
+ from torchvision.transforms import InterpolationMode
11
+ from transformers import BatchFeature
12
+ from transformers.image_processing_utils_fast import (
13
+ BaseImageProcessorFast,
14
+ group_images_by_shape,
15
+ reorder_images,
16
+ )
17
+ from transformers.image_utils import PILImageResampling, SizeDict
18
+ from transformers.processing_utils import (
19
+ ImagesKwargs,
20
+ Unpack,
21
+ )
22
+ from transformers.utils import TensorType
23
+
24
+ MAX_RATIO = 200
25
+
26
+
27
+ def round_by_factor(number: int, factor: int) -> int:
28
+ return round(number / factor) * factor
29
+
30
+
31
+ def ceil_by_factor(number: int, factor: int) -> int:
32
+ return math.ceil(number / factor) * factor
33
+
34
+
35
+ def floor_by_factor(number: int, factor: int) -> int:
36
+ return math.floor(number / factor) * factor
37
+
38
+
39
+ def smart_resize(
40
+ height: int,
41
+ width: int,
42
+ factor: int = 28,
43
+ min_pixels: int = 4 * 28 * 28,
44
+ max_pixels: int = 451584,
45
+ ) -> tuple[int, int]:
46
+ if max(height, width) / min(height, width) > MAX_RATIO:
47
+ raise ValueError(
48
+ f"absolute aspect ratio must be smaller than {MAX_RATIO}, "
49
+ f"got {max(height, width) / min(height, width)}"
50
+ )
51
+ h_bar = max(factor, round_by_factor(height, factor))
52
+ w_bar = max(factor, round_by_factor(width, factor))
53
+ if h_bar * w_bar > max_pixels:
54
+ beta = math.sqrt((height * width) / max_pixels)
55
+ h_bar = floor_by_factor(height / beta, factor)
56
+ w_bar = floor_by_factor(width / beta, factor)
57
+ elif h_bar * w_bar < min_pixels:
58
+ beta = math.sqrt(min_pixels / (height * width))
59
+ h_bar = ceil_by_factor(height * beta, factor)
60
+ w_bar = ceil_by_factor(width * beta, factor)
61
+ return h_bar, w_bar
62
+
63
+
64
+ # ==============================================================================
65
+ # MiniMax M3 VL Image Processor Fast (Fast Mode - Torch based)
66
+ # ==============================================================================
67
+
68
+
69
+ class MiniMaxM3VLImageProcessorKwargs(ImagesKwargs, total=False):
70
+ patch_size: int
71
+ temporal_patch_size: int
72
+ merge_size: int
73
+ max_pixels: int
74
+
75
+
76
+ class MiniMaxM3VLImageProcessor(BaseImageProcessorFast):
77
+ do_resize = True
78
+ resample = PILImageResampling.BICUBIC
79
+ size = {"height": 672, "width": 672} # required by base class validation, not used as resize bound
80
+ default_to_square = False
81
+ do_rescale = True
82
+ rescale_factor = 1 / 255
83
+ do_normalize = True
84
+ image_mean = [0.48145466, 0.4578275, 0.40821073]
85
+ image_std = [0.26862954, 0.26130258, 0.27577711]
86
+ do_convert_rgb = True
87
+ patch_size = 14
88
+ temporal_patch_size = 2
89
+ merge_size = 2
90
+ max_pixels = 451584 # 672*672
91
+ valid_kwargs = MiniMaxM3VLImageProcessorKwargs
92
+ model_input_names = ["pixel_values", "image_grid_thw"]
93
+
94
+ def __init__(self, **kwargs: Unpack[MiniMaxM3VLImageProcessorKwargs]):
95
+ super().__init__(**kwargs)
96
+
97
+ def preprocess(
98
+ self, images, **kwargs: Unpack[MiniMaxM3VLImageProcessorKwargs]
99
+ ) -> BatchFeature:
100
+ return super().preprocess(images, **kwargs)
101
+
102
+ def _preprocess(
103
+ self,
104
+ images: List[torch.Tensor],
105
+ do_resize: bool,
106
+ size: SizeDict,
107
+ resample: PILImageResampling | InterpolationMode | int | None,
108
+ do_rescale: bool,
109
+ rescale_factor: float,
110
+ do_normalize: bool,
111
+ image_mean: float | List[float] | None,
112
+ image_std: float | List[float] | None,
113
+ patch_size: int,
114
+ temporal_patch_size: int,
115
+ merge_size: int,
116
+ max_pixels: int,
117
+ disable_grouping: bool | None,
118
+ return_tensors: str | TensorType | None,
119
+ **kwargs,
120
+ ) -> BatchFeature:
121
+ grouped_images, grouped_images_index = group_images_by_shape(
122
+ images, disable_grouping=disable_grouping
123
+ )
124
+ resized_images_grouped = {}
125
+ factor = patch_size * merge_size
126
+ for shape, stacked_images in grouped_images.items():
127
+ height, width = stacked_images.shape[-2:]
128
+ if do_resize:
129
+ resized_height, resized_width = smart_resize(
130
+ height, width, factor=factor,
131
+ max_pixels=max_pixels,
132
+ )
133
+ stacked_images = self.resize(
134
+ stacked_images,
135
+ size=SizeDict(height=resized_height, width=resized_width),
136
+ resample=resample,
137
+ )
138
+ resized_images_grouped[shape] = stacked_images
139
+
140
+ resized_images = reorder_images(resized_images_grouped, grouped_images_index)
141
+
142
+ grouped_images, grouped_images_index = group_images_by_shape(
143
+ resized_images, disable_grouping=disable_grouping
144
+ )
145
+ processed_images_grouped = {}
146
+ processed_grids = {}
147
+
148
+ for shape, stacked_images in grouped_images.items():
149
+ resized_height, resized_width = stacked_images.shape[-2:]
150
+
151
+ patches = self.rescale_and_normalize(
152
+ stacked_images,
153
+ do_rescale,
154
+ rescale_factor,
155
+ do_normalize,
156
+ image_mean,
157
+ image_std,
158
+ )
159
+ if patches.ndim == 4:
160
+ patches = patches.unsqueeze(1)
161
+
162
+ if patches.shape[1] % temporal_patch_size != 0:
163
+ repeats = patches[:, -1:].repeat(
164
+ 1,
165
+ temporal_patch_size - (patches.shape[1] % temporal_patch_size),
166
+ 1,
167
+ 1,
168
+ 1,
169
+ )
170
+ patches = torch.cat([patches, repeats], dim=1)
171
+
172
+ batch_size, grid_t, channel = patches.shape[:3]
173
+ grid_t = grid_t // temporal_patch_size
174
+ grid_h, grid_w = resized_height // patch_size, resized_width // patch_size
175
+
176
+ patches = patches.view(
177
+ batch_size,
178
+ grid_t,
179
+ temporal_patch_size,
180
+ channel,
181
+ grid_h // merge_size,
182
+ merge_size,
183
+ patch_size,
184
+ grid_w // merge_size,
185
+ merge_size,
186
+ patch_size,
187
+ )
188
+ patches = patches.permute(0, 1, 4, 7, 5, 8, 3, 2, 6, 9)
189
+
190
+ flatten_patches = patches.reshape(
191
+ batch_size,
192
+ grid_t * grid_h * grid_w,
193
+ channel * temporal_patch_size * patch_size * patch_size,
194
+ )
195
+
196
+ processed_images_grouped[shape] = flatten_patches
197
+ processed_grids[shape] = [[grid_t, grid_h, grid_w]] * batch_size
198
+
199
+ processed_images = reorder_images(
200
+ processed_images_grouped, grouped_images_index
201
+ )
202
+ processed_grids = reorder_images(processed_grids, grouped_images_index)
203
+
204
+ pixel_values = torch.cat(processed_images, dim=0)
205
+ image_grid_thw = torch.tensor(processed_grids, dtype=torch.long)
206
+
207
+ return BatchFeature(
208
+ data={"pixel_values": pixel_values, "image_grid_thw": image_grid_thw},
209
+ tensor_type=return_tensors,
210
+ )
211
+
212
+ def get_number_of_image_patches(self, height: int, width: int, images_kwargs=None):
213
+ images_kwargs = images_kwargs or {}
214
+ patch_size = images_kwargs.get("patch_size", self.patch_size)
215
+ merge_size = images_kwargs.get("merge_size", self.merge_size)
216
+ max_pixels = images_kwargs.get("max_pixels", self.max_pixels)
217
+
218
+ resized_height, resized_width = smart_resize(
219
+ height, width, factor=patch_size * merge_size,
220
+ max_pixels=max_pixels,
221
+ )
222
+ grid_h, grid_w = resized_height // patch_size, resized_width // patch_size
223
+ return grid_h * grid_w
model-00001-of-00059.safetensors ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:33e2c66bb6e03673e396a88a207d031fee08208aa4c2e9879580db74fd7613d1
3
+ size 3166017650
model-00003-of-00059.safetensors ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:fdb9f05764648306a73a77425a71ddcbb1b8c1fe5058f666983bb92b6cd5bb04
3
+ size 4657231227
model-00005-of-00059.safetensors ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:4e7f2cce601cad5c95c1dc0b11f35697e59bea7a35f16ede4b618ca8792f3967
3
+ size 4657231159
model-00006-of-00059.safetensors ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:b0257598613aa5ebb3140f18274e574b8fe89710f0c769f1bf13ebe677e409c8
3
+ size 4657231149
model-00009-of-00059.safetensors ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:080c2190da17149b01ee5a7878e57e387fd03e4349e38f4bfa1f10f4cabf1389
3
+ size 4657231174
model-00011-of-00059.safetensors ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:d0f889d08d256ef5200a4ab82e629a52337cf82b525ef5f1c625aec419937890
3
+ size 4657231192
model-00012-of-00059.safetensors ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:38c03234662b9bfa094d1eff540608732324276d9f0706bc2bd632e6eff4528a
3
+ size 4657231166
model-00017-of-00059.safetensors ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:f0a921ca2b0569a83e9506a0d2618274f8c1d36946338e7f9cbdee6b25766b43
3
+ size 4657231236
model-00018-of-00059.safetensors ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:8327c7db553d19ff6061f30a33ade74212b4951a194122fb96741eaa2fc248ae
3
+ size 4657231160
model-00020-of-00059.safetensors ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:da1b452c34be78661aecb4586c59f3742df23da3032b908c323c23311117f840
3
+ size 4657231208
model-00024-of-00059.safetensors ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:d6e25e065bc8aee93642d76f43355e8abd5c8e4f07d2190ce214a9e08023ab2f
3
+ size 4657231230
model-00025-of-00059.safetensors ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:725727316742a7ea552e279a76decbfece07ef2dcce214a940f19a0c504e3e17
3
+ size 4657231206
model-00029-of-00059.safetensors ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:cf3a79010f022ab039c3352d7daac917ed5ee95bbe7493b59613bcd9ee200b0d
3
+ size 4657231202
model-00032-of-00059.safetensors ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:7b6dac609f239168323915693ddaa20d01d896c1323a5c6045a64d3244e4c856
3
+ size 4657231216
model-00034-of-00059.safetensors ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:9fac4f81879cac17ce1ca421fcee51327710756127e7bf26816ab1df691adcea
3
+ size 4657231234
model-00037-of-00059.safetensors ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:6788eeae35f9c7295404f35b4164269a0b3d872886c4498baedb9ef0ab1d9904
3
+ size 4657231188
model-00038-of-00059.safetensors ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:b5364d3065dc49ca296391ff9bc58e770bad53eaf19f46833400cb784946ad12
3
+ size 4657231232
model-00041-of-00059.safetensors ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:9779ee56b3ec98119c1a72b59cf59e683a64ce75852005ade6f7bff4af08e31f
3
+ size 4657231202
model-00042-of-00059.safetensors ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:e3927ccbc2806d930d2af41ae82f31ed8c79ebe66a205001c51b09a7fcc2ae29
3
+ size 4657231228
model-00044-of-00059.safetensors ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:adbbb54abd36592f60db9460d84693304456fd84306f46fb75504b497bd3a037
3
+ size 4657231254
model-00047-of-00059.safetensors ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:08553574d456494e6dee01cf875e966c530b369ec07be20c05d4bd60b3c7ef8a
3
+ size 4657231186
model-00048-of-00059.safetensors ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:f3e72c59af802312500f06daeb9050ef81171569cee14304fb8979eabfaf045b
3
+ size 4657231180
model-00050-of-00059.safetensors ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:b9333ad2fc3b0e5840cb27037a45505353feb077e184ee6770af343c8cf9f444
3
+ size 4657231190
model-00053-of-00059.safetensors ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:3dade8595e4f85dddbe5dc3e7bc95e83baeb41939d3bb0eaf086350a9b45bd7d
3
+ size 4657231184
model-00056-of-00059.safetensors ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:8b2e460e389a66c54f2286feac2a059e7e600ac57b5a9175103523b35b9aec4a
3
+ size 4657231214
model-00057-of-00059.safetensors ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:96041398b4703db8a8cfd0e9ccdaa3638fe34e633efbed88bb2caa46885b0c58
3
+ size 4657231244
model-00059-of-00059.safetensors ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:85e544b3b4ee82cc4f91b08a18439b262a2209dd9470098c4f223de75e87ab4f
3
+ size 1774818666
model.safetensors.index.json ADDED
The diff for this file is too large to render. See raw diff
 
preprocessor_config.json ADDED
@@ -0,0 +1,27 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "do_convert_rgb": true,
3
+ "do_normalize": true,
4
+ "do_rescale": true,
5
+ "do_resize": true,
6
+ "image_mean": [
7
+ 0.48145466,
8
+ 0.4578275,
9
+ 0.40821073
10
+ ],
11
+ "image_processor_type": "MiniMaxM3VLImageProcessor",
12
+ "image_std": [
13
+ 0.26862954,
14
+ 0.26130258,
15
+ 0.27577711
16
+ ],
17
+ "max_pixels": 451584,
18
+ "merge_size": 2,
19
+ "min_pixels": 3136,
20
+ "patch_size": 14,
21
+ "rescale_factor": 0.00392156862745098,
22
+ "size": [
23
+ 672,
24
+ 672
25
+ ],
26
+ "temporal_patch_size": 2
27
+ }
processing_minimax.py ADDED
@@ -0,0 +1,254 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright 2023-2024 SGLang Team
2
+ # Licensed under the Apache License, Version 2.0 (the "License");
3
+ """
4
+ MiniMax VL family HuggingFace-compatible Processor, ImageProcessor, VideoProcessor.
5
+ """
6
+
7
+ import math
8
+ import re
9
+ from typing import List, Optional, Tuple, Union
10
+
11
+ import torch
12
+ import torchvision
13
+ from torchvision.transforms import InterpolationMode
14
+ from transformers import BatchFeature
15
+ from transformers.image_processing_utils_fast import (
16
+ BaseImageProcessorFast,
17
+ group_images_by_shape,
18
+ reorder_images,
19
+ )
20
+ from transformers.image_utils import PILImageResampling, SizeDict
21
+ from transformers.processing_utils import (
22
+ ImagesKwargs,
23
+ ProcessingKwargs,
24
+ ProcessorMixin,
25
+ Unpack,
26
+ VideosKwargs,
27
+ )
28
+ from transformers.utils import TensorType
29
+ from transformers.video_processing_utils import BaseVideoProcessor
30
+ from transformers.video_utils import group_videos_by_shape, reorder_videos
31
+
32
+
33
+ class MiniMaxVLProcessorKwargs(ProcessingKwargs, total=False):
34
+ _defaults = {
35
+ "videos_kwargs": {
36
+ "do_resize": False,
37
+ "return_metadata": True,
38
+ },
39
+ }
40
+
41
+
42
+ class MiniMaxVLProcessor(ProcessorMixin):
43
+ IMAGE_TOKEN = "]<]image[>["
44
+ VIDEO_TOKEN = "]<]video[>["
45
+ VISION_START_TOKEN = "]<]start of image[>["
46
+ VISION_END_TOKEN = "]<]end of image[>["
47
+
48
+ def __init__(
49
+ self, image_processor=None, tokenizer=None, video_processor=None, **kwargs
50
+ ):
51
+ self.image_token_id = tokenizer.convert_tokens_to_ids(self.IMAGE_TOKEN)
52
+ self.video_token_id = tokenizer.convert_tokens_to_ids(self.VIDEO_TOKEN)
53
+ super().__init__(image_processor, tokenizer, video_processor)
54
+ # Video expansion also uses image start/end tokens. Separate video
55
+ # start/end tokens exist in the tokenizer, but the original MiniMax
56
+ # serving path did not use them; keep that behavior for compatibility.
57
+ self.vision_start_token_id = tokenizer.convert_tokens_to_ids(
58
+ self.VISION_START_TOKEN
59
+ )
60
+ self.vision_end_token_id = tokenizer.convert_tokens_to_ids(
61
+ self.VISION_END_TOKEN
62
+ )
63
+
64
+ def _prune_video_tokens(
65
+ self,
66
+ input_text: str,
67
+ video_segments: List[int],
68
+ video_token: str,
69
+ ) -> str:
70
+ """
71
+ Prune video tokens by temporal_patch_size (e.g., 2:1).
72
+
73
+ Expects the prompt to carry exactly sum(video_segments) video
74
+ tokens — i.e. one token per *sampled* frame. Then drops token.
75
+
76
+ Args:
77
+ input_text: prompt with N video_tokens per segment
78
+ video_segments: actual sampled frame count per video segment
79
+ video_token: the video token string, e.g. ']<]video[>['
80
+
81
+ Returns:
82
+ Pruned input_text with ~N/temporal_patch_size tokens per segment.
83
+ """
84
+ # If no videos or temporal_patch_size <= 1, no pruning needed
85
+ if not video_segments or self.video_processor.temporal_patch_size <= 1:
86
+ return input_text
87
+
88
+ # Split while keeping delimiters
89
+ special_tokens = [video_token] # , image_token]
90
+ pattern = "|".join(map(re.escape, special_tokens))
91
+ parts = re.split(f"({pattern})", input_text)
92
+
93
+ def is_timestamp(text: str) -> bool:
94
+ """Check if text ends with timestamp format like ']<]0.0 seconds[>['"""
95
+ return (
96
+ text.endswith("seconds[>[")
97
+ or text.endswith("seconds[>[ ")
98
+ or text.endswith("seconds [>[")
99
+ or text.endswith("seconds [>[ ")
100
+ )
101
+
102
+ def extract_timestamp(text: str) -> str:
103
+ """Extract timestamp text from the end, starting from ']<]'"""
104
+ start_index = text.rfind("]<]")
105
+ if start_index == -1:
106
+ raise ValueError(f"Failed to extract timestamp: {text}")
107
+ return text[start_index:]
108
+
109
+ # Build new text with pruned video tokens
110
+ final_parts = []
111
+ current_seg_idx = 0 # Which video segment we're in
112
+ frame_in_seg = 0 # Frame index within current segment
113
+ last_timestamp_len = 0 # Length of timestamp to potentially remove
114
+
115
+ for part in parts:
116
+ if part == video_token:
117
+ if current_seg_idx < len(video_segments):
118
+ if frame_in_seg % self.video_processor.temporal_patch_size == 0:
119
+ # Keep this video token
120
+ final_parts.append(part)
121
+ frame_in_seg += 1
122
+ if frame_in_seg >= video_segments[current_seg_idx]:
123
+ current_seg_idx += 1
124
+ frame_in_seg = 0
125
+ last_timestamp_len = 0
126
+ else:
127
+ # Skip this video token
128
+ frame_in_seg += 1
129
+ if frame_in_seg >= video_segments[current_seg_idx]:
130
+ current_seg_idx += 1
131
+ frame_in_seg = 0
132
+ # Remove the timestamp that was already appended
133
+ if last_timestamp_len > 0:
134
+ # Truncate the last part to remove timestamp
135
+ assert len(final_parts) > 0
136
+ final_parts[-1] = final_parts[-1][:-last_timestamp_len]
137
+ last_timestamp_len = 0
138
+ else:
139
+ # No more video segments, keep as is
140
+ final_parts.append(part)
141
+ last_timestamp_len = 0
142
+ else:
143
+ # Text part
144
+ final_parts.append(part)
145
+ # Check if this text ends with a timestamp
146
+ if is_timestamp(part):
147
+ last_timestamp_len = len(extract_timestamp(part))
148
+ else:
149
+ last_timestamp_len = 0
150
+
151
+ return "".join(final_parts)
152
+
153
+ def __call__(
154
+ self,
155
+ images=None,
156
+ text=None,
157
+ videos=None,
158
+ **kwargs: Unpack[MiniMaxVLProcessorKwargs],
159
+ ) -> BatchFeature:
160
+ output_kwargs = self._merge_kwargs(
161
+ MiniMaxVLProcessorKwargs,
162
+ tokenizer_init_kwargs=self.tokenizer.init_kwargs,
163
+ **kwargs,
164
+ )
165
+
166
+ if images is not None:
167
+ images_kwargs = output_kwargs["images_kwargs"]
168
+ image_inputs = self.image_processor(images=images, **images_kwargs)
169
+ image_grid_thw = image_inputs["image_grid_thw"]
170
+
171
+ else:
172
+ image_inputs = {}
173
+ image_grid_thw = None
174
+
175
+ if videos is not None:
176
+ videos_kwargs = output_kwargs["videos_kwargs"]
177
+ video_inputs = self.video_processor(videos=videos, **videos_kwargs)
178
+ video_grid_thw = video_inputs["video_grid_thw"]
179
+ if not kwargs.get("return_metadata"):
180
+ video_metadata = video_inputs.pop("video_metadata")
181
+ else:
182
+ video_metadata = video_inputs["video_metadata"]
183
+ else:
184
+ video_inputs = {}
185
+ video_grid_thw = None
186
+
187
+ if not isinstance(text, list):
188
+ text = [text]
189
+ text = text.copy()
190
+
191
+ # Expand image tokens
192
+ if image_grid_thw is not None:
193
+ merge_length = self.image_processor.merge_size**2
194
+ placeholder = "]<]placeholder[>["
195
+ index = 0
196
+ for i in range(len(text)):
197
+ while self.IMAGE_TOKEN in text[i]:
198
+ num_tokens = image_grid_thw[index].prod() // merge_length
199
+ text[i] = text[i].replace(
200
+ self.IMAGE_TOKEN,
201
+ self.VISION_START_TOKEN
202
+ + placeholder * num_tokens
203
+ + self.VISION_END_TOKEN,
204
+ 1,
205
+ )
206
+ index += 1
207
+ text[i] = text[i].replace(placeholder, self.IMAGE_TOKEN)
208
+
209
+ # Expand video tokens
210
+ if video_grid_thw is not None:
211
+ merge_length = self.image_processor.merge_size**2
212
+ placeholder = "]<]placeholder[>["
213
+ index = 0
214
+ for i in range(len(text)):
215
+ while self.VIDEO_TOKEN in text[i]:
216
+ metadata = video_metadata[index]
217
+ grid_t = video_grid_thw[index][0]
218
+ frame_seqlen = video_grid_thw[index][1:].prod() // merge_length
219
+
220
+ video_placeholder = ""
221
+ for frame_idx in range(grid_t):
222
+ if (
223
+ metadata.fps is not None
224
+ and metadata.frames_indices is not None
225
+ ):
226
+ ts = (
227
+ metadata.frames_indices[
228
+ min(
229
+ frame_idx
230
+ * self.video_processor.temporal_patch_size,
231
+ len(metadata.frames_indices) - 1,
232
+ )
233
+ ]
234
+ / metadata.fps
235
+ )
236
+ video_placeholder += f"]<]{ts:.1f} seconds[>["
237
+ video_placeholder += (
238
+ self.VISION_START_TOKEN
239
+ + placeholder * frame_seqlen
240
+ + self.VISION_END_TOKEN
241
+ )
242
+
243
+ text[i] = text[i].replace(self.VIDEO_TOKEN, video_placeholder, 1)
244
+ index += 1
245
+ text[i] = text[i].replace(placeholder, self.VIDEO_TOKEN)
246
+
247
+ # Tokenize
248
+ return_tensors = output_kwargs["text_kwargs"].pop("return_tensors", None)
249
+ text_inputs = self.tokenizer(text, **output_kwargs["text_kwargs"])
250
+
251
+ return BatchFeature(
252
+ data={**text_inputs, **image_inputs, **video_inputs},
253
+ tensor_type=return_tensors,
254
+ )
processor_config.json ADDED
@@ -0,0 +1,63 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "chat_template": "{# ---------- special token variables ---------- #}\n{%- set ns_token = ']<]minimax[>[' -%}\n{%- set bod_token = ']~!b[' -%}\n{%- set bos_token = ']~b]' -%}\n{%- set eos_token = '[e~[' -%}\n{%- set toolcall_begin_token = ns_token ~ '<tool_call>' -%}\n{%- set toolcall_end_token = ns_token ~ '</tool_call>' -%}\n{%- set think_begin_token = '<mm:think>' -%}\n{%- set think_end_token = '</mm:think>' -%}\n{%- set image_token = ']<]image[>[' -%}\n{%- set video_token = ']<]video[>[' -%}\n{#- Thinking mode: \"enabled\" / \"disabled\" / \"adaptive\" / not defined -#}\n{#- Recursive XML renderer for tool_call arguments ======================== -#}\n{#- None values are intentionally skipped in mapping iteration so that\n `<key>null</key>` (which would round-trip to the literal string \"null\")\n never appears in the rendered tool_call. The convention is: omit the\n field entirely. The top-level `_args` loop applies the same rule.\n The `val is none` branch below is a safety net only \u2014 upstream cleaning\n (drop_none_in_tool_arguments) should ensure no None ever reaches here. -#}\n{%- macro to_xml(val, ns) -%}\n{%- if val is mapping -%}\n{%- for k, v in val.items() if v is not none -%}\n{{ ns }}<{{ k }}>{{ to_xml(v, ns) }}{{ ns }}</{{ k }}>\n{%- endfor -%}\n{%- elif val is iterable and val is not string -%}\n{%- for item in val -%}\n{{ ns }}<item>{{ to_xml(item, ns) }}{{ ns }}</item>\n{%- endfor -%}\n{%- elif val is none -%}\n{#- Should be unreachable when upstream cleaning is applied. -#}\n{%- elif val is boolean -%}\n{{ val | tojson }}\n{%- else -%}\n{{ val }}\n{%- endif -%}\n{%- endmacro -%}\n{#- Tool Rendering Functions ============================================== -#}\n{%- macro render_tool_namespace(namespace_name, tool_list) -%}\n{%- for tool in tool_list -%}\n<tool>{{ tool.function | tojson(ensure_ascii=False) }}</tool>\n{% endfor -%}\n{%- endmacro -%}\n{%- macro visible_text(content) -%}\n {%- if content is string -%}\n {{ content }}\n {%- elif content is iterable and content is not mapping -%}\n {%- for item in content -%}\n {%- if item is mapping and item.type == 'text' -%}\n {{- item.text }}\n {%- elif item is mapping and item.type == 'image' -%}\n {{- image_token }}\n {%- elif item is mapping and item.type == 'video' -%}\n {{- video_token}}\n {%- elif item is string -%}\n {{- item }}\n {%- endif -%}\n {%- endfor -%}\n {%- elif content is none -%}\n {{- '' }}\n {%- else -%}\n {{- content }}\n {%- endif -%}\n{%- endmacro -%}\n{#- System Message Construction ============================================ -#}\n{%- macro build_system_message(system_message) -%}\n {%- if system_message and system_message.content -%}\n {{- visible_text(system_message.content) }}\n {%- else -%}\n {{- 'Your model version is MiniMax-M3, developed by MiniMax. Knowledge cutoff: January 2026. Founded in early 2022, MiniMax is a global AI foundation model company committed to advancing the frontiers of AI towards AGI.' }}\n {%- endif -%}\n\n {#- Thinking mode instructions -#}\n {{- '\\n\\n<thinking_instructions>\\n' }}\n {{- 'You have a thinking capability that allows you to reason step by step before responding. When thinking is enabled, wrap your reasoning in ' ~ think_begin_token ~ think_end_token ~ ' tags before your response. When thinking is disabled, begin your response directly after the ' ~ think_end_token ~ ' prefix. When thinking is adaptive, decide on your own whether to think for the current turn.\\n' }}\n {%- if thinking_mode is defined -%}\n {%- if thinking_mode == \"enabled\" -%}\n {{- 'Current thinking mode: enabled. You MUST think step by step before every response, including after receiving function/tool results.\\n' }}\n {%- elif thinking_mode == \"disabled\" -%}\n {{- 'Current thinking mode: disabled. Do not output any thinking process.\\n' }}\n {%- elif thinking_mode == \"adaptive\" -%}\n {{- 'Current thinking mode: adaptive. You are encouraged to think for complex decision-making, multi-step reasoning, or when analyzing function/tool results.\\n' }}\n {%- endif -%}\n {%- else -%}\n {{- 'Current thinking mode: adaptive. You are encouraged to think for complex decision-making, multi-step reasoning, or when analyzing function/tool results.\\n' }}\n {%- endif -%}\n {{- '</thinking_instructions>' }}\n{%- endmacro -%}\n{%- macro build_developer_message(developer_message) -%}\n {%- if developer_message and developer_message.content -%}\n {{- visible_text(developer_message.content) }}\n {%- else -%}\n {%- if model_identity is not defined -%}\n {%- set model_identity = \"You are a helpful assistant.\" -%}\n {%- endif -%}\n {{- model_identity }}\n {%- endif -%}\n{%- endmacro -%}\n{#- Main Template Logic ================================================= -#}\n{#- Role mapping: root -> system sp (high priority), system/developer -> developer sp (low priority) -#}\n{%- set system_message = none -%}\n{%- set developer_message = none -%}\n{%- set conversation_messages = messages -%}\n{%- if messages and messages[0].role == \"root\" -%}\n {%- set system_message = messages[0] -%}\n {%- set conversation_messages = messages[1:] -%}\n {%- if conversation_messages and conversation_messages[0].role in [\"system\", \"developer\"] -%}\n {%- set developer_message = conversation_messages[0] -%}\n {%- set conversation_messages = conversation_messages[1:] -%}\n {%- endif -%}\n{%- elif messages and messages[0].role in [\"system\", \"developer\"] -%}\n {%- set developer_message = messages[0] -%}\n {%- set conversation_messages = messages[1:] -%}\n{%- endif -%}\n{#- Render system sp (higher priority, root role only) -#}\n{{- bod_token ~ bos_token ~ 'system' ~ '\\n' }}\n{{- build_system_message(system_message) }}\n{{- eos_token ~ '\\n' }}\n\n{#- Render developer sp (lower priority: system/developer role + tools) -#}\n{{- bos_token ~ 'developer' ~ '\\n' }}\n{{- build_developer_message(developer_message) }}\n{%- if tools -%}\n {{- '\\n\\n' ~ '# Tools' ~ '\\n' ~ 'You may call one or more tools to assist with the user query.\\nHere are the tools available in JSONSchema format:' ~ '\\n' }}\n {{- '\\n' ~ '<tools>' ~ '\\n' }}\n {{- render_tool_namespace(\"functions\", tools) }}\n {{- '</tools>' ~ '\\n\\n' }}\n {{- 'To call tools, wrap all invocations in a single ' ~ toolcall_begin_token ~ toolcall_end_token ~ ' block. Parameter values containing nested objects or arrays are recursively expanded into XML elements. Example:\\n' }}\n {{- '\\n' ~ toolcall_begin_token ~ '\\n' }}\n {{- ns_token + '<invoke name=\"tool-name-1\">' }}\n {{- ns_token + '<param-1>value-1' + ns_token + '</param-1>' }}\n {{- ns_token + '<param-2>' }}\n {{- ns_token + '<item>' }}\n {{- ns_token + '<key-a>val-a' + ns_token + '</key-a>' }}\n {{- ns_token + '<key-b>val-b' + ns_token + '</key-b>' }}\n {{- ns_token + '</item>' }}\n {{- ns_token + '</param-2>' }}\n {{- ns_token + '</invoke>\\n' }}\n {{- ns_token + '<invoke name=\"tool-name-2\">' }}\n {{- ns_token + '<param-1>value-1' + ns_token + '</param-1>' }}\n {{- ns_token + '</invoke>\\n' }}\n {{- toolcall_end_token }}\n{%- endif -%}\n{{- eos_token ~ '\\n' }}\n\n{#- Render messages -#}\n{%- set last_tool_call = namespace(name=none) -%}\n{%- for message in conversation_messages -%}\n {%- if message.role == 'assistant' -%}\n {{- bos_token ~ 'ai' ~ '\\n' }}\n\n {%- set reasoning_content = '' %}\n {%- set content = visible_text(message.content) %}\n {%- if message.reasoning_content is string %}\n {%- set reasoning_content = message.reasoning_content %}\n {%- else %}\n {%- if think_end_token in content %}\n {%- set reasoning_content = content.split(think_end_token)[0].strip('\\n').split(think_begin_token)[-1].strip('\\n') %}\n {%- set content = content.split(think_end_token)[-1].strip('\\n') %}\n {%- endif %}\n {%- endif %}\n\n {%- if reasoning_content -%}\n {#- Render thinking for every assistant turn (all-turn visible) -#}\n {{- think_begin_token ~ reasoning_content ~ think_end_token }}\n {%- else -%}\n {#- No thinking rendered \u2192 prefix with think_end_token -#}\n {{- think_end_token }}\n {%- endif -%}\n\n {%- if content -%}\n {{- content }}\n {%- endif -%}\n {%- if message.tool_calls -%}\n {{- toolcall_begin_token ~ '\\n' }}\n\n {%- for tool_call in message.tool_calls -%}\n {%- if tool_call.function -%}\n {%- set tool_call = tool_call.function -%}\n {%- endif -%}\n{{- ns_token + '<invoke name=\"' + tool_call.name + '\">' }}\n{%- set _args = tool_call.arguments -%}\n{%- for k, v in _args.items() if v is not none %}\n{{- ns_token + '<' + k + '>' -}}\n{{- to_xml(v, ns_token) -}}\n{{- ns_token + '</' + k + '>' }}\n{%- endfor -%}\n{{- ns_token + '</invoke>' ~ '\\n' }}\n {%- endfor -%}\n\n {{- toolcall_end_token }}\n {%- if message.tool_calls[-1].function -%}\n {%- set last_tool_call.name = message.tool_calls[-1].function.name -%}\n {%- else -%}\n {%- set last_tool_call.name = message.tool_calls[-1].name -%}\n {%- endif -%}\n {%- else -%}\n {%- set last_tool_call.name = none -%}\n {%- endif -%}\n {{- eos_token ~ '\\n' }}\n\n {%- elif message.role == 'tool' -%}\n {%- if last_tool_call.name is none -%}\n {{- raise_exception(\"Message has tool role, but there was no previous assistant message with a tool call!\") }}\n {%- endif -%}\n {%- if loop.first or (conversation_messages[loop.index0 - 1].role != 'tool') -%}\n {{- bos_token ~ 'tool' }}\n {%- endif -%}\n {{- '\\n<response>' }}\n {%- if message.content is string -%}\n {{- message.content }}\n {%- else -%}\n {%- for tr in message.content -%}\n {%- if tr is mapping and tr.type is defined and tr.type == 'image' -%}\n {{- image_token }}\n {%- elif tr is mapping and tr.type is defined and tr.type == 'video' -%}\n {{- video_token }}\n {%- else -%}\n {{- tr.output if tr.output is defined else (tr.text if tr.type == 'text' and tr.text is defined else tr) }}\n {%- endif -%}\n {%- endfor -%}\n {%- endif -%}\n {{- '</response>' }}\n {%- if loop.last or (conversation_messages[loop.index0 + 1].role != 'tool') -%}\n {{- eos_token ~ '\\n' -}}\n {%- endif -%}\n\n {%- elif message.role == 'user' -%}\n {{- bos_token ~ 'user' ~ '\\n' }}\n {{- visible_text(message.content) }}\n {{- eos_token ~ '\\n' }}\n {%- endif -%}\n{%- endfor -%}\n\n{#- Generation prompt -#}\n{%- if add_generation_prompt -%}\n{{- bos_token ~ 'ai' ~ '\\n' }}\n{%- if thinking_mode is defined and thinking_mode == \"disabled\" -%}\n {{- think_end_token }}\n{%- elif thinking_mode is defined and thinking_mode == \"adaptive\" -%}\n {#- adaptive: no prefix, let model decide -#}\n{%- elif thinking_mode is defined and thinking_mode == \"enabled\" -%}\n {#- enabled or not defined: default to think -#}\n {{- think_begin_token }}\n{%- else -%}\n {#- adaptive: no prefix, let model decide -#}\n{%- endif -%}\n{%- endif -%}\n",
3
+ "image_processor": {
4
+ "do_convert_rgb": true,
5
+ "do_normalize": true,
6
+ "do_rescale": true,
7
+ "do_resize": true,
8
+ "image_mean": [
9
+ 0.48145466,
10
+ 0.4578275,
11
+ 0.40821073
12
+ ],
13
+ "image_processor_type": "MiniMaxM3VLImageProcessor",
14
+ "image_std": [
15
+ 0.26862954,
16
+ 0.26130258,
17
+ 0.27577711
18
+ ],
19
+ "max_pixels": 451584,
20
+ "merge_size": 2,
21
+ "min_pixels": 3136,
22
+ "patch_size": 14,
23
+ "rescale_factor": 0.00392156862745098,
24
+ "size": [
25
+ 672,
26
+ 672
27
+ ],
28
+ "temporal_patch_size": 2
29
+ },
30
+ "processor_class": "MiniMaxM3VLProcessor",
31
+ "video_processor": {
32
+ "do_convert_rgb": true,
33
+ "do_normalize": true,
34
+ "do_rescale": true,
35
+ "do_resize": true,
36
+ "do_sample_frames": false,
37
+ "fps": 1.0,
38
+ "image_mean": [
39
+ 0.48145466,
40
+ 0.4578275,
41
+ 0.40821073
42
+ ],
43
+ "image_std": [
44
+ 0.26862954,
45
+ 0.26130258,
46
+ 0.27577711
47
+ ],
48
+ "max_frames": 768,
49
+ "max_pixels": 602112,
50
+ "merge_size": 2,
51
+ "min_frames": 4,
52
+ "min_pixels": 3136,
53
+ "patch_size": 14,
54
+ "rescale_factor": 0.00392156862745098,
55
+ "size": {
56
+ "height": 672,
57
+ "width": 672
58
+ },
59
+ "temporal_patch_size": 2,
60
+ "total_pixels": 45158400,
61
+ "video_processor_type": "MiniMaxM3VLVideoProcessor"
62
+ }
63
+ }
special_tokens_map.json ADDED
@@ -0,0 +1,16 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "bos_token": {
3
+ "content": "]~b]",
4
+ "lstrip": false,
5
+ "normalized": false,
6
+ "rstrip": false,
7
+ "single_word": false
8
+ },
9
+ "eos_token": {
10
+ "content": "[e~[",
11
+ "lstrip": false,
12
+ "normalized": false,
13
+ "rstrip": false,
14
+ "single_word": false
15
+ }
16
+ }
tokenizer.json ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:ae73e31fefce04b005cb41c6781389426fae1a8553b6e58d29f133eaa31ebfb5
3
+ size 15524484
tokenizer_config.json ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "add_prefix_space": false,
3
+ "backend": "tokenizers",
4
+ "bos_token": "]~b]",
5
+ "clean_up_tokenization_spaces": false,
6
+ "eos_token": "[e~[",
7
+ "is_local": true,
8
+ "local_files_only": false,
9
+ "model_max_length": 40960000,
10
+ "pad_token": "]!p~[",
11
+ "tokenizer_class": "TokenizersBackend",
12
+ "unk_token": "[e~["
13
+ }
video_preprocessor_config.json ADDED
@@ -0,0 +1,32 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "do_convert_rgb": true,
3
+ "do_normalize": true,
4
+ "do_rescale": true,
5
+ "do_resize": true,
6
+ "do_sample_frames": false,
7
+ "fps": 1.0,
8
+ "image_mean": [
9
+ 0.48145466,
10
+ 0.4578275,
11
+ 0.40821073
12
+ ],
13
+ "image_std": [
14
+ 0.26862954,
15
+ 0.26130258,
16
+ 0.27577711
17
+ ],
18
+ "max_frames": 768,
19
+ "max_pixels": 602112,
20
+ "merge_size": 2,
21
+ "min_frames": 4,
22
+ "min_pixels": 3136,
23
+ "patch_size": 14,
24
+ "rescale_factor": 0.00392156862745098,
25
+ "size": {
26
+ "height": 672,
27
+ "width": 672
28
+ },
29
+ "temporal_patch_size": 2,
30
+ "total_pixels": 45158400,
31
+ "video_processor_type": "MiniMaxM3VLVideoProcessor"
32
+ }
video_processor.py ADDED
@@ -0,0 +1,208 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright 2023-2024 SGLang Team
2
+ # Licensed under the Apache License, Version 2.0 (the "License");
3
+ """
4
+ MiniMax VL family HuggingFace-compatible VideoProcessor.
5
+ """
6
+
7
+ import math
8
+ from typing import List, Optional, Tuple, Union
9
+
10
+ import torch
11
+ import torchvision
12
+ from torchvision.transforms import InterpolationMode
13
+ from transformers import BatchFeature
14
+ from transformers.image_utils import PILImageResampling, SizeDict
15
+ from transformers.processing_utils import (
16
+ Unpack,
17
+ VideosKwargs,
18
+ )
19
+ from transformers.utils import TensorType
20
+ from transformers.video_processing_utils import BaseVideoProcessor
21
+ from transformers.video_utils import group_videos_by_shape, reorder_videos
22
+
23
+ MAX_RATIO = 200
24
+
25
+
26
+ def round_by_factor(number: int, factor: int) -> int:
27
+ return round(number / factor) * factor
28
+
29
+
30
+ def ceil_by_factor(number: int, factor: int) -> int:
31
+ return math.ceil(number / factor) * factor
32
+
33
+
34
+ def floor_by_factor(number: int, factor: int) -> int:
35
+ return math.floor(number / factor) * factor
36
+
37
+
38
+ def smart_resize(
39
+ height: int,
40
+ width: int,
41
+ factor: int = 28,
42
+ min_pixels: int = 4 * 28 * 28,
43
+ max_pixels: int = 451584,
44
+ ) -> tuple[int, int]:
45
+ if max(height, width) / min(height, width) > MAX_RATIO:
46
+ raise ValueError(
47
+ f"absolute aspect ratio must be smaller than {MAX_RATIO}, "
48
+ f"got {max(height, width) / min(height, width)}"
49
+ )
50
+ h_bar = max(factor, round_by_factor(height, factor))
51
+ w_bar = max(factor, round_by_factor(width, factor))
52
+ if h_bar * w_bar > max_pixels:
53
+ beta = math.sqrt((height * width) / max_pixels)
54
+ h_bar = floor_by_factor(height / beta, factor)
55
+ w_bar = floor_by_factor(width / beta, factor)
56
+ elif h_bar * w_bar < min_pixels:
57
+ beta = math.sqrt(min_pixels / (height * width))
58
+ h_bar = ceil_by_factor(height * beta, factor)
59
+ w_bar = ceil_by_factor(width * beta, factor)
60
+ return h_bar, w_bar
61
+
62
+
63
+ class MiniMaxM3VLVideoProcessorKwargs(VideosKwargs, total=False):
64
+ patch_size: int
65
+ temporal_patch_size: int
66
+ merge_size: int
67
+ min_pixels: int
68
+ max_pixels: int
69
+ total_pixels: int
70
+ min_frames: int
71
+ max_frames: int
72
+ fps: float | int
73
+
74
+
75
+ class MiniMaxM3VLVideoProcessor(BaseVideoProcessor):
76
+ do_resize = True
77
+ resample = PILImageResampling.BICUBIC
78
+ size = {"height": 672, "width": 672}
79
+ default_to_square = False
80
+ do_rescale = True
81
+ rescale_factor = 1 / 255
82
+ do_normalize = True
83
+ image_mean = [0.48145466, 0.4578275, 0.40821073]
84
+ image_std = [0.26862954, 0.26130258, 0.27577711]
85
+ do_convert_rgb = True
86
+ do_sample_frames = False
87
+ patch_size = 14
88
+ temporal_patch_size = 2
89
+ merge_size = 2
90
+ min_pixels = 4 * 28 * 28
91
+ max_pixels = 768 * 28 * 28 # 602,112
92
+ total_pixels = int(64000 * 28 * 28 * 0.9) # ~45M, ~64k tokens budget
93
+ fps = 1.0
94
+ min_frames = 4
95
+ max_frames = 768
96
+ valid_kwargs = MiniMaxM3VLVideoProcessorKwargs
97
+ model_input_names = ["pixel_values_videos", "video_grid_thw"]
98
+
99
+ def __init__(self, **kwargs: Unpack[MiniMaxM3VLVideoProcessorKwargs]):
100
+ super().__init__(**kwargs)
101
+
102
+ def _preprocess(
103
+ self,
104
+ videos: List[torch.Tensor],
105
+ do_convert_rgb: bool,
106
+ do_resize: bool,
107
+ size: SizeDict,
108
+ resample: PILImageResampling | InterpolationMode | int | None,
109
+ do_rescale: bool,
110
+ rescale_factor: float,
111
+ do_normalize: bool,
112
+ image_mean: float | List[float] | None,
113
+ image_std: float | List[float] | None,
114
+ patch_size: int,
115
+ temporal_patch_size: int,
116
+ merge_size: int,
117
+ min_pixels: int,
118
+ max_pixels: int,
119
+ return_tensors: str | TensorType | None = None,
120
+ **kwargs,
121
+ ) -> BatchFeature:
122
+ grouped_videos, grouped_videos_index = group_videos_by_shape(videos)
123
+ resized_videos_grouped = {}
124
+ factor = patch_size * merge_size
125
+ for shape, stacked_videos in grouped_videos.items():
126
+ batch_size, num_frames, channels, height, width = stacked_videos.shape
127
+ resized_height, resized_width = height, width
128
+ if do_resize:
129
+ resized_height, resized_width = smart_resize(
130
+ height, width, factor=factor,
131
+ min_pixels=min_pixels, max_pixels=max_pixels,
132
+ )
133
+ stacked_videos = stacked_videos.view(
134
+ batch_size * num_frames, channels, height, width
135
+ )
136
+ stacked_videos = self.resize(
137
+ stacked_videos,
138
+ size=SizeDict(height=resized_height, width=resized_width),
139
+ resample=resample,
140
+ )
141
+ stacked_videos = stacked_videos.view(
142
+ batch_size,
143
+ num_frames,
144
+ channels,
145
+ resized_height,
146
+ resized_width,
147
+ )
148
+ resized_videos_grouped[shape] = stacked_videos
149
+ resized_videos = reorder_videos(resized_videos_grouped, grouped_videos_index)
150
+
151
+ grouped_videos, grouped_videos_index = group_videos_by_shape(resized_videos)
152
+ processed_videos_grouped = {}
153
+ processed_grids = {}
154
+ for shape, stacked_videos in grouped_videos.items():
155
+ resized_height, resized_width = stacked_videos.shape[-2:]
156
+ patches = self.rescale_and_normalize(
157
+ stacked_videos,
158
+ do_rescale,
159
+ rescale_factor,
160
+ do_normalize,
161
+ image_mean,
162
+ image_std,
163
+ )
164
+
165
+ if pad := -patches.shape[1] % temporal_patch_size:
166
+ repeats = patches[:, -1:].expand(-1, pad, -1, -1, -1)
167
+ patches = torch.cat([patches, repeats], dim=1)
168
+
169
+ batch_size, grid_t, channels = patches.shape[:3]
170
+ grid_t = grid_t // temporal_patch_size
171
+ grid_h, grid_w = resized_height // patch_size, resized_width // patch_size
172
+
173
+ patches = patches.view(
174
+ batch_size,
175
+ grid_t,
176
+ temporal_patch_size,
177
+ channels,
178
+ grid_h // merge_size,
179
+ merge_size,
180
+ patch_size,
181
+ grid_w // merge_size,
182
+ merge_size,
183
+ patch_size,
184
+ )
185
+ patches = patches.permute(0, 1, 4, 7, 5, 8, 3, 2, 6, 9)
186
+ flatten_patches = patches.reshape(
187
+ batch_size,
188
+ grid_t * grid_h * grid_w,
189
+ channels * temporal_patch_size * patch_size * patch_size,
190
+ )
191
+
192
+ processed_videos_grouped[shape] = flatten_patches
193
+ processed_grids[shape] = [[grid_t, grid_h, grid_w]] * batch_size
194
+
195
+ processed_videos = reorder_videos(
196
+ processed_videos_grouped, grouped_videos_index
197
+ )
198
+ processed_grids = reorder_videos(processed_grids, grouped_videos_index)
199
+ pixel_values_videos = torch.cat(processed_videos, dim=0)
200
+ video_grid_thw = torch.tensor(processed_grids, dtype=torch.long)
201
+
202
+ return BatchFeature(
203
+ data={
204
+ "pixel_values_videos": pixel_values_videos,
205
+ "video_grid_thw": video_grid_thw,
206
+ },
207
+ tensor_type=return_tensors,
208
+ )