Text Generation
MLX
Safetensors
kimi_k3
Mixture of Experts
reap
pruned
kimi
apple-silicon
custom_code
Instructions to use pipenetwork/Kimi-K3-REAP73-zh-code-MLX-mxfp4-q8 with libraries, inference providers, notebooks, and local apps. Follow these links to get started.
- Libraries
- MLX
How to use pipenetwork/Kimi-K3-REAP73-zh-code-MLX-mxfp4-q8 with MLX:
# Make sure mlx-lm is installed # pip install --upgrade mlx-lm # if on a CUDA device, also pip install mlx[cuda] # Generate text with mlx-lm from mlx_lm import load, generate model, tokenizer = load("pipenetwork/Kimi-K3-REAP73-zh-code-MLX-mxfp4-q8") prompt = "Once upon a time in" text = generate(model, tokenizer, prompt=prompt, verbose=True) - Notebooks
- Google Colab
- Kaggle
- Local Apps Settings
- LM Studio
- MLX LM
How to use pipenetwork/Kimi-K3-REAP73-zh-code-MLX-mxfp4-q8 with MLX LM:
Generate or start a chat session
# Install MLX LM uv tool install mlx-lm # Generate some text mlx_lm.generate --model "pipenetwork/Kimi-K3-REAP73-zh-code-MLX-mxfp4-q8" --prompt "Once upon a time"
Add files using upload-large-folder tool
Browse files- LICENSE +52 -0
- README.md +160 -0
- config.json +0 -0
- configuration_kimi_k3.py +285 -0
- encoding_k3.py +647 -0
- generation_config.json +4 -0
- kimi_k3.py +954 -0
- kimi_k3_processor.py +187 -0
- kimi_k3_vision.py +404 -0
- kimi_k3_vision_processing.py +179 -0
- kimi_k3_vl/__init__.py +14 -0
- kimi_k3_vl/config.py +85 -0
- kimi_k3_vl/kimi_k3_vl.py +191 -0
- kimi_k3_vl/language.py +46 -0
- kimi_k3_vl/vision.py +52 -0
- media_utils.py +376 -0
- model-00003-of-00075.safetensors +3 -0
- model-00006-of-00075.safetensors +3 -0
- model-00009-of-00075.safetensors +3 -0
- model-00011-of-00075.safetensors +3 -0
- model-00012-of-00075.safetensors +3 -0
- model-00014-of-00075.safetensors +3 -0
- model-00017-of-00075.safetensors +3 -0
- model-00018-of-00075.safetensors +3 -0
- model-00020-of-00075.safetensors +3 -0
- model-00025-of-00075.safetensors +3 -0
- model-00029-of-00075.safetensors +3 -0
- model-00031-of-00075.safetensors +3 -0
- model-00032-of-00075.safetensors +3 -0
- model-00034-of-00075.safetensors +3 -0
- model-00037-of-00075.safetensors +3 -0
- model-00038-of-00075.safetensors +3 -0
- model-00041-of-00075.safetensors +3 -0
- model-00042-of-00075.safetensors +3 -0
- model-00044-of-00075.safetensors +3 -0
- model-00047-of-00075.safetensors +3 -0
- model-00048-of-00075.safetensors +3 -0
- model-00053-of-00075.safetensors +3 -0
- model-00055-of-00075.safetensors +3 -0
- model-00056-of-00075.safetensors +3 -0
- model-00061-of-00075.safetensors +3 -0
- model-00064-of-00075.safetensors +3 -0
- model-00068-of-00075.safetensors +3 -0
- model-00070-of-00075.safetensors +3 -0
- model-00073-of-00075.safetensors +3 -0
- model-00075-of-00075.safetensors +3 -0
- model.safetensors.index.json +0 -0
- preprocessor_config.json +38 -0
- tokenization_kimi.py +408 -0
- tokenizer_config.json +157 -0
LICENSE
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
Kimi K3 License
|
| 2 |
+
|
| 3 |
+
Copyright (c) 2026 Moonshot AI
|
| 4 |
+
|
| 5 |
+
Permission is hereby granted, free of charge, to any person (the "Licensee")
|
| 6 |
+
obtaining a copy of this software — including the model weights, parameters,
|
| 7 |
+
configuration files, inference and training code, and associated documentation
|
| 8 |
+
(collectively, the "Software") — to deal in the Software without restriction.
|
| 9 |
+
This includes, without limitation, the rights to use, copy, modify, merge,
|
| 10 |
+
publish, distribute, sublicense, and/or sell copies of the Software; to run,
|
| 11 |
+
deploy, fine-tune, or otherwise modify the Software and create derivative works
|
| 12 |
+
from it; and to permit persons to whom the Software is furnished to do so, in
|
| 13 |
+
each case subject to the following conditions:
|
| 14 |
+
|
| 15 |
+
1. The above copyright notice and this permission notice shall be included in
|
| 16 |
+
all copies or substantial portions of the Software. Licensee's use of the
|
| 17 |
+
Software must comply with applicable laws and regulations.
|
| 18 |
+
|
| 19 |
+
2. "Model as a Service" means giving a third party access to language model
|
| 20 |
+
inference or fine-tuning (e.g., via API) in a manner that allows such third
|
| 21 |
+
party to exercise meaningful control over the inputs, parameters, or training
|
| 22 |
+
data. This does not include (a) end-user products with model capabilities solely
|
| 23 |
+
embedded within specific features or harnesses, or (b) mere relaying of requests
|
| 24 |
+
to models hosted by others.
|
| 25 |
+
|
| 26 |
+
If the Licensee or any of its affiliates operates a Model as a Service business,
|
| 27 |
+
and the aggregate revenue of the Licensee and its affiliates exceeds 20 million
|
| 28 |
+
US dollars (or the equivalent in other currencies) in total over any consecutive
|
| 29 |
+
12 months, the Licensee must enter into a separate agreement with Moonshot AI
|
| 30 |
+
before using the Software or its derivative works for any commercial purpose.
|
| 31 |
+
|
| 32 |
+
3. If the Software (or any derivative works thereof) is used for any of the
|
| 33 |
+
Licensee's commercial products or services that have more than 100 million
|
| 34 |
+
monthly active users, or more than 20 million US dollars (or equivalent in other
|
| 35 |
+
currencies) in monthly revenue, "Kimi K3" must be prominently displayed on the
|
| 36 |
+
user interface of such product or service.
|
| 37 |
+
|
| 38 |
+
4. The requirements set forth in Sections 2 and 3 do not apply to: (a) internal
|
| 39 |
+
use of the Software, defined as any use that does not make the Software, its
|
| 40 |
+
outputs, or its underlying capabilities available to third parties; or (b) any
|
| 41 |
+
use of the Software accessed through Moonshot AI's official products or
|
| 42 |
+
certified inference partners.
|
| 43 |
+
|
| 44 |
+
5. THE SOFTWARE AND ANY OUTPUT AND RESULTS THEREFROM ARE PROVIDED ON AN “AS IS”
|
| 45 |
+
BASIS, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT
|
| 46 |
+
LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE
|
| 47 |
+
AND NONINFRINGEMENT. IN NO EVENT SHALL MOONSHOT AI OR ITS AFFILIATES OR
|
| 48 |
+
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
| 49 |
+
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
| 50 |
+
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
| 51 |
+
|
| 52 |
+
For any questions regarding this license, please contact <license@moonshot.ai>.
|
README.md
ADDED
|
@@ -0,0 +1,160 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
license: other
|
| 3 |
+
license_name: kimi-k3
|
| 4 |
+
base_model: moonshotai/Kimi-K3
|
| 5 |
+
base_model_relation: quantized
|
| 6 |
+
library_name: mlx
|
| 7 |
+
pipeline_tag: text-generation
|
| 8 |
+
tags:
|
| 9 |
+
- mlx
|
| 10 |
+
- moe
|
| 11 |
+
- reap
|
| 12 |
+
- pruned
|
| 13 |
+
- kimi
|
| 14 |
+
- apple-silicon
|
| 15 |
+
---
|
| 16 |
+
|
| 17 |
+
# Kimi-K3-REAP73-zh-code-MLX-mxfp4-q8
|
| 18 |
+
|
| 19 |
+
**Kimi-K3, REAP expert-pruned (73%) and converted to MLX** — 451 GB,
|
| 20 |
+
sized to actually load on a 512 GB Apple Silicon machine.
|
| 21 |
+
|
| 22 |
+
Base: [moonshotai/Kimi-K3](https://huggingface.co/moonshotai/Kimi-K3) — 2.78T total / 104B active,
|
| 23 |
+
native-multimodal MoE with Kimi Delta Attention, Attention Residuals and a 1M
|
| 24 |
+
context.
|
| 25 |
+
|
| 26 |
+
## Why this exists
|
| 27 |
+
|
| 28 |
+
Unpruned Kimi-K3 does not fit on any Mac. The full model is 1.56 TB; even a
|
| 29 |
+
2-bit quant is ~870 GB against a 512 GB ceiling. Fitting 4-bit into 512 GB would
|
| 30 |
+
need ≤1.38 bits/weight.
|
| 31 |
+
|
| 32 |
+
REAP ([Cerebras](https://github.com/CerebrasResearch/reap)) scores each expert by
|
| 33 |
+
`gate x ||expert_output||` over a calibration set and keeps the most salient.
|
| 34 |
+
This build keeps **242 of 896 experts per layer** across 92 MoE
|
| 35 |
+
layers → **793B params, 451 GB**.
|
| 36 |
+
|
| 37 |
+
## Precision: mxfp4, and it is lossless
|
| 38 |
+
|
| 39 |
+
K3's routed experts ship from Moonshot as **MXFP4** (`weight_packed` + e8m0
|
| 40 |
+
`weight_scale`, group 32). MLX's native `mxfp4` mode uses the identical encoding,
|
| 41 |
+
so the surviving experts here are a **bit-exact byte copy of the source** — the
|
| 42 |
+
only information lost in this repo is the pruning itself, not the quantization.
|
| 43 |
+
|
| 44 |
+
Requantizing those same weights to affine 4-bit would cost ~9.8% mean relative
|
| 45 |
+
error *and* be larger (4.5 vs 4.25 bits/weight), so no affine 4-bit tier is
|
| 46 |
+
published.
|
| 47 |
+
|
| 48 |
+
Non-expert tensors (mxfp4 global config; attention, shared experts,
|
| 49 |
+
latent projections, embeddings) are carried at higher precision.
|
| 50 |
+
|
| 51 |
+
## Measured behaviour
|
| 52 |
+
|
| 53 |
+
Loaded on a 512 GiB M3 Ultra (62 s, 451 GB peak). Verbatim, greedy, unedited:
|
| 54 |
+
|
| 55 |
+
```
|
| 56 |
+
prompt: 'def merge_intervals(intervals):\n """Merge overlapping intervals."""\n'
|
| 57 |
+
--> if not intervals:
|
| 58 |
+
return []
|
| 59 |
+
intervals.sort(key=lambda x: x[0])
|
| 60 |
+
merged = [list
|
| 61 |
+
|
| 62 |
+
prompt: 'The capital of France is'
|
| 63 |
+
--> Paris",
|
| 64 |
+
+ "The capital of Germany is Berlin",
|
| 65 |
+
"The capital of Italy is Rome",
|
| 66 |
+
"The
|
| 67 |
+
|
| 68 |
+
prompt: '机器学习的基本原理是'
|
| 69 |
+
--> :通过训练数据,学习算法,然后对未知数据进行预测。机器学习的过程是:输入数据→学习算法→
|
| 70 |
+
```
|
| 71 |
+
|
| 72 |
+
Samples above are verbatim, greedy-decoded, and unedited — including the failure
|
| 73 |
+
modes. Degradation from the pruning shows up as drift into list-like or
|
| 74 |
+
source-file-like continuations rather than answering directly, and at heavier
|
| 75 |
+
prune ratios as outright repetition loops.
|
| 76 |
+
|
| 77 |
+
Code completions stay structurally correct across every ratio tested,
|
| 78 |
+
which matches the calibration data: code experts form a dense, self-similar
|
| 79 |
+
cluster (57% self-overlap in a top-242 set, versus a 27% chance baseline) and so
|
| 80 |
+
survive pruning better than more diffuse language capability.
|
| 81 |
+
|
| 82 |
+
## Speed — read this before downloading
|
| 83 |
+
|
| 84 |
+
**~0.14 tok/s** on a 512 GiB M3 Ultra. Not a typo, and not interactive.
|
| 85 |
+
|
| 86 |
+
Each decoded token reads roughly 87 GB of weights: 25.8 GB of routed
|
| 87 |
+
experts plus 60.8 GB of non-expert weights, which every token touches. This is a
|
| 88 |
+
bandwidth wall, not a headroom problem -- a 350 GB build with 160 GiB of spare
|
| 89 |
+
memory measured 0.20 tok/s versus 0.16 for a 451 GB build, i.e. gains track size
|
| 90 |
+
almost exactly. No prune ratio makes K3 interactive on this hardware.
|
| 91 |
+
|
| 92 |
+
Note the non-experts dominate per-token traffic despite being ~2% of parameters:
|
| 93 |
+
all of them are read every token, while only 16 of 242 experts are.
|
| 94 |
+
|
| 95 |
+
## Quality expectations — read this
|
| 96 |
+
|
| 97 |
+
This is an **aggressive** prune. Top-16 routing over 242 experts is
|
| 98 |
+
6.6% density, comparable to a REAP-50 of a 256-expert model. Expect
|
| 99 |
+
noticeable degradation versus full K3. It is the "fits on one machine" build, not
|
| 100 |
+
a quality build.
|
| 101 |
+
|
| 102 |
+
Two things work in its favour that a plain expert cull would not have: K3 keeps
|
| 103 |
+
**2 shared experts** that fire on every token regardless of pruning, and its
|
| 104 |
+
LatentMoE applies RMSNorm to the *combined* expert output, which partially
|
| 105 |
+
self-corrects the magnitude lost when experts are removed.
|
| 106 |
+
|
| 107 |
+
## Calibration — targeted at Chinese + code
|
| 108 |
+
|
| 109 |
+
This build is calibrated on **Chinese and code only**, not the full mixed
|
| 110 |
+
corpus. Kimi-K3's experts cluster by domain — measured over a top-242 set
|
| 111 |
+
against a 27% chance baseline, code-python↔code-multi overlap is 57.2% while
|
| 112 |
+
chinese↔code-python is 17.8%, i.e. *below* chance — so dropping the languages
|
| 113 |
+
you do not need frees expert slots for the ones you do. Saliency retained rises
|
| 114 |
+
from 59.1% (mixed) to 69.3% here at identical size.
|
| 115 |
+
|
| 116 |
+
The tradeoff is real: Japanese, Korean, Russian, Arabic, German, French and
|
| 117 |
+
Spanish are materially degraded relative to the mixed build. Use
|
| 118 |
+
`Kimi-K3-REAP73-MLX-mxfp4-q8` if you need those.
|
| 119 |
+
|
| 120 |
+
Measured against the mixed build on the same prompts: Chinese improves (the
|
| 121 |
+
mixed build drifts into restating the prompt by ~18 tokens; this one does not),
|
| 122 |
+
and code is unchanged.
|
| 123 |
+
|
| 124 |
+
Saliency was measured on a deliberately mixed 12.6 MB corpus — 40% code
|
| 125 |
+
(multi-language + real Python), 30% English web, 15% Chinese, 15% across
|
| 126 |
+
ja/ru/ko/de/fr/es/ar. The mix matters: whatever a calibration corpus
|
| 127 |
+
under-represents gets pruned away silently. An earlier attempt using C4's pooled
|
| 128 |
+
`multilingual` config left CJK at 0.03% of the corpus, which would have quietly
|
| 129 |
+
removed the experts handling Chinese.
|
| 130 |
+
|
| 131 |
+
## Usage
|
| 132 |
+
|
| 133 |
+
Requires **mlx-lm** plus the bundled `kimi_k3.py` loader (the architecture is not
|
| 134 |
+
upstream yet). On a 512 GB machine you must raise the GPU wired limit first —
|
| 135 |
+
the default is ~75% of RAM, below this model's footprint:
|
| 136 |
+
|
| 137 |
+
```bash
|
| 138 |
+
sudo sysctl iogpu.wired_limit_mb=480000
|
| 139 |
+
pip install mlx-lm
|
| 140 |
+
|
| 141 |
+
python - <<'PY'
|
| 142 |
+
import os, shutil, mlx_lm
|
| 143 |
+
from huggingface_hub import hf_hub_download
|
| 144 |
+
for f in ("kimi_k3.py",):
|
| 145 |
+
dst = os.path.join(os.path.dirname(mlx_lm.__file__), "models", f)
|
| 146 |
+
shutil.copy(hf_hub_download("pipenetwork/Kimi-K3-REAP73-zh-code-MLX-mxfp4-q8", f), dst)
|
| 147 |
+
PY
|
| 148 |
+
|
| 149 |
+
mlx_lm.generate --model pipenetwork/Kimi-K3-REAP73-zh-code-MLX-mxfp4-q8 --max-tokens 256 \
|
| 150 |
+
--prompt "Write a Python function that merges overlapping intervals."
|
| 151 |
+
```
|
| 152 |
+
|
| 153 |
+
`kimi_k3_vision.py` and `kimi_k3_vl/` ship alongside for the vision tower; the
|
| 154 |
+
image path needs mlx-vlm and is not exercised by `mlx_lm.generate`.
|
| 155 |
+
|
| 156 |
+
## Provenance
|
| 157 |
+
|
| 158 |
+
Converted with [PipeNetwork/kimi-k3-mlx](https://github.com/PipeNetwork/kimi-k3-mlx):
|
| 159 |
+
a streaming converter (the model never fits in memory at any stage) and a
|
| 160 |
+
streaming REAP calibration harness. Weights remain under the Kimi K3 License.
|
config.json
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
configuration_kimi_k3.py
ADDED
|
@@ -0,0 +1,285 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from typing import Optional
|
| 2 |
+
|
| 3 |
+
from transformers.configuration_utils import PretrainedConfig
|
| 4 |
+
|
| 5 |
+
|
| 6 |
+
class KimiLinearConfig(PretrainedConfig):
|
| 7 |
+
model_type = "kimi_linear"
|
| 8 |
+
keys_to_ignore_at_inference = ["past_key_values"]
|
| 9 |
+
|
| 10 |
+
def __init__(
|
| 11 |
+
self,
|
| 12 |
+
model_type="kimi_linear",
|
| 13 |
+
vocab_size=163840,
|
| 14 |
+
hidden_size=4096,
|
| 15 |
+
head_dim=None,
|
| 16 |
+
intermediate_size=11008,
|
| 17 |
+
num_hidden_layers=32,
|
| 18 |
+
num_attention_heads=32,
|
| 19 |
+
num_key_value_heads=None,
|
| 20 |
+
hidden_act="silu",
|
| 21 |
+
initializer_range=0.02,
|
| 22 |
+
rms_norm_eps=1e-6,
|
| 23 |
+
use_cache=True,
|
| 24 |
+
pad_token_id=0,
|
| 25 |
+
bos_token_id=1,
|
| 26 |
+
eos_token_id=2,
|
| 27 |
+
rope_theta=10000.0,
|
| 28 |
+
rope_scaling=None,
|
| 29 |
+
tie_word_embeddings=False,
|
| 30 |
+
moe_intermediate_size: Optional[int] = None,
|
| 31 |
+
moe_renormalize: bool = True,
|
| 32 |
+
moe_router_activation_func: str = "sigmoid",
|
| 33 |
+
num_experts: Optional[int] = None,
|
| 34 |
+
num_experts_per_token: Optional[int] = None,
|
| 35 |
+
num_shared_experts: int = 0,
|
| 36 |
+
routed_scaling_factor: float = 1.0,
|
| 37 |
+
first_k_dense_replace: int = 0,
|
| 38 |
+
moe_layer_freq: int = 1,
|
| 39 |
+
use_grouped_topk: bool = True,
|
| 40 |
+
num_expert_group: int = 1,
|
| 41 |
+
topk_group: int = 1,
|
| 42 |
+
q_lora_rank: Optional[int] = None,
|
| 43 |
+
kv_lora_rank: Optional[int] = None,
|
| 44 |
+
qk_nope_head_dim: Optional[int] = None,
|
| 45 |
+
qk_rope_head_dim: Optional[int] = None,
|
| 46 |
+
v_head_dim: Optional[int] = None,
|
| 47 |
+
mla_use_nope: Optional[bool] = False,
|
| 48 |
+
mla_use_output_gate: Optional[bool] = False,
|
| 49 |
+
num_nextn_predict_layers: int = 0,
|
| 50 |
+
linear_attn_config: Optional[dict] = None,
|
| 51 |
+
attn_res_block_size: Optional[int] = None,
|
| 52 |
+
latent_moe_use_norm: bool = False,
|
| 53 |
+
activation_situ_beta: Optional[float] = None,
|
| 54 |
+
activation_situ_linear_beta: Optional[float] = None,
|
| 55 |
+
max_position_embeddings: int = 4096,
|
| 56 |
+
routed_expert_hidden_size: Optional[int] = None,
|
| 57 |
+
topk_method: str = "noaux_tc",
|
| 58 |
+
**kwargs,
|
| 59 |
+
):
|
| 60 |
+
self.model_type = model_type
|
| 61 |
+
self.vocab_size = vocab_size
|
| 62 |
+
self.hidden_size = hidden_size
|
| 63 |
+
self.head_dim = (
|
| 64 |
+
head_dim if head_dim is not None else hidden_size // num_attention_heads
|
| 65 |
+
)
|
| 66 |
+
self.intermediate_size = intermediate_size
|
| 67 |
+
self.num_hidden_layers = num_hidden_layers
|
| 68 |
+
self.num_attention_heads = num_attention_heads
|
| 69 |
+
|
| 70 |
+
# for backward compatibility
|
| 71 |
+
if num_key_value_heads is None:
|
| 72 |
+
num_key_value_heads = num_attention_heads
|
| 73 |
+
|
| 74 |
+
self.num_key_value_heads = num_key_value_heads
|
| 75 |
+
self.hidden_act = hidden_act
|
| 76 |
+
self.initializer_range = initializer_range
|
| 77 |
+
self.rms_norm_eps = rms_norm_eps
|
| 78 |
+
self.use_cache = use_cache
|
| 79 |
+
self.rope_theta = rope_theta
|
| 80 |
+
self.rope_scaling = rope_scaling
|
| 81 |
+
|
| 82 |
+
self.q_lora_rank = q_lora_rank
|
| 83 |
+
self.kv_lora_rank = kv_lora_rank
|
| 84 |
+
self.qk_nope_head_dim = qk_nope_head_dim
|
| 85 |
+
self.qk_rope_head_dim = qk_rope_head_dim
|
| 86 |
+
self.v_head_dim = v_head_dim
|
| 87 |
+
self.mla_use_nope = mla_use_nope
|
| 88 |
+
self.mla_use_output_gate = mla_use_output_gate
|
| 89 |
+
# moe config
|
| 90 |
+
self.num_experts = num_experts
|
| 91 |
+
self.num_experts_per_token = num_experts_per_token
|
| 92 |
+
self.moe_renormalize = moe_renormalize
|
| 93 |
+
self.num_shared_experts = num_shared_experts
|
| 94 |
+
self.routed_scaling_factor = routed_scaling_factor
|
| 95 |
+
self.moe_router_activation_func = moe_router_activation_func
|
| 96 |
+
assert self.moe_router_activation_func in ("softmax", "sigmoid")
|
| 97 |
+
self.moe_intermediate_size = moe_intermediate_size
|
| 98 |
+
self.first_k_dense_replace = first_k_dense_replace
|
| 99 |
+
self.moe_layer_freq = moe_layer_freq
|
| 100 |
+
self.use_grouped_topk = use_grouped_topk
|
| 101 |
+
self.num_expert_group = num_expert_group
|
| 102 |
+
self.topk_group = topk_group
|
| 103 |
+
self.num_nextn_predict_layers = num_nextn_predict_layers
|
| 104 |
+
|
| 105 |
+
self.attn_res_block_size = attn_res_block_size
|
| 106 |
+
self.latent_moe_use_norm = latent_moe_use_norm
|
| 107 |
+
self.activation_situ_beta = activation_situ_beta
|
| 108 |
+
self.activation_situ_linear_beta = activation_situ_linear_beta
|
| 109 |
+
self.max_position_embeddings = max_position_embeddings
|
| 110 |
+
self.routed_expert_hidden_size = routed_expert_hidden_size
|
| 111 |
+
self.topk_method = topk_method
|
| 112 |
+
|
| 113 |
+
if linear_attn_config is not None:
|
| 114 |
+
assert linear_attn_config["kda_layers"] is not None
|
| 115 |
+
assert linear_attn_config["full_attn_layers"] is not None
|
| 116 |
+
self.linear_attn_config = linear_attn_config
|
| 117 |
+
|
| 118 |
+
super().__init__(
|
| 119 |
+
pad_token_id=pad_token_id,
|
| 120 |
+
bos_token_id=bos_token_id,
|
| 121 |
+
eos_token_id=eos_token_id,
|
| 122 |
+
tie_word_embeddings=tie_word_embeddings,
|
| 123 |
+
**kwargs,
|
| 124 |
+
)
|
| 125 |
+
|
| 126 |
+
@property
|
| 127 |
+
def is_mla(self):
|
| 128 |
+
return (
|
| 129 |
+
self.q_lora_rank is not None
|
| 130 |
+
or self.kv_lora_rank is not None
|
| 131 |
+
or self.qk_nope_head_dim is not None
|
| 132 |
+
or self.qk_rope_head_dim is not None
|
| 133 |
+
or self.v_head_dim is not None
|
| 134 |
+
or self.mla_use_nope is True
|
| 135 |
+
)
|
| 136 |
+
|
| 137 |
+
@property
|
| 138 |
+
def is_moe(self):
|
| 139 |
+
return self.num_experts is not None
|
| 140 |
+
|
| 141 |
+
@property
|
| 142 |
+
def is_linear_attn(self) -> bool:
|
| 143 |
+
return not (
|
| 144 |
+
self.linear_attn_config is None
|
| 145 |
+
or (
|
| 146 |
+
isinstance(self.linear_attn_config, dict)
|
| 147 |
+
and self.linear_attn_config["kda_layers"] is not None
|
| 148 |
+
and len(self.linear_attn_config["kda_layers"]) == 0
|
| 149 |
+
)
|
| 150 |
+
)
|
| 151 |
+
|
| 152 |
+
def is_kda_layer(self, layer_idx: int):
|
| 153 |
+
return (
|
| 154 |
+
self.linear_attn_config is not None
|
| 155 |
+
and (layer_idx + 1) in self.linear_attn_config["kda_layers"]
|
| 156 |
+
)
|
| 157 |
+
|
| 158 |
+
|
| 159 |
+
class KimiK3VisionConfig(PretrainedConfig):
|
| 160 |
+
|
| 161 |
+
def __init__(
|
| 162 |
+
self,
|
| 163 |
+
patch_size: int = 14,
|
| 164 |
+
init_pos_emb_height: int = 64,
|
| 165 |
+
init_pos_emb_width: int = 64,
|
| 166 |
+
init_pos_emb_time: int = 4,
|
| 167 |
+
pos_emb_type: str = 'divided_fixed',
|
| 168 |
+
vt_num_attention_heads: int = 12,
|
| 169 |
+
vt_num_hidden_layers: int = 27,
|
| 170 |
+
vt_hidden_size: int = 1024,
|
| 171 |
+
vt_intermediate_size: int = 4096,
|
| 172 |
+
merge_kernel_size: tuple = (2, 2),
|
| 173 |
+
merge_type: str = 'sd2_tpool',
|
| 174 |
+
_attn_implementation: str = 'flash_attention_2',
|
| 175 |
+
# MM Projector parameters
|
| 176 |
+
mm_projector_type: str = 'patchmergerv2',
|
| 177 |
+
mm_hidden_size: int | None = None,
|
| 178 |
+
projector_hidden_act: str = "gelu",
|
| 179 |
+
projector_ln_eps: float = 1e-5,
|
| 180 |
+
# vision tower parameters
|
| 181 |
+
qkv_hidden_size: int = 1536,
|
| 182 |
+
norm_type: str = 'rmsnorm',
|
| 183 |
+
attn_bias: bool = False,
|
| 184 |
+
patch_embed_proj_bias: bool = False,
|
| 185 |
+
mlp_type: str = 'mlp2',
|
| 186 |
+
linear_bias: bool = False,
|
| 187 |
+
activation_func: str = 'gelu_pytorch_tanh',
|
| 188 |
+
pos_emb_interpolation_mode: str = 'bilinear',
|
| 189 |
+
# Other parameters
|
| 190 |
+
ignore_index: int = -100,
|
| 191 |
+
media_placeholder_token_id: int = 163605,
|
| 192 |
+
pad_token_id: int = 0,
|
| 193 |
+
text_hidden_size=7168,
|
| 194 |
+
**kwargs):
|
| 195 |
+
|
| 196 |
+
self.patch_size = patch_size
|
| 197 |
+
self.init_pos_emb_height = init_pos_emb_height
|
| 198 |
+
self.init_pos_emb_width = init_pos_emb_width
|
| 199 |
+
self.init_pos_emb_time = init_pos_emb_time
|
| 200 |
+
self.pos_emb_type = pos_emb_type
|
| 201 |
+
self.vt_num_attention_heads = vt_num_attention_heads
|
| 202 |
+
self.vt_num_hidden_layers = vt_num_hidden_layers
|
| 203 |
+
self.vt_hidden_size = vt_hidden_size
|
| 204 |
+
self.vt_intermediate_size = vt_intermediate_size
|
| 205 |
+
self.merge_kernel_size = merge_kernel_size
|
| 206 |
+
self.merge_type = merge_type
|
| 207 |
+
self._attn_implementation = _attn_implementation
|
| 208 |
+
|
| 209 |
+
# MM Projector config
|
| 210 |
+
self.mm_projector_type = mm_projector_type
|
| 211 |
+
self.mm_hidden_size = mm_hidden_size if mm_hidden_size is not None else vt_hidden_size
|
| 212 |
+
self.projector_hidden_act = projector_hidden_act
|
| 213 |
+
self.projector_ln_eps = projector_ln_eps
|
| 214 |
+
self.text_hidden_size = text_hidden_size
|
| 215 |
+
|
| 216 |
+
# vision tower parameters
|
| 217 |
+
self.qkv_hidden_size = qkv_hidden_size
|
| 218 |
+
self.norm_type = norm_type
|
| 219 |
+
self.attn_bias = attn_bias
|
| 220 |
+
self.patch_embed_proj_bias = patch_embed_proj_bias
|
| 221 |
+
self.mlp_type = mlp_type
|
| 222 |
+
self.linear_bias = linear_bias
|
| 223 |
+
self.activation_func = activation_func
|
| 224 |
+
self.pos_emb_interpolation_mode = pos_emb_interpolation_mode
|
| 225 |
+
|
| 226 |
+
super().__init__(**kwargs)
|
| 227 |
+
|
| 228 |
+
|
| 229 |
+
class KimiK3Config(PretrainedConfig):
|
| 230 |
+
"""Kimi-K3 model configuration.
|
| 231 |
+
|
| 232 |
+
Args:
|
| 233 |
+
text_config (dict | KimiLinearConfig): Configuration for the text model.
|
| 234 |
+
|
| 235 |
+
Vision Tower Parameters (from MoonViT3dConfig):
|
| 236 |
+
patch_size (int): Patch size for vision tower.
|
| 237 |
+
init_pos_emb_height (int): Initial position embedding height.
|
| 238 |
+
init_pos_emb_width (int): Initial position embedding width.
|
| 239 |
+
init_pos_emb_time (int): Initial position embedding time dimension.
|
| 240 |
+
pos_emb_type (str): Type of position embedding.
|
| 241 |
+
vt_num_attention_heads (int): Number of attention heads in vision tower.
|
| 242 |
+
vt_num_hidden_layers (int): Number of hidden layers in vision tower.
|
| 243 |
+
vt_hidden_size (int): Hidden size of vision tower.
|
| 244 |
+
vt_intermediate_size (int): Intermediate size in vision tower FFN.
|
| 245 |
+
merge_kernel_size (tuple): Kernel size for patch merging.
|
| 246 |
+
merge_type (str): Type of merge operation.
|
| 247 |
+
_attn_implementation (str): Attention implementation type.
|
| 248 |
+
|
| 249 |
+
MM Projector Parameters (from MultiModalProjectorConfig):
|
| 250 |
+
mm_projector_type (str): Type of multimodal projector.
|
| 251 |
+
mm_hidden_size (int): Hidden size from vision tower (should match vt_hidden_size).
|
| 252 |
+
projector_hidden_act (str): Activation function for projector.
|
| 253 |
+
projector_ln_eps (float): Layer norm epsilon for projector.
|
| 254 |
+
|
| 255 |
+
Other Parameters:
|
| 256 |
+
ignore_index (int): The ignore index for the loss function.
|
| 257 |
+
media_placeholder_token_id (int): The token ID to use for media placeholders.
|
| 258 |
+
pad_token_id (int): The token ID to use for padding.
|
| 259 |
+
"""
|
| 260 |
+
|
| 261 |
+
model_type = "kimi_k3"
|
| 262 |
+
|
| 263 |
+
def __init__(
|
| 264 |
+
self,
|
| 265 |
+
text_config: dict | KimiLinearConfig = None,
|
| 266 |
+
vision_config: dict | KimiK3VisionConfig = None,
|
| 267 |
+
# Other parameters
|
| 268 |
+
ignore_index: int = -100,
|
| 269 |
+
media_placeholder_token_id: int = 163605,
|
| 270 |
+
pad_token_id: int = 0,
|
| 271 |
+
**kwargs,
|
| 272 |
+
):
|
| 273 |
+
if isinstance(text_config, dict):
|
| 274 |
+
text_config = KimiLinearConfig(**text_config)
|
| 275 |
+
if isinstance(vision_config, dict):
|
| 276 |
+
vision_config = KimiK3VisionConfig(**vision_config)
|
| 277 |
+
self.text_config = text_config
|
| 278 |
+
self.vision_config = vision_config
|
| 279 |
+
# Other config
|
| 280 |
+
self.ignore_index = ignore_index
|
| 281 |
+
self.media_placeholder_token_id = media_placeholder_token_id
|
| 282 |
+
if getattr(self.text_config, "quantization_config", None) is not None:
|
| 283 |
+
self.quantization_config = self.text_config.quantization_config
|
| 284 |
+
|
| 285 |
+
super().__init__(pad_token_id=pad_token_id, **kwargs)
|
encoding_k3.py
ADDED
|
@@ -0,0 +1,647 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Kimi K3 XTML encoding helpers.
|
| 2 |
+
|
| 3 |
+
This module keeps chat rendering in Python.
|
| 4 |
+
Callers that need token IDs should consume ``EncodeSegment`` objects directly:
|
| 5 |
+
structural markers may be encoded as tiktoken special tokens, while user/tool
|
| 6 |
+
text and attribute values are encoded as ordinary text.
|
| 7 |
+
"""
|
| 8 |
+
|
| 9 |
+
from __future__ import annotations
|
| 10 |
+
|
| 11 |
+
import json
|
| 12 |
+
from dataclasses import dataclass
|
| 13 |
+
from typing import Any, Iterable, Optional
|
| 14 |
+
|
| 15 |
+
OPEN_TOKEN = "<|open|>"
|
| 16 |
+
CLOSE_TOKEN = "<|close|>"
|
| 17 |
+
SEP_TOKEN = "<|sep|>"
|
| 18 |
+
END_OF_MSG_TOKEN = "<|end_of_msg|>"
|
| 19 |
+
IMAGE_PLACEHOLDER = "<|kimi_image_placeholder|>"
|
| 20 |
+
|
| 21 |
+
_VALID_THINKING_EFFORTS = {"low", "high", "max"}
|
| 22 |
+
|
| 23 |
+
|
| 24 |
+
@dataclass(frozen=True)
|
| 25 |
+
class EncodeSegment:
|
| 26 |
+
text: str
|
| 27 |
+
allow_special: bool = False
|
| 28 |
+
|
| 29 |
+
|
| 30 |
+
class _ImagePromptState:
|
| 31 |
+
def __init__(self, image_prompts: Optional[list[str]] = None):
|
| 32 |
+
self.image_prompts = image_prompts
|
| 33 |
+
self.index = 0
|
| 34 |
+
|
| 35 |
+
def next_prompt(self) -> str:
|
| 36 |
+
if self.image_prompts is None:
|
| 37 |
+
return IMAGE_PLACEHOLDER
|
| 38 |
+
if self.index >= len(self.image_prompts):
|
| 39 |
+
raise ValueError("More image placeholders than image prompts.")
|
| 40 |
+
prompt = self.image_prompts[self.index]
|
| 41 |
+
self.index += 1
|
| 42 |
+
return prompt
|
| 43 |
+
|
| 44 |
+
def assert_consumed(self) -> None:
|
| 45 |
+
if self.image_prompts is None:
|
| 46 |
+
return
|
| 47 |
+
if self.index != len(self.image_prompts):
|
| 48 |
+
raise ValueError(
|
| 49 |
+
f"image prompt count {len(self.image_prompts)} != "
|
| 50 |
+
f"consumed placeholder count {self.index}"
|
| 51 |
+
)
|
| 52 |
+
|
| 53 |
+
|
| 54 |
+
def _segment(text: Any, *, allow_special: bool = False) -> list[EncodeSegment]:
|
| 55 |
+
text = str(text)
|
| 56 |
+
if not text:
|
| 57 |
+
return []
|
| 58 |
+
return [EncodeSegment(text, allow_special=allow_special)]
|
| 59 |
+
|
| 60 |
+
|
| 61 |
+
def _control(text: str) -> list[EncodeSegment]:
|
| 62 |
+
return _segment(text, allow_special=True)
|
| 63 |
+
|
| 64 |
+
|
| 65 |
+
def _text(text: Any) -> list[EncodeSegment]:
|
| 66 |
+
return _segment(text, allow_special=False)
|
| 67 |
+
|
| 68 |
+
|
| 69 |
+
def _append_text(
|
| 70 |
+
segments: list[EncodeSegment],
|
| 71 |
+
text: Any,
|
| 72 |
+
image_state: _ImagePromptState,
|
| 73 |
+
) -> None:
|
| 74 |
+
text = str(text)
|
| 75 |
+
if text == "":
|
| 76 |
+
return
|
| 77 |
+
if image_state.image_prompts is None or IMAGE_PLACEHOLDER not in text:
|
| 78 |
+
segments.extend(_text(text))
|
| 79 |
+
return
|
| 80 |
+
|
| 81 |
+
parts = text.split(IMAGE_PLACEHOLDER)
|
| 82 |
+
for i, part in enumerate(parts):
|
| 83 |
+
segments.extend(_text(part))
|
| 84 |
+
if i < len(parts) - 1:
|
| 85 |
+
segments.extend(_segment(image_state.next_prompt(),
|
| 86 |
+
allow_special=True))
|
| 87 |
+
|
| 88 |
+
|
| 89 |
+
def _escape_attr_value(value: Any) -> str:
|
| 90 |
+
return str(value).replace("&", "&").replace('"', """)
|
| 91 |
+
|
| 92 |
+
|
| 93 |
+
def _attr(key: str, value: Any) -> list[EncodeSegment]:
|
| 94 |
+
return (
|
| 95 |
+
_text(f" {key}")
|
| 96 |
+
+ _text('="')
|
| 97 |
+
+ _text(_escape_attr_value(value))
|
| 98 |
+
+ _text('"')
|
| 99 |
+
)
|
| 100 |
+
|
| 101 |
+
|
| 102 |
+
def _open_tag(tag: str, attrs: Iterable[tuple[str, Any]] = ()) -> list[EncodeSegment]:
|
| 103 |
+
segments: list[EncodeSegment] = []
|
| 104 |
+
segments.extend(_control(OPEN_TOKEN))
|
| 105 |
+
segments.extend(_text(tag))
|
| 106 |
+
for key, value in attrs:
|
| 107 |
+
segments.extend(_attr(key, value))
|
| 108 |
+
segments.extend(_control(SEP_TOKEN))
|
| 109 |
+
return segments
|
| 110 |
+
|
| 111 |
+
def _close_tag(tag: str) -> list[EncodeSegment]:
|
| 112 |
+
segments: list[EncodeSegment] = []
|
| 113 |
+
segments.extend(_control(CLOSE_TOKEN))
|
| 114 |
+
segments.extend(_text(tag))
|
| 115 |
+
segments.extend(_control(SEP_TOKEN))
|
| 116 |
+
return segments
|
| 117 |
+
|
| 118 |
+
|
| 119 |
+
def _end_of_msg() -> list[EncodeSegment]:
|
| 120 |
+
return _control(END_OF_MSG_TOKEN)
|
| 121 |
+
|
| 122 |
+
|
| 123 |
+
def _json_compact(value: Any) -> str:
|
| 124 |
+
return json.dumps(value, ensure_ascii=False, separators=(",", ":"))
|
| 125 |
+
|
| 126 |
+
|
| 127 |
+
def _is_mapping(value: Any) -> bool:
|
| 128 |
+
return isinstance(value, dict)
|
| 129 |
+
|
| 130 |
+
|
| 131 |
+
def _xtml_type(value: Any) -> str:
|
| 132 |
+
if isinstance(value, bool):
|
| 133 |
+
return "boolean"
|
| 134 |
+
if value is None:
|
| 135 |
+
return "null"
|
| 136 |
+
if isinstance(value, (int, float)) and not isinstance(value, bool):
|
| 137 |
+
return "number"
|
| 138 |
+
if isinstance(value, str):
|
| 139 |
+
return "string"
|
| 140 |
+
if _is_mapping(value):
|
| 141 |
+
return "object"
|
| 142 |
+
return "array"
|
| 143 |
+
|
| 144 |
+
|
| 145 |
+
def _xtml_value(value: Any) -> str:
|
| 146 |
+
if isinstance(value, str):
|
| 147 |
+
return value
|
| 148 |
+
return json.dumps(value, ensure_ascii=False)
|
| 149 |
+
|
| 150 |
+
|
| 151 |
+
def _get_value(obj: Any, key: str, default: Any = None) -> Any:
|
| 152 |
+
if isinstance(obj, dict):
|
| 153 |
+
return obj.get(key, default)
|
| 154 |
+
return getattr(obj, key, default)
|
| 155 |
+
|
| 156 |
+
|
| 157 |
+
def extract_response_schema(response_format: Any) -> Any:
|
| 158 |
+
if response_format is None:
|
| 159 |
+
return None
|
| 160 |
+
|
| 161 |
+
json_schema = _get_value(response_format, "json_schema")
|
| 162 |
+
if json_schema is None:
|
| 163 |
+
return None
|
| 164 |
+
|
| 165 |
+
if isinstance(json_schema, dict):
|
| 166 |
+
return json_schema.get(
|
| 167 |
+
"schema",
|
| 168 |
+
json_schema.get("json_schema", json_schema),
|
| 169 |
+
)
|
| 170 |
+
|
| 171 |
+
schema = _get_value(json_schema, "schema")
|
| 172 |
+
if schema is not None:
|
| 173 |
+
return schema
|
| 174 |
+
|
| 175 |
+
schema = _get_value(json_schema, "json_schema")
|
| 176 |
+
if schema is not None:
|
| 177 |
+
return schema
|
| 178 |
+
|
| 179 |
+
return json_schema
|
| 180 |
+
|
| 181 |
+
|
| 182 |
+
def deep_sort_dict(obj: Any) -> Any:
|
| 183 |
+
if isinstance(obj, dict):
|
| 184 |
+
return {k: deep_sort_dict(v) for k, v in sorted(obj.items())}
|
| 185 |
+
if isinstance(obj, list):
|
| 186 |
+
return [deep_sort_dict(item) for item in obj]
|
| 187 |
+
return obj
|
| 188 |
+
|
| 189 |
+
|
| 190 |
+
def normalize_tool_arguments(arguments: Any) -> tuple[dict[str, Any], Optional[str]]:
|
| 191 |
+
if arguments is None:
|
| 192 |
+
return {}, None
|
| 193 |
+
if isinstance(arguments, dict):
|
| 194 |
+
return arguments, None
|
| 195 |
+
if isinstance(arguments, str):
|
| 196 |
+
if not arguments.strip():
|
| 197 |
+
return {}, None
|
| 198 |
+
try:
|
| 199 |
+
parsed = json.loads(arguments)
|
| 200 |
+
except json.JSONDecodeError:
|
| 201 |
+
return {}, arguments
|
| 202 |
+
if not isinstance(parsed, dict):
|
| 203 |
+
raise ValueError("Kimi K3 tool call arguments must be a JSON object.")
|
| 204 |
+
return parsed, None
|
| 205 |
+
raise TypeError(
|
| 206 |
+
"Kimi K3 tool call arguments must be a dict or a JSON object string."
|
| 207 |
+
)
|
| 208 |
+
|
| 209 |
+
|
| 210 |
+
def normalize_message(message: Any) -> Any:
|
| 211 |
+
if not isinstance(message, dict):
|
| 212 |
+
return message
|
| 213 |
+
|
| 214 |
+
normalized = dict(message)
|
| 215 |
+
|
| 216 |
+
tools = normalized.get("tools")
|
| 217 |
+
if tools is not None:
|
| 218 |
+
normalized["tools"] = deep_sort_dict(tools)
|
| 219 |
+
|
| 220 |
+
tool_calls = normalized.get("tool_calls")
|
| 221 |
+
if not tool_calls:
|
| 222 |
+
return normalized
|
| 223 |
+
|
| 224 |
+
normalized_calls = []
|
| 225 |
+
for tool_call in tool_calls:
|
| 226 |
+
if not isinstance(tool_call, dict):
|
| 227 |
+
normalized_calls.append(tool_call)
|
| 228 |
+
continue
|
| 229 |
+
|
| 230 |
+
tc = dict(tool_call)
|
| 231 |
+
function = tc.get("function")
|
| 232 |
+
if isinstance(function, dict):
|
| 233 |
+
fn = dict(function)
|
| 234 |
+
arguments, json_block = normalize_tool_arguments(fn.get("arguments"))
|
| 235 |
+
fn["arguments"] = arguments
|
| 236 |
+
if json_block is None:
|
| 237 |
+
fn.pop("_xtml_json_block", None)
|
| 238 |
+
else:
|
| 239 |
+
fn["_xtml_json_block"] = json_block
|
| 240 |
+
tc["function"] = fn
|
| 241 |
+
else:
|
| 242 |
+
arguments, json_block = normalize_tool_arguments(tc.get("arguments"))
|
| 243 |
+
tc["arguments"] = arguments
|
| 244 |
+
if json_block is None:
|
| 245 |
+
tc.pop("_xtml_json_block", None)
|
| 246 |
+
else:
|
| 247 |
+
tc["_xtml_json_block"] = json_block
|
| 248 |
+
normalized_calls.append(tc)
|
| 249 |
+
|
| 250 |
+
normalized["tool_calls"] = normalized_calls
|
| 251 |
+
return normalized
|
| 252 |
+
|
| 253 |
+
|
| 254 |
+
def normalize_conversation(conversation: Any) -> Any:
|
| 255 |
+
if not isinstance(conversation, list):
|
| 256 |
+
return conversation
|
| 257 |
+
|
| 258 |
+
def normalize_messages(messages: list[Any]) -> list[Any]:
|
| 259 |
+
return [normalize_message(message) for message in messages]
|
| 260 |
+
|
| 261 |
+
if conversation and isinstance(conversation[0], list):
|
| 262 |
+
return [normalize_messages(messages) for messages in conversation]
|
| 263 |
+
return normalize_messages(conversation)
|
| 264 |
+
|
| 265 |
+
|
| 266 |
+
def _tool_call_id_index(tool_calls: Any) -> dict:
|
| 267 |
+
"""Map assistant ``tool_calls[].id`` to ``(1-based position, function name)``.
|
| 268 |
+
|
| 269 |
+
The position mirrors the chat template's enumeration over ``tool_calls``
|
| 270 |
+
(every entry advances the position, even an id-less one). Duplicate ids keep
|
| 271 |
+
their first occurrence.
|
| 272 |
+
"""
|
| 273 |
+
index: dict = {}
|
| 274 |
+
if not isinstance(tool_calls, list):
|
| 275 |
+
return index
|
| 276 |
+
for position, tool_call in enumerate(tool_calls, start=1):
|
| 277 |
+
if not isinstance(tool_call, dict):
|
| 278 |
+
continue
|
| 279 |
+
call_id = tool_call.get("id")
|
| 280 |
+
if call_id is None:
|
| 281 |
+
continue
|
| 282 |
+
key = str(call_id)
|
| 283 |
+
if key in index:
|
| 284 |
+
continue
|
| 285 |
+
function = tool_call.get("function")
|
| 286 |
+
name = (
|
| 287 |
+
function.get("name") if isinstance(function, dict) else tool_call.get("name")
|
| 288 |
+
)
|
| 289 |
+
index[key] = (position, name)
|
| 290 |
+
return index
|
| 291 |
+
|
| 292 |
+
|
| 293 |
+
def normalize_xtml_tool_result_messages(messages: list[Any]) -> list[Any]:
|
| 294 |
+
"""Re-sort K3 XTML tool results into assistant ``tool_calls`` order.
|
| 295 |
+
|
| 296 |
+
Serving frameworks generally deliver tool results already in call order. A
|
| 297 |
+
direct Transformers caller, however, may pass OpenAI-style tool messages in any
|
| 298 |
+
order, so each run of consecutive tool messages is matched against the most
|
| 299 |
+
recent preceding assistant ``tool_calls`` by opaque ``tool_call_id`` ==
|
| 300 |
+
``tool_calls[].id`` (K3 drops the ``func:index`` format requirement) and
|
| 301 |
+
sorted by the matched 1-based position. The matched call is authoritative,
|
| 302 |
+
so each matched message's ``tool`` is set to that call's function name --
|
| 303 |
+
this keeps an explicit (and possibly stale) ``tool``/``name`` from drifting
|
| 304 |
+
out of sync with the reordered position. ``index`` is still derived from the
|
| 305 |
+
rendered position by the chat template. A run that cannot be fully matched is
|
| 306 |
+
left untouched. Re-running is idempotent.
|
| 307 |
+
|
| 308 |
+
This function is side-effect free: matched tool messages are shallow-copied
|
| 309 |
+
before their ``tool``/``name`` is rewritten, and every other message is
|
| 310 |
+
appended to the output as-is. The input list and its message objects are
|
| 311 |
+
never mutated.
|
| 312 |
+
"""
|
| 313 |
+
if not isinstance(messages, list):
|
| 314 |
+
return messages
|
| 315 |
+
|
| 316 |
+
output: list[Any] = []
|
| 317 |
+
current_index: dict = {}
|
| 318 |
+
i = 0
|
| 319 |
+
n = len(messages)
|
| 320 |
+
|
| 321 |
+
while i < n:
|
| 322 |
+
message = messages[i]
|
| 323 |
+
|
| 324 |
+
if isinstance(message, dict) and message.get("role") == "assistant":
|
| 325 |
+
tool_calls = message.get("tool_calls")
|
| 326 |
+
current_index = _tool_call_id_index(tool_calls) if tool_calls else {}
|
| 327 |
+
output.append(message)
|
| 328 |
+
i += 1
|
| 329 |
+
continue
|
| 330 |
+
|
| 331 |
+
if not isinstance(message, dict) or message.get("role") != "tool":
|
| 332 |
+
output.append(message)
|
| 333 |
+
i += 1
|
| 334 |
+
continue
|
| 335 |
+
|
| 336 |
+
run: list[tuple] = [] # (position, original_offset, message, name)
|
| 337 |
+
unresolved = False
|
| 338 |
+
offset = 0
|
| 339 |
+
while (
|
| 340 |
+
i < n and isinstance(messages[i], dict) and messages[i].get("role") == "tool"
|
| 341 |
+
):
|
| 342 |
+
tool_message = messages[i]
|
| 343 |
+
call_id = tool_message.get("tool_call_id", tool_message.get("id"))
|
| 344 |
+
matched = current_index.get(str(call_id)) if call_id is not None else None
|
| 345 |
+
if matched is None:
|
| 346 |
+
unresolved = True
|
| 347 |
+
run.append((None, offset, tool_message, None))
|
| 348 |
+
else:
|
| 349 |
+
position, name = matched
|
| 350 |
+
run.append((position, offset, tool_message, name))
|
| 351 |
+
offset += 1
|
| 352 |
+
i += 1
|
| 353 |
+
|
| 354 |
+
if unresolved:
|
| 355 |
+
output.extend(item[2] for item in run)
|
| 356 |
+
else:
|
| 357 |
+
run.sort(key=lambda item: (item[0], item[1]))
|
| 358 |
+
for _, _, tool_message, name in run:
|
| 359 |
+
if name is None:
|
| 360 |
+
output.append(tool_message)
|
| 361 |
+
continue
|
| 362 |
+
# The id-matched call is authoritative: align tool (and any
|
| 363 |
+
# explicit name) so the rendered XTML tool attribute cannot
|
| 364 |
+
# disagree with the reordered position. Copy first so the
|
| 365 |
+
# caller's message object is never mutated.
|
| 366 |
+
resolved = dict(tool_message)
|
| 367 |
+
resolved["tool"] = name
|
| 368 |
+
if "name" in resolved:
|
| 369 |
+
resolved["name"] = name
|
| 370 |
+
output.append(resolved)
|
| 371 |
+
|
| 372 |
+
return output
|
| 373 |
+
|
| 374 |
+
|
| 375 |
+
def is_batched_conversation(conversation: Any) -> bool:
|
| 376 |
+
return (
|
| 377 |
+
isinstance(conversation, list)
|
| 378 |
+
and bool(conversation)
|
| 379 |
+
and isinstance(conversation[0], list)
|
| 380 |
+
)
|
| 381 |
+
|
| 382 |
+
|
| 383 |
+
def _render_content_segments(
|
| 384 |
+
content: Any,
|
| 385 |
+
image_state: _ImagePromptState,
|
| 386 |
+
) -> list[EncodeSegment]:
|
| 387 |
+
segments: list[EncodeSegment] = []
|
| 388 |
+
if isinstance(content, str):
|
| 389 |
+
_append_text(segments, content, image_state)
|
| 390 |
+
elif content is not None:
|
| 391 |
+
for part in content:
|
| 392 |
+
if part["type"] in ["image", "image_url"]:
|
| 393 |
+
segments.extend(
|
| 394 |
+
_segment(image_state.next_prompt(), allow_special=True))
|
| 395 |
+
else:
|
| 396 |
+
_append_text(segments, part["text"], image_state)
|
| 397 |
+
return segments
|
| 398 |
+
|
| 399 |
+
|
| 400 |
+
def _internal_system_message(message_type: str, body: str) -> list[EncodeSegment]:
|
| 401 |
+
segments: list[EncodeSegment] = []
|
| 402 |
+
segments.extend(_open_tag("message", [("role", "system"), ("type", message_type)]))
|
| 403 |
+
segments.extend(_text(body.strip()))
|
| 404 |
+
segments.extend(_close_tag("message"))
|
| 405 |
+
segments.extend(_end_of_msg())
|
| 406 |
+
return segments
|
| 407 |
+
|
| 408 |
+
|
| 409 |
+
def _render_assistant_segments(
|
| 410 |
+
message: dict[str, Any],
|
| 411 |
+
image_state: _ImagePromptState,
|
| 412 |
+
thinking: bool = True,
|
| 413 |
+
) -> list[EncodeSegment]:
|
| 414 |
+
segments: list[EncodeSegment] = []
|
| 415 |
+
# The <think> channel is structural: in thinking mode every assistant
|
| 416 |
+
# message carries the open/close tags even when there is no reasoning
|
| 417 |
+
# content to fill in. In non-thinking mode the channel is dropped
|
| 418 |
+
# entirely.
|
| 419 |
+
if thinking:
|
| 420 |
+
reasoning_content = message.get("reasoning_content") or message.get(
|
| 421 |
+
"reasoning"
|
| 422 |
+
)
|
| 423 |
+
segments.extend(_open_tag("think"))
|
| 424 |
+
if reasoning_content is not None and str(reasoning_content).strip():
|
| 425 |
+
_append_text(segments, reasoning_content, image_state)
|
| 426 |
+
segments.extend(_close_tag("think"))
|
| 427 |
+
|
| 428 |
+
segments.extend(_open_tag("response"))
|
| 429 |
+
segments.extend(_render_content_segments(message.get("content"), image_state))
|
| 430 |
+
segments.extend(_close_tag("response"))
|
| 431 |
+
|
| 432 |
+
tool_calls = message.get("tool_calls")
|
| 433 |
+
if tool_calls:
|
| 434 |
+
segments.extend(_open_tag("tools"))
|
| 435 |
+
for index, tool_call in enumerate(tool_calls, start=1):
|
| 436 |
+
fn = tool_call.get("function", tool_call)
|
| 437 |
+
segments.extend(
|
| 438 |
+
_open_tag("call", [("tool", fn["name"]), ("index", index)])
|
| 439 |
+
)
|
| 440 |
+
args = fn.get("arguments", {})
|
| 441 |
+
json_block = fn.get("_xtml_json_block")
|
| 442 |
+
if json_block is not None:
|
| 443 |
+
segments.extend(_open_tag("json", [("type", "object")]))
|
| 444 |
+
_append_text(segments, json_block, image_state)
|
| 445 |
+
segments.extend(_close_tag("json"))
|
| 446 |
+
elif _is_mapping(args):
|
| 447 |
+
for key, value in args.items():
|
| 448 |
+
segments.extend(
|
| 449 |
+
_open_tag(
|
| 450 |
+
"argument",
|
| 451 |
+
[("key", key), ("type", _xtml_type(value))],
|
| 452 |
+
)
|
| 453 |
+
)
|
| 454 |
+
_append_text(segments, _xtml_value(value), image_state)
|
| 455 |
+
segments.extend(_close_tag("argument"))
|
| 456 |
+
segments.extend(_close_tag("call"))
|
| 457 |
+
segments.extend(_close_tag("tools"))
|
| 458 |
+
|
| 459 |
+
return segments
|
| 460 |
+
|
| 461 |
+
|
| 462 |
+
def _render_tool_declare(tools: Any, *, dynamic: bool = False) -> list[EncodeSegment]:
|
| 463 |
+
if dynamic:
|
| 464 |
+
body = (
|
| 465 |
+
"## New Tools Available\n"
|
| 466 |
+
"The system dynamically extends the toolset via lazy-loading.\n"
|
| 467 |
+
"You have access to all existing and extended tools.\n"
|
| 468 |
+
"Here are the specs for the extended tools.\n\n"
|
| 469 |
+
"```json\n"
|
| 470 |
+
f"{_json_compact(tools)}\n"
|
| 471 |
+
"```"
|
| 472 |
+
)
|
| 473 |
+
else:
|
| 474 |
+
body = (
|
| 475 |
+
"# Tools\n"
|
| 476 |
+
"Here are the available tools, described in JSONSchema.\n\n"
|
| 477 |
+
"```json\n"
|
| 478 |
+
f"{_json_compact(tools)}\n"
|
| 479 |
+
"```"
|
| 480 |
+
)
|
| 481 |
+
segments: list[EncodeSegment] = []
|
| 482 |
+
segments.extend(_open_tag("message", [("role", "system"), ("type", "tool-declare")]))
|
| 483 |
+
segments.extend(_text(body))
|
| 484 |
+
segments.extend(_close_tag("message"))
|
| 485 |
+
segments.extend(_end_of_msg())
|
| 486 |
+
return segments
|
| 487 |
+
|
| 488 |
+
|
| 489 |
+
def build_chat_segments(
|
| 490 |
+
messages: list[Any],
|
| 491 |
+
tools: Optional[list[dict]] = None,
|
| 492 |
+
*,
|
| 493 |
+
add_generation_prompt: bool = True,
|
| 494 |
+
thinking: bool = True,
|
| 495 |
+
image_prompts: Optional[list[str]] = None,
|
| 496 |
+
**kwargs: Any,
|
| 497 |
+
) -> list[EncodeSegment]:
|
| 498 |
+
# Re-sort tool results by tool_call_id at the lowest layer so every caller
|
| 499 |
+
# (processor or direct tokenizer) gets correctly ordered XTML. The helper is
|
| 500 |
+
# side-effect free, so the caller's message objects are left untouched.
|
| 501 |
+
messages = normalize_xtml_tool_result_messages(messages)
|
| 502 |
+
messages = normalize_conversation(messages)
|
| 503 |
+
tools = deep_sort_dict(tools)
|
| 504 |
+
|
| 505 |
+
kwargs = dict(kwargs)
|
| 506 |
+
response_format = kwargs.get("response_format")
|
| 507 |
+
if "response_schema" not in kwargs:
|
| 508 |
+
response_schema = extract_response_schema(response_format)
|
| 509 |
+
if response_schema is not None:
|
| 510 |
+
kwargs["response_schema"] = response_schema
|
| 511 |
+
if kwargs.get("response_schema") is not None:
|
| 512 |
+
kwargs["response_schema"] = deep_sort_dict(kwargs["response_schema"])
|
| 513 |
+
|
| 514 |
+
image_state = _ImagePromptState(image_prompts)
|
| 515 |
+
segments: list[EncodeSegment] = []
|
| 516 |
+
|
| 517 |
+
tool_calls = None
|
| 518 |
+
tool_index = 0
|
| 519 |
+
|
| 520 |
+
if tools:
|
| 521 |
+
segments.extend(_render_tool_declare(tools))
|
| 522 |
+
|
| 523 |
+
thinking_effort = kwargs.get("thinking_effort")
|
| 524 |
+
if thinking and thinking_effort is not None:
|
| 525 |
+
assert thinking_effort in _VALID_THINKING_EFFORTS, (
|
| 526 |
+
f"Unsupported thinking_effort={thinking_effort!r}; "
|
| 527 |
+
f"supported values are {sorted(_VALID_THINKING_EFFORTS)}."
|
| 528 |
+
)
|
| 529 |
+
if thinking and thinking_effort in _VALID_THINKING_EFFORTS:
|
| 530 |
+
segments.extend(
|
| 531 |
+
_internal_system_message(
|
| 532 |
+
"thinking-effort",
|
| 533 |
+
"`thinking_effort` guides on how much to think in your "
|
| 534 |
+
"thinking channel (not including the response channel), "
|
| 535 |
+
"supported values include `low`, `medium`, `high`, and `max`.\n"
|
| 536 |
+
f"Now the system is invoked with `thinking_effort={thinking_effort}`.",
|
| 537 |
+
)
|
| 538 |
+
)
|
| 539 |
+
|
| 540 |
+
for message_index, message in enumerate(messages):
|
| 541 |
+
if not isinstance(message, dict):
|
| 542 |
+
continue
|
| 543 |
+
|
| 544 |
+
role = message["role"]
|
| 545 |
+
if role == "user":
|
| 546 |
+
attrs = [("role", "user")]
|
| 547 |
+
if message.get("name"):
|
| 548 |
+
attrs.append(("name", message["name"]))
|
| 549 |
+
segments.extend(_open_tag("message", attrs))
|
| 550 |
+
segments.extend(_render_content_segments(message.get("content"), image_state))
|
| 551 |
+
segments.extend(_close_tag("message"))
|
| 552 |
+
segments.extend(_end_of_msg())
|
| 553 |
+
elif role == "system" and message.get("tools"):
|
| 554 |
+
segments.extend(_render_tool_declare(message["tools"], dynamic=True))
|
| 555 |
+
elif role == "system":
|
| 556 |
+
attrs = [("role", "system")]
|
| 557 |
+
if message.get("name"):
|
| 558 |
+
attrs.append(("name", message["name"]))
|
| 559 |
+
segments.extend(_open_tag("message", attrs))
|
| 560 |
+
segments.extend(_render_content_segments(message.get("content"), image_state))
|
| 561 |
+
segments.extend(_close_tag("message"))
|
| 562 |
+
segments.extend(_end_of_msg())
|
| 563 |
+
elif role == "tool":
|
| 564 |
+
tool_index += 1
|
| 565 |
+
tool_name = message.get("tool", message.get("name"))
|
| 566 |
+
if (
|
| 567 |
+
tool_name is None
|
| 568 |
+
and tool_calls is not None
|
| 569 |
+
and tool_index <= len(tool_calls)
|
| 570 |
+
):
|
| 571 |
+
tc = tool_calls[tool_index - 1]
|
| 572 |
+
fn = tc.get("function", tc)
|
| 573 |
+
tool_name = fn["name"]
|
| 574 |
+
if tool_name is None:
|
| 575 |
+
raise ValueError(
|
| 576 |
+
"Kimi K3 tool messages need a resolvable tool name: "
|
| 577 |
+
"carry `tool`/`name`, or match a preceding assistant "
|
| 578 |
+
"tool_call by order."
|
| 579 |
+
)
|
| 580 |
+
segments.extend(
|
| 581 |
+
_open_tag(
|
| 582 |
+
"message",
|
| 583 |
+
[("role", "tool"), ("tool", tool_name), ("index", tool_index)],
|
| 584 |
+
)
|
| 585 |
+
)
|
| 586 |
+
segments.extend(_render_content_segments(message.get("content"), image_state))
|
| 587 |
+
segments.extend(_close_tag("message"))
|
| 588 |
+
segments.extend(_end_of_msg())
|
| 589 |
+
elif role == "assistant":
|
| 590 |
+
tool_calls = message.get("tool_calls")
|
| 591 |
+
tool_index = 0
|
| 592 |
+
attrs = [("role", "assistant")]
|
| 593 |
+
if message.get("name"):
|
| 594 |
+
attrs.append(("name", message["name"]))
|
| 595 |
+
segments.extend(_open_tag("message", attrs))
|
| 596 |
+
segments.extend(_render_assistant_segments(message, image_state, thinking))
|
| 597 |
+
segments.extend(_close_tag("message"))
|
| 598 |
+
segments.extend(_end_of_msg())
|
| 599 |
+
|
| 600 |
+
tool_choice = kwargs.get("tool_choice")
|
| 601 |
+
if tool_choice == "required":
|
| 602 |
+
segments.extend(
|
| 603 |
+
_internal_system_message(
|
| 604 |
+
"tool-choice",
|
| 605 |
+
"The system is invoked with `tool_choice=required`.\n"
|
| 606 |
+
"You MUST call tools in the next message.",
|
| 607 |
+
)
|
| 608 |
+
)
|
| 609 |
+
elif tool_choice == "none":
|
| 610 |
+
segments.extend(
|
| 611 |
+
_internal_system_message(
|
| 612 |
+
"tool-choice",
|
| 613 |
+
"The system is invoked with `tool_choice=none`.\n"
|
| 614 |
+
"You MUST NOT call any tools in the next message.",
|
| 615 |
+
)
|
| 616 |
+
)
|
| 617 |
+
|
| 618 |
+
rf = kwargs.get("response_format")
|
| 619 |
+
rf_type = _get_value(rf, "type", rf) if isinstance(rf, dict) else rf
|
| 620 |
+
if rf_type == "json_object":
|
| 621 |
+
segments.extend(
|
| 622 |
+
_internal_system_message(
|
| 623 |
+
"response-format",
|
| 624 |
+
"The system is invoked with `response_format=json_object`.\n"
|
| 625 |
+
"Your response must be raw JSON data without markdown code "
|
| 626 |
+
"blocks (```json) or any additional formatting.",
|
| 627 |
+
)
|
| 628 |
+
)
|
| 629 |
+
elif rf_type == "json_schema":
|
| 630 |
+
schema = _json_compact(kwargs.get("response_schema"))
|
| 631 |
+
segments.extend(
|
| 632 |
+
_internal_system_message(
|
| 633 |
+
"response-format",
|
| 634 |
+
"The system is invoked with `response_format=json_schema`.\n"
|
| 635 |
+
"Your response must be raw JSON data without markdown code "
|
| 636 |
+
"blocks (```json) or any additional formatting.\n"
|
| 637 |
+
"The JSON data must match the following schema:\n"
|
| 638 |
+
f"```json\n{schema}\n```",
|
| 639 |
+
)
|
| 640 |
+
)
|
| 641 |
+
|
| 642 |
+
if add_generation_prompt:
|
| 643 |
+
segments.extend(_open_tag("message", [("role", "assistant")]))
|
| 644 |
+
segments.extend(_open_tag("think" if thinking else "response"))
|
| 645 |
+
|
| 646 |
+
image_state.assert_consumed()
|
| 647 |
+
return segments
|
generation_config.json
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"max_length": 1048576,
|
| 3 |
+
"eos_token_id": 163586
|
| 4 |
+
}
|
kimi_k3.py
ADDED
|
@@ -0,0 +1,954 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# MLX port of moonshotai/Kimi-K3 (2.78T-total / 104B-active multimodal MoE).
|
| 2 |
+
#
|
| 3 |
+
# Text tower only — the vision tower rides along in the checkpoint and is handled
|
| 4 |
+
# separately (see scripts/convert.py, which passes it through untouched).
|
| 5 |
+
#
|
| 6 |
+
# Derived from mlx-lm's `kimi_linear.py` (Kimi-Linear-48B-A3B), which already
|
| 7 |
+
# covers KDA + gated MLA + DeepSeek-style sparse MoE. K3 adds five things that
|
| 8 |
+
# do not exist anywhere in MLX; each is marked [K3] below:
|
| 9 |
+
#
|
| 10 |
+
# [K3-1] SiTU-GLU activation beta=4.0, linear_beta=25.0
|
| 11 |
+
# [K3-2] Attention Residuals (AttnRes) rank-1 [1,H] score projections, block 12
|
| 12 |
+
# [K3-3] Stable LatentMoE routed experts live in a 3584-d latent
|
| 13 |
+
# [K3-4] q-LoRA + output-gated MLA q_a/q_b rank 1536, sigmoid g_proj
|
| 14 |
+
# [K3-5] per-channel KDA decay A_log is [head_dim], not [num_heads]
|
| 15 |
+
#
|
| 16 |
+
# NOTE on [K3-5]: the reference `modeling_kimi_linear.py` allocates
|
| 17 |
+
# `A_log = Parameter(log(empty(num_heads)))` — i.e. [96] — but every checkpoint
|
| 18 |
+
# shard ships A_log as [128] == head_dim, while b_proj is [96, H] and dt_bias is
|
| 19 |
+
# [12288] == 96*128. fla's kernel broadcasts A_log against g of shape
|
| 20 |
+
# (B,T,96,128), so a length-128 vector can only align with the trailing head-dim
|
| 21 |
+
# axis. The decay rate is therefore per-channel and shared across heads. The
|
| 22 |
+
# reference init code is simply stale w.r.t. the released weights; the shapes are
|
| 23 |
+
# authoritative. tests/test_kimi_k3.py asserts this against the real shard.
|
| 24 |
+
|
| 25 |
+
from dataclasses import dataclass, field
|
| 26 |
+
from typing import Any, Dict, List, Optional
|
| 27 |
+
|
| 28 |
+
import mlx.core as mx
|
| 29 |
+
import mlx.nn as nn
|
| 30 |
+
import numpy as np
|
| 31 |
+
|
| 32 |
+
from .base import (
|
| 33 |
+
BaseModelArgs,
|
| 34 |
+
create_attention_mask,
|
| 35 |
+
create_ssm_mask,
|
| 36 |
+
scaled_dot_product_attention,
|
| 37 |
+
)
|
| 38 |
+
from .cache import ArraysCache, KVCache
|
| 39 |
+
from .gated_delta import gated_delta_kernel, gated_delta_ops
|
| 40 |
+
from .mla import MultiLinear
|
| 41 |
+
from .switch_layers import SwitchGLU
|
| 42 |
+
|
| 43 |
+
|
| 44 |
+
@dataclass
|
| 45 |
+
class ModelArgs(BaseModelArgs):
|
| 46 |
+
model_type: str
|
| 47 |
+
vocab_size: int
|
| 48 |
+
hidden_size: int
|
| 49 |
+
num_hidden_layers: int
|
| 50 |
+
num_attention_heads: int
|
| 51 |
+
num_key_value_heads: int
|
| 52 |
+
intermediate_size: int
|
| 53 |
+
rms_norm_eps: float
|
| 54 |
+
linear_attn_config: Dict[str, Any]
|
| 55 |
+
num_experts: int
|
| 56 |
+
moe_intermediate_size: int
|
| 57 |
+
kv_lora_rank: int
|
| 58 |
+
q_lora_rank: Optional[int] = None
|
| 59 |
+
tie_word_embeddings: bool = False
|
| 60 |
+
qk_nope_head_dim: Optional[int] = None
|
| 61 |
+
qk_rope_head_dim: Optional[int] = None
|
| 62 |
+
v_head_dim: Optional[int] = None
|
| 63 |
+
mla_use_nope: bool = True
|
| 64 |
+
mla_use_output_gate: bool = False
|
| 65 |
+
num_experts_per_token: int = 1
|
| 66 |
+
num_shared_experts: int = 0
|
| 67 |
+
moe_router_activation_func: str = "sigmoid"
|
| 68 |
+
moe_renormalize: bool = True
|
| 69 |
+
routed_scaling_factor: float = 1.0
|
| 70 |
+
first_k_dense_replace: int = 0
|
| 71 |
+
moe_layer_freq: int = 1
|
| 72 |
+
num_expert_group: int = 1
|
| 73 |
+
topk_group: int = 1
|
| 74 |
+
max_position_embeddings: int = 1048576
|
| 75 |
+
# [K3-1]
|
| 76 |
+
hidden_act: str = "situ"
|
| 77 |
+
activation_situ_beta: float = 1.0
|
| 78 |
+
activation_situ_linear_beta: Optional[float] = None
|
| 79 |
+
# [K3-2]
|
| 80 |
+
attn_res_block_size: Optional[int] = None
|
| 81 |
+
# [K3-3]
|
| 82 |
+
routed_expert_hidden_size: Optional[int] = None
|
| 83 |
+
latent_moe_use_norm: bool = False
|
| 84 |
+
# REAP-pruned checkpoints keep a different number of experts per layer
|
| 85 |
+
# (0 for dense layers). None => every MoE layer has `num_experts`.
|
| 86 |
+
expert_counts: Optional[List[int]] = None
|
| 87 |
+
# Graded REAP: per layer, how many of that layer's experts sit in the
|
| 88 |
+
# high-precision bank. 0 (or None) => a single uniform-width bank.
|
| 89 |
+
expert_bank_split: Optional[List[int]] = None
|
| 90 |
+
# multimodal wrapper fields (ignored by the text tower)
|
| 91 |
+
vision_config: Dict[str, Any] = field(default_factory=dict)
|
| 92 |
+
|
| 93 |
+
@classmethod
|
| 94 |
+
def from_dict(cls, params: Dict[str, Any]) -> "ModelArgs":
|
| 95 |
+
# K3's config.json nests the language model under `text_config`.
|
| 96 |
+
if "text_config" in params:
|
| 97 |
+
merged = dict(params["text_config"])
|
| 98 |
+
merged["model_type"] = params.get("model_type", "kimi_k3")
|
| 99 |
+
merged["vision_config"] = params.get("vision_config", {})
|
| 100 |
+
for k in ("bos_token_id", "eos_token_id", "pad_token_id"):
|
| 101 |
+
merged.setdefault(k, params.get(k))
|
| 102 |
+
params = merged
|
| 103 |
+
return super().from_dict(params)
|
| 104 |
+
|
| 105 |
+
|
| 106 |
+
# ---------------------------------------------------------------- [K3-1] SiTU
|
| 107 |
+
|
| 108 |
+
|
| 109 |
+
class SituGLU(nn.Module):
|
| 110 |
+
"""beta*tanh(gate/beta)*sigmoid(gate) * linear_beta*tanh(up/linear_beta).
|
| 111 |
+
|
| 112 |
+
Matches SituAndMul in the reference, including the float32 upcast — the
|
| 113 |
+
tanh saturation points (beta=4, linear_beta=25) are far enough out that
|
| 114 |
+
bf16 rounding visibly changes the result.
|
| 115 |
+
"""
|
| 116 |
+
|
| 117 |
+
def __init__(self, beta: float = 1.0, linear_beta: Optional[float] = None):
|
| 118 |
+
super().__init__()
|
| 119 |
+
self.beta = beta
|
| 120 |
+
self.linear_beta = linear_beta
|
| 121 |
+
|
| 122 |
+
def __call__(self, x_up: mx.array, x_gate: mx.array) -> mx.array:
|
| 123 |
+
dtype = x_up.dtype
|
| 124 |
+
gate = x_gate.astype(mx.float32)
|
| 125 |
+
up = x_up.astype(mx.float32)
|
| 126 |
+
out = self.beta * mx.tanh(gate / self.beta) * mx.sigmoid(gate)
|
| 127 |
+
if self.linear_beta is not None:
|
| 128 |
+
up = self.linear_beta * mx.tanh(up / self.linear_beta)
|
| 129 |
+
return (out * up).astype(dtype)
|
| 130 |
+
|
| 131 |
+
|
| 132 |
+
def _situ_from_args(args: ModelArgs) -> SituGLU:
|
| 133 |
+
return SituGLU(args.activation_situ_beta or 1.0, args.activation_situ_linear_beta)
|
| 134 |
+
|
| 135 |
+
|
| 136 |
+
class KimiMLP(nn.Module):
|
| 137 |
+
def __init__(
|
| 138 |
+
self,
|
| 139 |
+
args: ModelArgs,
|
| 140 |
+
hidden_size: Optional[int] = None,
|
| 141 |
+
intermediate_size: Optional[int] = None,
|
| 142 |
+
):
|
| 143 |
+
super().__init__()
|
| 144 |
+
h = hidden_size or args.hidden_size
|
| 145 |
+
i = intermediate_size or args.intermediate_size
|
| 146 |
+
self.gate_proj = nn.Linear(h, i, bias=False)
|
| 147 |
+
self.up_proj = nn.Linear(h, i, bias=False)
|
| 148 |
+
self.down_proj = nn.Linear(i, h, bias=False)
|
| 149 |
+
self.act = _situ_from_args(args)
|
| 150 |
+
|
| 151 |
+
def __call__(self, x: mx.array) -> mx.array:
|
| 152 |
+
return self.down_proj(self.act(self.up_proj(x), self.gate_proj(x)))
|
| 153 |
+
|
| 154 |
+
|
| 155 |
+
# ------------------------------------------------------------- [K3-2] AttnRes
|
| 156 |
+
|
| 157 |
+
|
| 158 |
+
def _apply_attn_res(
|
| 159 |
+
prefix_sum: mx.array,
|
| 160 |
+
block_residual: mx.array,
|
| 161 |
+
proj_weight: mx.array,
|
| 162 |
+
norm_weight: mx.array,
|
| 163 |
+
eps: float,
|
| 164 |
+
) -> mx.array:
|
| 165 |
+
"""Softmax mixture over [block residuals..., prefix_sum].
|
| 166 |
+
|
| 167 |
+
prefix_sum: (N, H)
|
| 168 |
+
block_residual: (N, K, H) -> returns (N, H)
|
| 169 |
+
|
| 170 |
+
Each candidate is RMS-normed, scored against a single learned direction
|
| 171 |
+
(norm.weight * proj.weight, both length H), and the candidates are combined
|
| 172 |
+
by a softmax over K+1. All in float32, as in the reference.
|
| 173 |
+
"""
|
| 174 |
+
v = mx.concatenate([block_residual, mx.expand_dims(prefix_sum, 1)], axis=1)
|
| 175 |
+
vf = v.astype(mx.float32)
|
| 176 |
+
variance = mx.mean(vf * vf, axis=-1, keepdims=True)
|
| 177 |
+
k = vf * mx.rsqrt(variance + eps)
|
| 178 |
+
score_weight = norm_weight.astype(mx.float32) * proj_weight.reshape(-1).astype(
|
| 179 |
+
mx.float32
|
| 180 |
+
)
|
| 181 |
+
scores = mx.sum(k * score_weight, axis=-1)
|
| 182 |
+
probs = mx.expand_dims(mx.softmax(scores, axis=-1), 1)
|
| 183 |
+
return (probs @ vf).squeeze(1).astype(v.dtype)
|
| 184 |
+
|
| 185 |
+
|
| 186 |
+
class AttnResProj(nn.Module):
|
| 187 |
+
"""Holds the rank-1 [1, H] score projection and its paired RMSNorm gain.
|
| 188 |
+
|
| 189 |
+
Kept as plain arrays rather than nn.Linear/nn.RMSNorm so the quantizer skips
|
| 190 |
+
them — they are 7168 floats each and feed a softmax, so quantizing them is
|
| 191 |
+
all downside.
|
| 192 |
+
"""
|
| 193 |
+
|
| 194 |
+
def __init__(self, hidden_size: int):
|
| 195 |
+
super().__init__()
|
| 196 |
+
self.proj_weight = mx.zeros((1, hidden_size))
|
| 197 |
+
self.norm_weight = mx.ones((hidden_size,))
|
| 198 |
+
|
| 199 |
+
def __call__(
|
| 200 |
+
self, prefix_sum: mx.array, block_residual: mx.array, eps: float
|
| 201 |
+
) -> mx.array:
|
| 202 |
+
return _apply_attn_res(
|
| 203 |
+
prefix_sum, block_residual, self.proj_weight, self.norm_weight, eps
|
| 204 |
+
)
|
| 205 |
+
|
| 206 |
+
|
| 207 |
+
# ------------------------------------------------------ [K3-4] q-LoRA gated MLA
|
| 208 |
+
|
| 209 |
+
|
| 210 |
+
class KimiMLAAttention(nn.Module):
|
| 211 |
+
def __init__(self, args: ModelArgs):
|
| 212 |
+
super().__init__()
|
| 213 |
+
self.args = args
|
| 214 |
+
self.num_heads = args.num_attention_heads
|
| 215 |
+
self.qk_nope_head_dim = args.qk_nope_head_dim or 128
|
| 216 |
+
self.qk_rope_head_dim = args.qk_rope_head_dim or 0
|
| 217 |
+
self.q_head_dim = self.qk_nope_head_dim + self.qk_rope_head_dim
|
| 218 |
+
self.v_head_dim = args.v_head_dim or 128
|
| 219 |
+
self.kv_lora_rank = args.kv_lora_rank
|
| 220 |
+
self.q_lora_rank = args.q_lora_rank
|
| 221 |
+
self.scale = self.q_head_dim**-0.5
|
| 222 |
+
|
| 223 |
+
h = args.hidden_size
|
| 224 |
+
if self.q_lora_rank is not None:
|
| 225 |
+
self.q_a_proj = nn.Linear(h, self.q_lora_rank, bias=False)
|
| 226 |
+
self.q_a_layernorm = nn.RMSNorm(self.q_lora_rank, eps=args.rms_norm_eps)
|
| 227 |
+
self.q_b_proj = nn.Linear(
|
| 228 |
+
self.q_lora_rank, self.num_heads * self.q_head_dim, bias=False
|
| 229 |
+
)
|
| 230 |
+
else:
|
| 231 |
+
self.q_proj = nn.Linear(h, self.num_heads * self.q_head_dim, bias=False)
|
| 232 |
+
|
| 233 |
+
self.kv_a_proj_with_mqa = nn.Linear(
|
| 234 |
+
h, self.kv_lora_rank + self.qk_rope_head_dim, bias=False
|
| 235 |
+
)
|
| 236 |
+
self.kv_a_layernorm = nn.RMSNorm(self.kv_lora_rank, eps=args.rms_norm_eps)
|
| 237 |
+
|
| 238 |
+
# kv_b_proj is absorbed into these two at load time (see sanitize), so the
|
| 239 |
+
# KV cache holds the 512-d latent instead of 96 full heads. At 1M context
|
| 240 |
+
# that is the difference between ~110 GB and ~1.2 GB of cache per layer.
|
| 241 |
+
self.embed_q = MultiLinear(self.qk_nope_head_dim, self.kv_lora_rank, self.num_heads)
|
| 242 |
+
self.unembed_out = MultiLinear(self.kv_lora_rank, self.v_head_dim, self.num_heads)
|
| 243 |
+
|
| 244 |
+
self.o_proj = nn.Linear(self.num_heads * self.v_head_dim, h, bias=False)
|
| 245 |
+
|
| 246 |
+
self.use_output_gate = args.mla_use_output_gate
|
| 247 |
+
if self.use_output_gate:
|
| 248 |
+
self.g_proj = nn.Linear(h, self.num_heads * self.v_head_dim, bias=False)
|
| 249 |
+
|
| 250 |
+
def __call__(
|
| 251 |
+
self,
|
| 252 |
+
x: mx.array,
|
| 253 |
+
mask: Optional[mx.array] = None,
|
| 254 |
+
cache: Optional[KVCache] = None,
|
| 255 |
+
) -> mx.array:
|
| 256 |
+
B, L, _ = x.shape
|
| 257 |
+
|
| 258 |
+
if self.q_lora_rank is not None:
|
| 259 |
+
q = self.q_b_proj(self.q_a_layernorm(self.q_a_proj(x)))
|
| 260 |
+
else:
|
| 261 |
+
q = self.q_proj(x)
|
| 262 |
+
q = q.reshape(B, L, self.num_heads, self.q_head_dim).transpose(0, 2, 1, 3)
|
| 263 |
+
q_nope, q_pe = mx.split(q, [self.qk_nope_head_dim], axis=-1)
|
| 264 |
+
|
| 265 |
+
compressed_kv = self.kv_a_proj_with_mqa(x)
|
| 266 |
+
compressed_kv, k_pe = mx.split(compressed_kv, [self.kv_lora_rank], axis=-1)
|
| 267 |
+
k_pe = k_pe.reshape(B, L, 1, self.qk_rope_head_dim).transpose(0, 2, 1, 3)
|
| 268 |
+
kv_latent = mx.expand_dims(self.kv_a_layernorm(compressed_kv), axis=1)
|
| 269 |
+
|
| 270 |
+
if cache is not None:
|
| 271 |
+
kv_latent, k_pe = cache.update_and_fetch(kv_latent, k_pe)
|
| 272 |
+
|
| 273 |
+
# NoPE: q_pe/k_pe carry no rotary embedding at all (the reference asserts
|
| 274 |
+
# use_nope and leaves rotary_emb=None). They are a plain extra 64 dims,
|
| 275 |
+
# folded in as an additive score term alongside the causal mask.
|
| 276 |
+
pe_scores = (q_pe * self.scale) @ k_pe.swapaxes(-1, -2)
|
| 277 |
+
if mask is not None:
|
| 278 |
+
pe_scores = mx.where(
|
| 279 |
+
mask, pe_scores, mx.array(mx.finfo(pe_scores.dtype).min, pe_scores.dtype)
|
| 280 |
+
)
|
| 281 |
+
|
| 282 |
+
if L == 1:
|
| 283 |
+
q_nope = self.embed_q(q_nope)
|
| 284 |
+
k = v = kv_latent
|
| 285 |
+
else:
|
| 286 |
+
k = self.embed_q(kv_latent, transpose=False)
|
| 287 |
+
v = self.unembed_out(kv_latent)
|
| 288 |
+
|
| 289 |
+
out = scaled_dot_product_attention(
|
| 290 |
+
q_nope, k, v, cache=cache, scale=self.scale, mask=pe_scores
|
| 291 |
+
)
|
| 292 |
+
if L == 1:
|
| 293 |
+
out = self.unembed_out(out)
|
| 294 |
+
|
| 295 |
+
out = out.transpose(0, 2, 1, 3).reshape(B, L, -1)
|
| 296 |
+
if self.use_output_gate:
|
| 297 |
+
out = out * mx.sigmoid(self.g_proj(x))
|
| 298 |
+
return self.o_proj(out)
|
| 299 |
+
|
| 300 |
+
|
| 301 |
+
# ------------------------------------------------------------- [K3-5] KDA
|
| 302 |
+
|
| 303 |
+
|
| 304 |
+
class ShortConv1d(nn.Module):
|
| 305 |
+
def __init__(self, channels: int, kernel_size: int):
|
| 306 |
+
super().__init__()
|
| 307 |
+
self.kernel_size = kernel_size
|
| 308 |
+
self.conv = nn.Conv1d(
|
| 309 |
+
in_channels=channels,
|
| 310 |
+
out_channels=channels,
|
| 311 |
+
kernel_size=kernel_size,
|
| 312 |
+
groups=channels,
|
| 313 |
+
padding=0,
|
| 314 |
+
bias=False,
|
| 315 |
+
)
|
| 316 |
+
|
| 317 |
+
def __call__(self, x, state, mask=None, lengths=None):
|
| 318 |
+
if mask is not None and state is not None:
|
| 319 |
+
x = x * mx.expand_dims(mask, -1) if mask.ndim == 2 else x
|
| 320 |
+
conv_input = mx.concatenate([state, x], axis=1)
|
| 321 |
+
new_state = conv_input[:, -(self.kernel_size - 1) :, :]
|
| 322 |
+
return nn.silu(self.conv(conv_input)), new_state
|
| 323 |
+
|
| 324 |
+
|
| 325 |
+
class KimiDeltaAttention(nn.Module):
|
| 326 |
+
def __init__(self, args: ModelArgs, layer_idx: int):
|
| 327 |
+
super().__init__()
|
| 328 |
+
cfg = args.linear_attn_config
|
| 329 |
+
self.layer_idx = layer_idx
|
| 330 |
+
self.num_heads = cfg["num_heads"]
|
| 331 |
+
self.head_dim = cfg["head_dim"]
|
| 332 |
+
self.conv_kernel = cfg.get("short_conv_kernel_size", 4)
|
| 333 |
+
self.gate_lower_bound = cfg.get("gate_lower_bound", None)
|
| 334 |
+
self.use_full_rank_gate = cfg.get("use_full_rank_gate", False)
|
| 335 |
+
|
| 336 |
+
self.projection_dim = self.num_heads * self.head_dim
|
| 337 |
+
h = args.hidden_size
|
| 338 |
+
self.scale = float(self.head_dim) ** -0.5
|
| 339 |
+
|
| 340 |
+
self.q_proj = nn.Linear(h, self.projection_dim, bias=False)
|
| 341 |
+
self.k_proj = nn.Linear(h, self.projection_dim, bias=False)
|
| 342 |
+
self.v_proj = nn.Linear(h, self.projection_dim, bias=False)
|
| 343 |
+
|
| 344 |
+
self.q_conv = ShortConv1d(self.projection_dim, self.conv_kernel)
|
| 345 |
+
self.k_conv = ShortConv1d(self.projection_dim, self.conv_kernel)
|
| 346 |
+
self.v_conv = ShortConv1d(self.projection_dim, self.conv_kernel)
|
| 347 |
+
|
| 348 |
+
self.f_a_proj = nn.Linear(h, self.head_dim, bias=False)
|
| 349 |
+
self.f_b_proj = nn.Linear(self.head_dim, self.projection_dim, bias=False)
|
| 350 |
+
self.b_proj = nn.Linear(h, self.num_heads, bias=False)
|
| 351 |
+
|
| 352 |
+
# [K3] full-rank output gate (use_full_rank_gate=True) replaces the
|
| 353 |
+
# low-rank g_a/g_b pair used by Kimi-Linear-48B.
|
| 354 |
+
if self.use_full_rank_gate:
|
| 355 |
+
self.g_proj = nn.Linear(h, self.projection_dim, bias=False)
|
| 356 |
+
else:
|
| 357 |
+
self.g_a_proj = nn.Linear(h, self.head_dim, bias=False)
|
| 358 |
+
self.g_b_proj = nn.Linear(self.head_dim, self.projection_dim, bias=False)
|
| 359 |
+
|
| 360 |
+
# [K3-5] per-channel, shared across heads -> broadcast as (1, head_dim).
|
| 361 |
+
self.A_log = mx.zeros((self.head_dim,))
|
| 362 |
+
self.dt_bias = mx.zeros((self.projection_dim,))
|
| 363 |
+
|
| 364 |
+
self.o_norm = nn.RMSNorm(self.head_dim, eps=args.rms_norm_eps)
|
| 365 |
+
self.o_proj = nn.Linear(self.projection_dim, h, bias=False)
|
| 366 |
+
|
| 367 |
+
def _compute_g(self, a: mx.array) -> mx.array:
|
| 368 |
+
# log-decay, then the safe-gate clamp (fla's `lower_bound`), then exp.
|
| 369 |
+
log_g = -mx.exp(self.A_log.reshape(1, self.head_dim).astype(mx.float32)) * nn.softplus(
|
| 370 |
+
a + self.dt_bias.reshape(self.num_heads, self.head_dim)
|
| 371 |
+
)
|
| 372 |
+
if self.gate_lower_bound is not None:
|
| 373 |
+
log_g = mx.maximum(log_g, self.gate_lower_bound)
|
| 374 |
+
return mx.exp(log_g)
|
| 375 |
+
|
| 376 |
+
def __call__(self, x, mask=None, cache=None):
|
| 377 |
+
B, T, _ = x.shape
|
| 378 |
+
dtype = x.dtype
|
| 379 |
+
|
| 380 |
+
if cache is not None:
|
| 381 |
+
q_state, k_state, v_state, ssm_state = cache
|
| 382 |
+
lengths = cache.lengths
|
| 383 |
+
else:
|
| 384 |
+
q_state = k_state = v_state = ssm_state = None
|
| 385 |
+
lengths = None
|
| 386 |
+
|
| 387 |
+
if q_state is None:
|
| 388 |
+
s = mx.zeros((B, self.conv_kernel - 1, self.projection_dim), dtype=dtype)
|
| 389 |
+
q_state = k_state = v_state = s
|
| 390 |
+
|
| 391 |
+
q_conv, q_state = self.q_conv(self.q_proj(x), q_state, mask, lengths)
|
| 392 |
+
k_conv, k_state = self.k_conv(self.k_proj(x), k_state, mask, lengths)
|
| 393 |
+
v_conv, v_state = self.v_conv(self.v_proj(x), v_state, mask, lengths)
|
| 394 |
+
|
| 395 |
+
if cache is not None:
|
| 396 |
+
cache[0], cache[1], cache[2] = q_state, k_state, v_state
|
| 397 |
+
|
| 398 |
+
q = q_conv.reshape(B, T, self.num_heads, self.head_dim)
|
| 399 |
+
k = k_conv.reshape(B, T, self.num_heads, self.head_dim)
|
| 400 |
+
v = v_conv.reshape(B, T, self.num_heads, self.head_dim)
|
| 401 |
+
|
| 402 |
+
# use_qk_l2norm_in_kernel=True in the reference
|
| 403 |
+
q = (self.scale**2) * mx.fast.rms_norm(q, None, 1e-6)
|
| 404 |
+
k = self.scale * mx.fast.rms_norm(k, None, 1e-6)
|
| 405 |
+
|
| 406 |
+
a_logits = self.f_b_proj(self.f_a_proj(x)).reshape(
|
| 407 |
+
B, T, self.num_heads, self.head_dim
|
| 408 |
+
)
|
| 409 |
+
beta = mx.sigmoid(self.b_proj(x).reshape(B, T, self.num_heads))
|
| 410 |
+
g = self._compute_g(a_logits)
|
| 411 |
+
|
| 412 |
+
if ssm_state is None:
|
| 413 |
+
ssm_state = mx.zeros(
|
| 414 |
+
(B, self.num_heads, self.head_dim, self.head_dim), dtype=mx.float32
|
| 415 |
+
)
|
| 416 |
+
use_kernel = mx.default_device() == mx.gpu and mx.metal.is_available()
|
| 417 |
+
fn = gated_delta_kernel if use_kernel else gated_delta_ops
|
| 418 |
+
out, ssm_state = fn(q, k, v, g, beta, ssm_state, mask)
|
| 419 |
+
|
| 420 |
+
if cache is not None:
|
| 421 |
+
cache[3] = ssm_state
|
| 422 |
+
cache.advance(T)
|
| 423 |
+
|
| 424 |
+
if self.use_full_rank_gate:
|
| 425 |
+
gate = self.g_proj(x)
|
| 426 |
+
else:
|
| 427 |
+
gate = self.g_b_proj(self.g_a_proj(x))
|
| 428 |
+
gate = gate.reshape(B, T, self.num_heads, self.head_dim)
|
| 429 |
+
out = (
|
| 430 |
+
self.o_norm(out.reshape(B, T, self.num_heads, self.head_dim))
|
| 431 |
+
* mx.sigmoid(gate)
|
| 432 |
+
).reshape(B, T, -1)
|
| 433 |
+
return self.o_proj(out)
|
| 434 |
+
|
| 435 |
+
|
| 436 |
+
# --------------------------------------------------------- [K3-3] LatentMoE
|
| 437 |
+
|
| 438 |
+
|
| 439 |
+
def _group_expert_select(
|
| 440 |
+
scores: mx.array,
|
| 441 |
+
bias: mx.array,
|
| 442 |
+
top_k: int,
|
| 443 |
+
num_expert_group: int,
|
| 444 |
+
topk_group: int,
|
| 445 |
+
routed_scaling_factor: float,
|
| 446 |
+
renormalize: bool,
|
| 447 |
+
activation: str,
|
| 448 |
+
):
|
| 449 |
+
if activation == "sigmoid":
|
| 450 |
+
scores = mx.sigmoid(scores.astype(mx.float32))
|
| 451 |
+
else:
|
| 452 |
+
scores = mx.softmax(scores.astype(mx.float32), axis=-1)
|
| 453 |
+
|
| 454 |
+
orig = scores
|
| 455 |
+
scores = scores + bias
|
| 456 |
+
if num_expert_group > 1 and num_expert_group > topk_group:
|
| 457 |
+
k = scores.shape[-1] // num_expert_group
|
| 458 |
+
scores = mx.unflatten(scores, -1, (num_expert_group, k))
|
| 459 |
+
group_scores = mx.topk(scores, 2, axis=-1).sum(axis=-1, keepdims=True)
|
| 460 |
+
gate = mx.argpartition(-group_scores, kth=topk_group - 1, axis=-2)
|
| 461 |
+
gate = mx.put_along_axis(
|
| 462 |
+
mx.ones_like(group_scores),
|
| 463 |
+
gate[..., :topk_group, :],
|
| 464 |
+
mx.zeros((1,), scores.dtype),
|
| 465 |
+
axis=-2,
|
| 466 |
+
)
|
| 467 |
+
scores = mx.flatten(scores - gate * mx.finfo(scores.dtype).max, -2, -1)
|
| 468 |
+
|
| 469 |
+
inds = mx.stop_gradient(mx.argpartition(-scores, kth=top_k - 1, axis=-1)[..., :top_k])
|
| 470 |
+
weights = mx.take_along_axis(orig, inds, axis=-1)
|
| 471 |
+
if top_k > 1 and renormalize:
|
| 472 |
+
weights = weights / (mx.sum(weights, axis=-1, keepdims=True) + 1e-20)
|
| 473 |
+
weights = weights * routed_scaling_factor
|
| 474 |
+
return inds, weights.astype(orig.dtype)
|
| 475 |
+
|
| 476 |
+
|
| 477 |
+
class TwoBankSwitchGLU(nn.Module):
|
| 478 |
+
"""SwitchGLU whose experts are split into two banks at different bit widths.
|
| 479 |
+
|
| 480 |
+
Experts [0, n_hi) live in `bank_hi`, [n_hi, n_hi+n_lo) in `bank_lo`. This
|
| 481 |
+
exists because MLX pins one bit width per expert tensor: QuantizedSwitchLinear
|
| 482 |
+
stores scalar bits/group_size/mode and gather_qmm takes them as scalars. Two
|
| 483 |
+
modules means two widths, which is what graded REAP needs -- most-salient
|
| 484 |
+
experts at mxfp4 (free on K3, it is the source encoding), the rest lower.
|
| 485 |
+
|
| 486 |
+
The naive way to do this costs 2x expert compute: run both banks over every
|
| 487 |
+
routed pair and select. Instead we exploit the sort SwitchGLU already
|
| 488 |
+
performs. After sorting (token, slot) pairs by expert index, a bank owning a
|
| 489 |
+
contiguous index range owns a contiguous *slice* of the sorted pairs, so
|
| 490 |
+
each bank does exactly its own share of the work and no more.
|
| 491 |
+
|
| 492 |
+
Cost: the split point is data-dependent, so it must be read back to the
|
| 493 |
+
host to slice on. Measured at K3 REAP dims (3584->3072, 240 experts,
|
| 494 |
+
top-16), that sync is essentially the *entire* overhead -- the second
|
| 495 |
+
dispatch is nearly free:
|
| 496 |
+
|
| 497 |
+
prefill (256 tok) 1.05x vs single-bank
|
| 498 |
+
decode (1 tok) 1.16x
|
| 499 |
+
|
| 500 |
+
Decode routes only top_k pairs, too few to amortise two kernel launches
|
| 501 |
+
plus a device sync, so below PARTITION_ON_HOST pairs we pull the indices
|
| 502 |
+
down in one transfer and partition with numpy instead. That alone took
|
| 503 |
+
decode from 1.44x to 1.16x.
|
| 504 |
+
"""
|
| 505 |
+
|
| 506 |
+
# Below this many (token, slot) pairs, partition on the host.
|
| 507 |
+
PARTITION_ON_HOST = 1024
|
| 508 |
+
|
| 509 |
+
def __init__(self, input_dims, hidden_dims, n_hi, n_lo, activation):
|
| 510 |
+
super().__init__()
|
| 511 |
+
if n_hi <= 0 or n_lo <= 0:
|
| 512 |
+
raise ValueError(f"both banks must be non-empty, got {n_hi}/{n_lo}")
|
| 513 |
+
self.n_hi, self.n_lo = n_hi, n_lo
|
| 514 |
+
self.bank_hi = SwitchGLU(input_dims, hidden_dims, n_hi, activation=activation)
|
| 515 |
+
self.bank_lo = SwitchGLU(input_dims, hidden_dims, n_lo, activation=activation)
|
| 516 |
+
|
| 517 |
+
@property
|
| 518 |
+
def num_experts(self):
|
| 519 |
+
return self.n_hi + self.n_lo
|
| 520 |
+
|
| 521 |
+
@staticmethod
|
| 522 |
+
def _run(bank, xp, idx):
|
| 523 |
+
"""One bank over an already-sorted slice of pairs."""
|
| 524 |
+
up = bank.up_proj(xp, idx, sorted_indices=True)
|
| 525 |
+
gate = bank.gate_proj(xp, idx, sorted_indices=True)
|
| 526 |
+
return bank.down_proj(bank.activation(up, gate), idx, sorted_indices=True)
|
| 527 |
+
|
| 528 |
+
def __call__(self, x: mx.array, indices: mx.array) -> mx.array:
|
| 529 |
+
lead = indices.shape[:-1]
|
| 530 |
+
K = indices.shape[-1]
|
| 531 |
+
flat = indices.reshape(-1)
|
| 532 |
+
|
| 533 |
+
total = flat.size
|
| 534 |
+
if total <= self.PARTITION_ON_HOST:
|
| 535 |
+
# one host transfer beats argsort-kernel + sum-kernel + sync
|
| 536 |
+
host = np.array(flat.tolist(), dtype=np.int32)
|
| 537 |
+
order = mx.array(np.argsort(host, kind="stable"))
|
| 538 |
+
split = int((host < self.n_hi).sum())
|
| 539 |
+
else:
|
| 540 |
+
order = mx.argsort(flat)
|
| 541 |
+
split = int(mx.sum(flat < self.n_hi))
|
| 542 |
+
|
| 543 |
+
inv_order = mx.argsort(order)
|
| 544 |
+
sorted_idx = flat[order]
|
| 545 |
+
# one row per (token, slot) pair, in expert order
|
| 546 |
+
pairs = mx.expand_dims(x, (-2, -3)).flatten(0, -3)[order // K]
|
| 547 |
+
|
| 548 |
+
if split == 0:
|
| 549 |
+
out = self._run(self.bank_lo, pairs, sorted_idx - self.n_hi)
|
| 550 |
+
elif split == total:
|
| 551 |
+
out = self._run(self.bank_hi, pairs, sorted_idx)
|
| 552 |
+
else:
|
| 553 |
+
out = mx.concatenate(
|
| 554 |
+
[
|
| 555 |
+
self._run(self.bank_hi, pairs[:split], sorted_idx[:split]),
|
| 556 |
+
self._run(self.bank_lo, pairs[split:], sorted_idx[split:] - self.n_hi),
|
| 557 |
+
],
|
| 558 |
+
axis=0,
|
| 559 |
+
)
|
| 560 |
+
|
| 561 |
+
return mx.unflatten(out[inv_order], 0, (*lead, K)).squeeze(-2)
|
| 562 |
+
|
| 563 |
+
|
| 564 |
+
class KimiSparseMoE(nn.Module):
|
| 565 |
+
"""LatentMoE: 896 experts run in a 3584-d latent, not the 7168-d residual.
|
| 566 |
+
|
| 567 |
+
hidden -> down_proj -> [top-16 of 896 experts] -> norm -> up_proj -> hidden
|
| 568 |
+
plus 2 shared experts applied to the *undown-projected* input.
|
| 569 |
+
"""
|
| 570 |
+
|
| 571 |
+
def __init__(
|
| 572 |
+
self,
|
| 573 |
+
args: ModelArgs,
|
| 574 |
+
num_experts: Optional[int] = None,
|
| 575 |
+
bank_split: int = 0,
|
| 576 |
+
):
|
| 577 |
+
super().__init__()
|
| 578 |
+
self.args = args
|
| 579 |
+
h = args.hidden_size
|
| 580 |
+
self.num_experts = num_experts if num_experts is not None else args.num_experts
|
| 581 |
+
self.bank_split = bank_split
|
| 582 |
+
self.use_latent = args.routed_expert_hidden_size is not None
|
| 583 |
+
self.moe_hidden = args.routed_expert_hidden_size if self.use_latent else h
|
| 584 |
+
self.use_norm = args.latent_moe_use_norm
|
| 585 |
+
|
| 586 |
+
self.gate = nn.Linear(h, self.num_experts, bias=False)
|
| 587 |
+
self.e_score_correction_bias = mx.zeros((self.num_experts,), dtype=mx.float32)
|
| 588 |
+
if bank_split:
|
| 589 |
+
self.switch_mlp = TwoBankSwitchGLU(
|
| 590 |
+
self.moe_hidden,
|
| 591 |
+
args.moe_intermediate_size,
|
| 592 |
+
bank_split,
|
| 593 |
+
self.num_experts - bank_split,
|
| 594 |
+
activation=_situ_from_args(args),
|
| 595 |
+
)
|
| 596 |
+
else:
|
| 597 |
+
self.switch_mlp = SwitchGLU(
|
| 598 |
+
self.moe_hidden,
|
| 599 |
+
args.moe_intermediate_size,
|
| 600 |
+
self.num_experts,
|
| 601 |
+
activation=_situ_from_args(args),
|
| 602 |
+
)
|
| 603 |
+
|
| 604 |
+
if self.use_latent:
|
| 605 |
+
self.routed_expert_down_proj = nn.Linear(h, self.moe_hidden, bias=False)
|
| 606 |
+
self.routed_expert_up_proj = nn.Linear(self.moe_hidden, h, bias=False)
|
| 607 |
+
if self.use_norm:
|
| 608 |
+
self.routed_expert_norm = nn.RMSNorm(
|
| 609 |
+
self.moe_hidden, eps=args.rms_norm_eps
|
| 610 |
+
)
|
| 611 |
+
|
| 612 |
+
if args.num_shared_experts:
|
| 613 |
+
self.shared_experts = KimiMLP(
|
| 614 |
+
args, intermediate_size=args.moe_intermediate_size * args.num_shared_experts
|
| 615 |
+
)
|
| 616 |
+
else:
|
| 617 |
+
self.shared_experts = None
|
| 618 |
+
|
| 619 |
+
# Optional REAP saliency tap (scripts/reap_calibrate.py). Left as None in
|
| 620 |
+
# normal use and costs nothing. It exists so the calibration harness reads
|
| 621 |
+
# gates and per-expert outputs from *this* forward pass rather than a
|
| 622 |
+
# reimplementation that could drift from it. MLX keeps non-array
|
| 623 |
+
# attributes out of parameters()/children(), so this never touches
|
| 624 |
+
# quantization, saving or loading.
|
| 625 |
+
self.expert_stats_hook = None
|
| 626 |
+
|
| 627 |
+
def __call__(self, x: mx.array) -> mx.array:
|
| 628 |
+
identity = x
|
| 629 |
+
inds, weights = _group_expert_select(
|
| 630 |
+
self.gate(x),
|
| 631 |
+
self.e_score_correction_bias,
|
| 632 |
+
min(self.args.num_experts_per_token, self.num_experts),
|
| 633 |
+
self.args.num_expert_group,
|
| 634 |
+
self.args.topk_group,
|
| 635 |
+
self.args.routed_scaling_factor,
|
| 636 |
+
self.args.moe_renormalize,
|
| 637 |
+
self.args.moe_router_activation_func,
|
| 638 |
+
)
|
| 639 |
+
y = self.routed_expert_down_proj(x) if self.use_latent else x
|
| 640 |
+
y = self.switch_mlp(y, inds)
|
| 641 |
+
if self.expert_stats_hook is not None:
|
| 642 |
+
# y here is the per-expert output BEFORE gate weighting, shape
|
| 643 |
+
# (..., top_k, moe_hidden) -- exactly what REAP saliency needs.
|
| 644 |
+
self.expert_stats_hook(inds, weights, y)
|
| 645 |
+
y = (y * weights[..., None]).sum(axis=-2)
|
| 646 |
+
if self.use_latent:
|
| 647 |
+
if self.use_norm:
|
| 648 |
+
y = self.routed_expert_norm(y)
|
| 649 |
+
y = self.routed_expert_up_proj(y)
|
| 650 |
+
if self.shared_experts is not None:
|
| 651 |
+
y = y + self.shared_experts(identity)
|
| 652 |
+
return y
|
| 653 |
+
|
| 654 |
+
|
| 655 |
+
# ------------------------------------------------- checkpoint key remapping
|
| 656 |
+
#
|
| 657 |
+
# Single source of truth, shared by Model.sanitize (whole-checkpoint, used when
|
| 658 |
+
# mlx-lm loads a converted repo) and scripts/convert.py (per-layer, used when
|
| 659 |
+
# streaming the 1.56 TB source). Keeping one implementation is the point: the
|
| 660 |
+
# streaming converter never instantiates a Model, so a second copy of these
|
| 661 |
+
# rules would be free to drift out of sync and silently mis-place weights.
|
| 662 |
+
|
| 663 |
+
|
| 664 |
+
def is_kda_layer(args: ModelArgs, idx: int) -> bool:
|
| 665 |
+
# config lists kda_layers/full_attn_layers 1-BASED: config n -> layer n-1.
|
| 666 |
+
return (idx + 1) in args.linear_attn_config["kda_layers"]
|
| 667 |
+
|
| 668 |
+
|
| 669 |
+
def is_moe_layer(args: ModelArgs, idx: int) -> bool:
|
| 670 |
+
return (
|
| 671 |
+
args.num_experts > 0
|
| 672 |
+
and idx >= args.first_k_dense_replace
|
| 673 |
+
and idx % args.moe_layer_freq == 0
|
| 674 |
+
)
|
| 675 |
+
|
| 676 |
+
|
| 677 |
+
def experts_in_layer(args: ModelArgs, idx: int) -> int:
|
| 678 |
+
"""Expert count for one layer — varies after REAP pruning."""
|
| 679 |
+
if args.expert_counts:
|
| 680 |
+
return int(args.expert_counts[idx])
|
| 681 |
+
return args.num_experts
|
| 682 |
+
|
| 683 |
+
|
| 684 |
+
def bank_split_in_layer(args: ModelArgs, idx: int) -> int:
|
| 685 |
+
"""How many of a layer's experts sit in the high-precision bank (0 = one bank)."""
|
| 686 |
+
if not args.expert_bank_split:
|
| 687 |
+
return 0
|
| 688 |
+
return int(args.expert_bank_split[idx])
|
| 689 |
+
|
| 690 |
+
|
| 691 |
+
def remap_checkpoint(
|
| 692 |
+
weights: Dict[str, mx.array],
|
| 693 |
+
args: ModelArgs,
|
| 694 |
+
layer_indices: Optional[List[int]] = None,
|
| 695 |
+
stack_experts: bool = True,
|
| 696 |
+
) -> Dict[str, mx.array]:
|
| 697 |
+
"""Map Kimi-K3 checkpoint keys onto this module tree.
|
| 698 |
+
|
| 699 |
+
`layer_indices` restricts the layer loop (the converter passes the single
|
| 700 |
+
layer it is holding). `stack_experts=False` leaves per-expert tensors alone
|
| 701 |
+
so the converter can quantize each expert before stacking, which keeps peak
|
| 702 |
+
memory at ~6 GB per layer instead of ~59 GB.
|
| 703 |
+
"""
|
| 704 |
+
out: Dict[str, mx.array] = {}
|
| 705 |
+
for k, v in weights.items():
|
| 706 |
+
# K3 wraps the text tower as `language_model.*`; the vision tower and
|
| 707 |
+
# projector are handled outside mlx-lm.
|
| 708 |
+
if k.startswith("vision_tower.") or k.startswith("mm_projector."):
|
| 709 |
+
continue
|
| 710 |
+
out[k[len("language_model.") :] if k.startswith("language_model.") else k] = v
|
| 711 |
+
weights = out
|
| 712 |
+
|
| 713 |
+
if args.tie_word_embeddings:
|
| 714 |
+
weights.pop("lm_head.weight", None)
|
| 715 |
+
|
| 716 |
+
if layer_indices is None:
|
| 717 |
+
layer_indices = range(args.num_hidden_layers)
|
| 718 |
+
|
| 719 |
+
for i in layer_indices:
|
| 720 |
+
p = f"model.layers.{i}"
|
| 721 |
+
|
| 722 |
+
# [K3-2] AttnRes: fold `*_res_proj` / `*_res_norm` into the AttnResProj
|
| 723 |
+
# containers so the quantizer leaves them alone.
|
| 724 |
+
for src in ("self_attention_res", "mlp_res"):
|
| 725 |
+
for suffix, field_name in (("proj", "proj_weight"), ("norm", "norm_weight")):
|
| 726 |
+
key = f"{p}.{src}_{suffix}.weight"
|
| 727 |
+
if key in weights:
|
| 728 |
+
weights[f"{p}.{src}.{field_name}"] = weights.pop(key)
|
| 729 |
+
|
| 730 |
+
if is_moe_layer(args, i):
|
| 731 |
+
src = f"{p}.block_sparse_moe"
|
| 732 |
+
if stack_experts:
|
| 733 |
+
for w, dst in (("w1", "gate_proj"), ("w3", "up_proj"), ("w2", "down_proj")):
|
| 734 |
+
if f"{src}.experts.0.{w}.weight" in weights:
|
| 735 |
+
weights[f"{src}.switch_mlp.{dst}.weight"] = mx.stack(
|
| 736 |
+
[
|
| 737 |
+
weights.pop(f"{src}.experts.{e}.{w}.weight")
|
| 738 |
+
for e in range(experts_in_layer(args, i))
|
| 739 |
+
]
|
| 740 |
+
)
|
| 741 |
+
bias_key = f"{src}.gate.e_score_correction_bias"
|
| 742 |
+
if bias_key in weights:
|
| 743 |
+
weights[f"{src}.e_score_correction_bias"] = weights.pop(bias_key)
|
| 744 |
+
|
| 745 |
+
attn = f"{p}.self_attn"
|
| 746 |
+
if is_kda_layer(args, i):
|
| 747 |
+
for src, dst in (("q_conv1d", "q_conv"), ("k_conv1d", "k_conv"), ("v_conv1d", "v_conv")):
|
| 748 |
+
key = f"{attn}.{src}.weight"
|
| 749 |
+
if key in weights:
|
| 750 |
+
w = weights.pop(key)
|
| 751 |
+
if w.ndim == 3:
|
| 752 |
+
w = w.moveaxis(2, 1)
|
| 753 |
+
weights[f"{attn}.{dst}.conv.weight"] = w
|
| 754 |
+
dt = f"{attn}.dt_bias"
|
| 755 |
+
if dt in weights and weights[dt].ndim > 1:
|
| 756 |
+
weights[dt] = weights[dt].reshape(-1)
|
| 757 |
+
else:
|
| 758 |
+
# [K3-4] absorb kv_b_proj -> embed_q / unembed_out so the KV cache
|
| 759 |
+
# stores the 512-d latent rather than 96 materialised heads.
|
| 760 |
+
kv_b = f"{attn}.kv_b_proj.weight"
|
| 761 |
+
if kv_b in weights:
|
| 762 |
+
qk_nope = args.qk_nope_head_dim or 128
|
| 763 |
+
v_head = args.v_head_dim or 128
|
| 764 |
+
v = weights.pop(kv_b).reshape(args.num_attention_heads, qk_nope + v_head, -1)
|
| 765 |
+
weights[f"{attn}.embed_q.weight"] = mx.contiguous(
|
| 766 |
+
v[:, :qk_nope, :].swapaxes(-1, -2)
|
| 767 |
+
)
|
| 768 |
+
weights[f"{attn}.unembed_out.weight"] = mx.contiguous(v[:, qk_nope:, :])
|
| 769 |
+
|
| 770 |
+
for suffix, field_name in (("proj", "proj_weight"), ("norm", "norm_weight")):
|
| 771 |
+
key = f"model.output_attn_res_{suffix}.weight"
|
| 772 |
+
if key in weights:
|
| 773 |
+
weights[f"model.output_attn_res.{field_name}"] = weights.pop(key)
|
| 774 |
+
|
| 775 |
+
return weights
|
| 776 |
+
|
| 777 |
+
|
| 778 |
+
# ------------------------------------------------------------------- layers
|
| 779 |
+
|
| 780 |
+
|
| 781 |
+
class KimiDecoderLayer(nn.Module):
|
| 782 |
+
def __init__(self, args: ModelArgs, layer_idx: int):
|
| 783 |
+
super().__init__()
|
| 784 |
+
self.args = args
|
| 785 |
+
self.layer_idx = layer_idx
|
| 786 |
+
# config lists full_attn_layers/kda_layers 1-BASED: config n -> layer n-1.
|
| 787 |
+
kda_layers = args.linear_attn_config["kda_layers"]
|
| 788 |
+
self.is_linear = (layer_idx + 1) in kda_layers
|
| 789 |
+
|
| 790 |
+
if self.is_linear:
|
| 791 |
+
self.self_attn = KimiDeltaAttention(args, layer_idx)
|
| 792 |
+
else:
|
| 793 |
+
self.self_attn = KimiMLAAttention(args)
|
| 794 |
+
|
| 795 |
+
is_moe = (
|
| 796 |
+
args.num_experts > 0
|
| 797 |
+
and layer_idx >= args.first_k_dense_replace
|
| 798 |
+
and layer_idx % args.moe_layer_freq == 0
|
| 799 |
+
)
|
| 800 |
+
if is_moe:
|
| 801 |
+
self.block_sparse_moe = KimiSparseMoE(
|
| 802 |
+
args,
|
| 803 |
+
experts_in_layer(args, layer_idx),
|
| 804 |
+
bank_split_in_layer(args, layer_idx),
|
| 805 |
+
)
|
| 806 |
+
else:
|
| 807 |
+
self.mlp = KimiMLP(args)
|
| 808 |
+
self.is_moe = is_moe
|
| 809 |
+
|
| 810 |
+
self.input_layernorm = nn.RMSNorm(args.hidden_size, eps=args.rms_norm_eps)
|
| 811 |
+
self.post_attention_layernorm = nn.RMSNorm(
|
| 812 |
+
args.hidden_size, eps=args.rms_norm_eps
|
| 813 |
+
)
|
| 814 |
+
|
| 815 |
+
self.use_attn_res = args.attn_res_block_size is not None
|
| 816 |
+
if self.use_attn_res:
|
| 817 |
+
self.attn_res_block_size = args.attn_res_block_size
|
| 818 |
+
self.self_attention_res = AttnResProj(args.hidden_size)
|
| 819 |
+
self.mlp_res = AttnResProj(args.hidden_size)
|
| 820 |
+
|
| 821 |
+
def _ffn(self, x: mx.array) -> mx.array:
|
| 822 |
+
return self.block_sparse_moe(x) if self.is_moe else self.mlp(x)
|
| 823 |
+
|
| 824 |
+
def __call__(self, x, mask=None, cache=None, block_residual=None):
|
| 825 |
+
if not self.use_attn_res:
|
| 826 |
+
h = x + self.self_attn(self.input_layernorm(x), mask, cache)
|
| 827 |
+
return h + self._ffn(self.post_attention_layernorm(h)), block_residual
|
| 828 |
+
|
| 829 |
+
B, T, H = x.shape
|
| 830 |
+
eps = self.args.rms_norm_eps
|
| 831 |
+
prefix_sum = x
|
| 832 |
+
|
| 833 |
+
if block_residual is not None and block_residual.shape[1] > 0:
|
| 834 |
+
x = self.self_attention_res(
|
| 835 |
+
prefix_sum.reshape(-1, H), block_residual, eps
|
| 836 |
+
).reshape(B, T, H)
|
| 837 |
+
|
| 838 |
+
pushed = self.layer_idx % self.attn_res_block_size == 0
|
| 839 |
+
if pushed:
|
| 840 |
+
block_residual = mx.concatenate(
|
| 841 |
+
[block_residual, mx.expand_dims(prefix_sum.reshape(-1, H), 1)], axis=1
|
| 842 |
+
)
|
| 843 |
+
|
| 844 |
+
y = self.self_attn(self.input_layernorm(x), mask, cache)
|
| 845 |
+
prefix_sum = y if pushed else prefix_sum + y
|
| 846 |
+
|
| 847 |
+
x = self.mlp_res(prefix_sum.reshape(-1, H), block_residual, eps).reshape(B, T, H)
|
| 848 |
+
z = self._ffn(self.post_attention_layernorm(x))
|
| 849 |
+
return prefix_sum + z, block_residual
|
| 850 |
+
|
| 851 |
+
|
| 852 |
+
class KimiK3Model(nn.Module):
|
| 853 |
+
def __init__(self, args: ModelArgs):
|
| 854 |
+
super().__init__()
|
| 855 |
+
self.args = args
|
| 856 |
+
self.embed_tokens = nn.Embedding(args.vocab_size, args.hidden_size)
|
| 857 |
+
self.layers = [KimiDecoderLayer(args, i) for i in range(args.num_hidden_layers)]
|
| 858 |
+
self.norm = nn.RMSNorm(args.hidden_size, eps=args.rms_norm_eps)
|
| 859 |
+
|
| 860 |
+
self.use_attn_res = args.attn_res_block_size is not None
|
| 861 |
+
if self.use_attn_res:
|
| 862 |
+
self.output_attn_res = AttnResProj(args.hidden_size)
|
| 863 |
+
|
| 864 |
+
kda_layers = args.linear_attn_config["kda_layers"]
|
| 865 |
+
self.ssm_idx = kda_layers[0] - 1
|
| 866 |
+
self.attn_idx = next(
|
| 867 |
+
i for i in range(len(self.layers)) if (i + 1) not in kda_layers
|
| 868 |
+
)
|
| 869 |
+
|
| 870 |
+
def __call__(
|
| 871 |
+
self,
|
| 872 |
+
inputs: mx.array,
|
| 873 |
+
cache: Optional[List[Any]] = None,
|
| 874 |
+
inputs_embeds: Optional[mx.array] = None,
|
| 875 |
+
) -> mx.array:
|
| 876 |
+
# inputs_embeds lets the multimodal wrapper splice image features in
|
| 877 |
+
# place of the <|media_pad|> placeholder before the stack runs.
|
| 878 |
+
h = self.embed_tokens(inputs) if inputs_embeds is None else inputs_embeds
|
| 879 |
+
if cache is None:
|
| 880 |
+
cache = [None] * len(self.layers)
|
| 881 |
+
|
| 882 |
+
ssm_mask = create_ssm_mask(h, cache[self.ssm_idx])
|
| 883 |
+
attn_mask = create_attention_mask(h, cache[self.attn_idx], return_array=True)
|
| 884 |
+
|
| 885 |
+
B, T, H = h.shape
|
| 886 |
+
block_residual = mx.zeros((B * T, 0, H), dtype=h.dtype) if self.use_attn_res else None
|
| 887 |
+
|
| 888 |
+
for layer, layer_cache in zip(self.layers, cache):
|
| 889 |
+
mask = ssm_mask if layer.is_linear else attn_mask
|
| 890 |
+
h, block_residual = layer(h, mask, layer_cache, block_residual)
|
| 891 |
+
|
| 892 |
+
if self.use_attn_res:
|
| 893 |
+
h = self.output_attn_res(
|
| 894 |
+
h.reshape(-1, H), block_residual, self.args.rms_norm_eps
|
| 895 |
+
).reshape(B, T, H)
|
| 896 |
+
return self.norm(h)
|
| 897 |
+
|
| 898 |
+
|
| 899 |
+
class Model(nn.Module):
|
| 900 |
+
def __init__(self, args: ModelArgs):
|
| 901 |
+
super().__init__()
|
| 902 |
+
self.args = args
|
| 903 |
+
self.model_type = args.model_type
|
| 904 |
+
self.model = KimiK3Model(args)
|
| 905 |
+
self.lm_head = (
|
| 906 |
+
None
|
| 907 |
+
if args.tie_word_embeddings
|
| 908 |
+
else nn.Linear(args.hidden_size, args.vocab_size, bias=False)
|
| 909 |
+
)
|
| 910 |
+
|
| 911 |
+
def __call__(
|
| 912 |
+
self,
|
| 913 |
+
inputs: mx.array,
|
| 914 |
+
cache: Optional[List[Any]] = None,
|
| 915 |
+
inputs_embeds: Optional[mx.array] = None,
|
| 916 |
+
) -> mx.array:
|
| 917 |
+
out = self.model(inputs, cache, inputs_embeds=inputs_embeds)
|
| 918 |
+
if self.lm_head is None:
|
| 919 |
+
return self.model.embed_tokens.as_linear(out)
|
| 920 |
+
return self.lm_head(out)
|
| 921 |
+
|
| 922 |
+
@property
|
| 923 |
+
def layers(self):
|
| 924 |
+
return self.model.layers
|
| 925 |
+
|
| 926 |
+
def make_cache(self):
|
| 927 |
+
return [
|
| 928 |
+
ArraysCache(size=4) if layer.is_linear else KVCache()
|
| 929 |
+
for layer in self.layers
|
| 930 |
+
]
|
| 931 |
+
|
| 932 |
+
def sanitize(self, weights: Dict[str, mx.array]) -> Dict[str, mx.array]:
|
| 933 |
+
return remap_checkpoint(weights, self.args)
|
| 934 |
+
|
| 935 |
+
@property
|
| 936 |
+
def cast_predicate(self):
|
| 937 |
+
def predicate(path: str):
|
| 938 |
+
if "e_score_correction_bias" in path:
|
| 939 |
+
return False
|
| 940 |
+
if path.endswith("A_log") or path.endswith("dt_bias"):
|
| 941 |
+
return False
|
| 942 |
+
return True
|
| 943 |
+
|
| 944 |
+
return predicate
|
| 945 |
+
|
| 946 |
+
@property
|
| 947 |
+
def quant_predicate(self):
|
| 948 |
+
def predicate(path, _):
|
| 949 |
+
# router gate drives top-16-of-896 selection; 8 bits is cheap insurance
|
| 950 |
+
if path.endswith("block_sparse_moe.gate"):
|
| 951 |
+
return {"group_size": 64, "bits": 8}
|
| 952 |
+
return True
|
| 953 |
+
|
| 954 |
+
return predicate
|
kimi_k3_processor.py
ADDED
|
@@ -0,0 +1,187 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Kimi-K3 processor: wraps vision processor + tokenizer into a single interface.
|
| 2 |
+
|
| 3 |
+
Chat rendering (including XTML tool-result ordering) is handled by the
|
| 4 |
+
tokenizer's Python encoder; this processor adds multimodal media preprocessing.
|
| 5 |
+
"""
|
| 6 |
+
|
| 7 |
+
from transformers.feature_extraction_utils import BatchFeature
|
| 8 |
+
from transformers.processing_utils import ProcessorMixin
|
| 9 |
+
from transformers.utils import logging
|
| 10 |
+
|
| 11 |
+
from .media_utils import ensure_media_type
|
| 12 |
+
|
| 13 |
+
logger = logging.get_logger(__name__)
|
| 14 |
+
|
| 15 |
+
# ── KimiK3Processor ───────────────────────────────────────────────────
|
| 16 |
+
|
| 17 |
+
|
| 18 |
+
class KimiK3Processor(ProcessorMixin):
|
| 19 |
+
r"""
|
| 20 |
+
Constructs a KimiK3 processor which wraps a KimiK3 image processor
|
| 21 |
+
and a tokenizer into a single processor.
|
| 22 |
+
|
| 23 |
+
[`KimiK3Processor`] offers all the functionalities of
|
| 24 |
+
[`KimiK3VisionProcessor`] and [`TikTokenTokenizer`].
|
| 25 |
+
|
| 26 |
+
Args:
|
| 27 |
+
image_processor ([`KimiK3VisionProcessor`], *optional*):
|
| 28 |
+
The image processor is a required input.
|
| 29 |
+
tokenizer ([`TikTokenTokenizer`], *optional*):
|
| 30 |
+
The tokenizer is a required input.
|
| 31 |
+
chat_template (`str`, *optional*): Kept for ProcessorMixin
|
| 32 |
+
compatibility. Kimi K3 chat encoding is implemented in Python by
|
| 33 |
+
the tokenizer.
|
| 34 |
+
"""
|
| 35 |
+
|
| 36 |
+
attributes = ["image_processor", "tokenizer"]
|
| 37 |
+
valid_kwargs = ["chat_template"]
|
| 38 |
+
image_processor_class = "AutoImageProcessor"
|
| 39 |
+
tokenizer_class = "AutoTokenizer"
|
| 40 |
+
|
| 41 |
+
def __init__(
|
| 42 |
+
self,
|
| 43 |
+
image_processor=None,
|
| 44 |
+
tokenizer=None,
|
| 45 |
+
chat_template=None,
|
| 46 |
+
**kwargs,
|
| 47 |
+
):
|
| 48 |
+
super().__init__(image_processor,
|
| 49 |
+
tokenizer,
|
| 50 |
+
chat_template=chat_template)
|
| 51 |
+
self.media_processor = image_processor
|
| 52 |
+
self.image_placeholder = "<|kimi_image_placeholder|>"
|
| 53 |
+
|
| 54 |
+
# ── Media preprocessing ────────────────────────────────────────────
|
| 55 |
+
|
| 56 |
+
def update_raw_text(self, text: str, image_prompts: list[str]) -> str:
|
| 57 |
+
# Replace image placeholders
|
| 58 |
+
image_count = text.count(self.image_placeholder)
|
| 59 |
+
if image_count > 0:
|
| 60 |
+
assert image_count == len(image_prompts), (
|
| 61 |
+
f"image placeholder count {image_count} != "
|
| 62 |
+
f"image_prompts count {len(image_prompts)}")
|
| 63 |
+
text_parts = text.split(self.image_placeholder)
|
| 64 |
+
assert len(text_parts) == len(image_prompts) + 1
|
| 65 |
+
text = "".join([
|
| 66 |
+
text_parts[i] + image_prompts[i]
|
| 67 |
+
for i in range(len(image_prompts))
|
| 68 |
+
])
|
| 69 |
+
text += text_parts[-1]
|
| 70 |
+
|
| 71 |
+
return text
|
| 72 |
+
|
| 73 |
+
def preprocess_medias(self,
|
| 74 |
+
medias: list[dict]) -> tuple[list[dict], list[str]]:
|
| 75 |
+
"""Process media items and generate corresponding prompts.
|
| 76 |
+
|
| 77 |
+
Returns:
|
| 78 |
+
A tuple of (updated_medias, image_prompts).
|
| 79 |
+
"""
|
| 80 |
+
updated_medias = []
|
| 81 |
+
image_prompts = []
|
| 82 |
+
for media in medias:
|
| 83 |
+
if media['type'] == 'image':
|
| 84 |
+
updated_medias.append(media)
|
| 85 |
+
img = ensure_media_type(
|
| 86 |
+
media,
|
| 87 |
+
transparent_bg_config=self.media_processor.
|
| 88 |
+
_transparent_bg_config,
|
| 89 |
+
transparent_bg_fill_stage=self.media_processor.
|
| 90 |
+
_transparent_bg_fill_stage,
|
| 91 |
+
)['image']
|
| 92 |
+
w, h = img.size
|
| 93 |
+
image_prompts.append(
|
| 94 |
+
self.media_processor.make_image_prompt(w, h))
|
| 95 |
+
else:
|
| 96 |
+
raise ValueError(f"unsupported media type: {media['type']}")
|
| 97 |
+
return updated_medias, image_prompts
|
| 98 |
+
|
| 99 |
+
# ── Main entry points ──────────────────────────────────────────────
|
| 100 |
+
|
| 101 |
+
def __call__(self,
|
| 102 |
+
messages: list[dict] = None,
|
| 103 |
+
medias: list[dict] = None,
|
| 104 |
+
text: str = None,
|
| 105 |
+
return_tensors: str = "pt",
|
| 106 |
+
**kwargs) -> BatchFeature:
|
| 107 |
+
"""
|
| 108 |
+
Process multimodal inputs for Kimi-K3 model.
|
| 109 |
+
|
| 110 |
+
Args:
|
| 111 |
+
messages: List of message dicts with 'role' and 'content' fields.
|
| 112 |
+
If provided, medias and text will be extracted automatically.
|
| 113 |
+
medias: Pre-extracted list of media dicts.
|
| 114 |
+
text: Pre-formatted text string.
|
| 115 |
+
return_tensors: Format of returned tensors. Default: 'pt'.
|
| 116 |
+
**kwargs: Additional arguments passed to apply_chat_template.
|
| 117 |
+
|
| 118 |
+
Returns:
|
| 119 |
+
BatchFeature with fields: input_ids, attention_mask,
|
| 120 |
+
pixel_values, grid_thws.
|
| 121 |
+
"""
|
| 122 |
+
if messages is None and (medias is None or text is None):
|
| 123 |
+
raise ValueError(
|
| 124 |
+
"Provide either 'messages' or both 'medias' and 'text'")
|
| 125 |
+
|
| 126 |
+
if medias is not None and text is not None:
|
| 127 |
+
updated_medias, image_prompts = (self.preprocess_medias(medias))
|
| 128 |
+
preprocessed = self.media_processor.preprocess(
|
| 129 |
+
updated_medias, return_tensors=return_tensors)
|
| 130 |
+
text = self.update_raw_text(text, image_prompts)
|
| 131 |
+
text_inputs = self.tokenizer(text, return_tensors=return_tensors)
|
| 132 |
+
return BatchFeature(data={**text_inputs, **preprocessed.data})
|
| 133 |
+
|
| 134 |
+
if medias is None:
|
| 135 |
+
medias = self._extract_medias_from_messages(messages)
|
| 136 |
+
updated_medias, image_prompts = (self.preprocess_medias(medias))
|
| 137 |
+
preprocessed = self.media_processor.preprocess(
|
| 138 |
+
updated_medias, return_tensors=return_tensors)
|
| 139 |
+
|
| 140 |
+
if text is None:
|
| 141 |
+
text_inputs = self.tokenizer.apply_chat_template(
|
| 142 |
+
messages,
|
| 143 |
+
tokenize=True,
|
| 144 |
+
return_tensors=return_tensors,
|
| 145 |
+
return_dict=True,
|
| 146 |
+
image_prompts=image_prompts,
|
| 147 |
+
**kwargs)
|
| 148 |
+
return BatchFeature(data={**text_inputs, **preprocessed.data})
|
| 149 |
+
|
| 150 |
+
text = self.update_raw_text(text, image_prompts)
|
| 151 |
+
text_inputs = self.tokenizer(text, return_tensors=return_tensors)
|
| 152 |
+
return BatchFeature(data={**text_inputs, **preprocessed.data})
|
| 153 |
+
|
| 154 |
+
@staticmethod
|
| 155 |
+
def _extract_medias_from_messages(messages: list[dict]) -> list[dict]:
|
| 156 |
+
"""Extract media items from messages in a single pass."""
|
| 157 |
+
medias = []
|
| 158 |
+
for msg in messages:
|
| 159 |
+
if msg['role'] != 'user' or not msg.get('content'):
|
| 160 |
+
continue
|
| 161 |
+
|
| 162 |
+
for content_part in msg['content']:
|
| 163 |
+
if not isinstance(content_part, dict):
|
| 164 |
+
continue
|
| 165 |
+
|
| 166 |
+
content_type = content_part.get('type')
|
| 167 |
+
if content_type in ['image_url', 'image']:
|
| 168 |
+
image_data = content_part.get(content_type)
|
| 169 |
+
assert image_data is not None, f"image data is missing for content part: {content_part}"
|
| 170 |
+
medias.append({
|
| 171 |
+
'type': 'image',
|
| 172 |
+
'image': image_data,
|
| 173 |
+
})
|
| 174 |
+
return medias
|
| 175 |
+
|
| 176 |
+
def apply_chat_template(self, messages, **kwargs):
|
| 177 |
+
return self.tokenizer.apply_chat_template(messages, **kwargs)
|
| 178 |
+
|
| 179 |
+
def batch_decode(self, *args, **kwargs):
|
| 180 |
+
return self.tokenizer.batch_decode(*args, **kwargs)
|
| 181 |
+
|
| 182 |
+
def decode(self, *args, **kwargs):
|
| 183 |
+
return self.tokenizer.decode(*args, **kwargs)
|
| 184 |
+
|
| 185 |
+
@property
|
| 186 |
+
def model_input_names(self):
|
| 187 |
+
return ['input_ids', 'attention_mask', 'pixel_values', 'grid_thws']
|
kimi_k3_vision.py
ADDED
|
@@ -0,0 +1,404 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# MLX port of Kimi-K3's vision tower — a 3D (video-capable) MoonViT variant plus
|
| 2 |
+
# the patchmergerv2 projector.
|
| 3 |
+
#
|
| 4 |
+
# 0.49 B params, 0.02% of the model, and the only reason K3 is multimodal.
|
| 5 |
+
#
|
| 6 |
+
# Closest existing MLX code is mlx-vlm's `kimi_vl/vision.py` (MoonViT for
|
| 7 |
+
# Kimi-VL). This shares its complex64 2D-rope approach but differs in six ways,
|
| 8 |
+
# each marked [K3-V*] below:
|
| 9 |
+
#
|
| 10 |
+
# [K3-V1] 3D grids (t,h,w): a 1D sin-cos *time* embedding is added on top of
|
| 11 |
+
# the learnable 2D grid, and rope frequencies repeat per frame.
|
| 12 |
+
# [K3-V2] bilinear position-embedding interpolation (Kimi-VL used bicubic);
|
| 13 |
+
# mlx-vlm ships bicubic/nearest kernels only, so it is written here.
|
| 14 |
+
# [K3-V3] qkv_hidden_size 1536 != hidden 1024 — attention runs wider than the
|
| 15 |
+
# residual stream, so wo is [1024, 1536] and is NOT square.
|
| 16 |
+
# [K3-V4] RMSNorm, not LayerNorm.
|
| 17 |
+
# [K3-V5] no biases anywhere (attn_bias / linear_bias / patch_embed_proj_bias
|
| 18 |
+
# are all false).
|
| 19 |
+
# [K3-V6] sd2_tpool merge: mean-pool over frames, then 2x2 spatial merge, then
|
| 20 |
+
# a 2-layer MLP projector with *exact* (erf) GELU and a post-RMSNorm.
|
| 21 |
+
#
|
| 22 |
+
# The ViT blocks use tanh-approximate GELU (`PytorchGELUTanh` in the reference)
|
| 23 |
+
# while the projector uses exact `nn.GELU()`. They are different functions and
|
| 24 |
+
# the reference deliberately uses both; do not unify them.
|
| 25 |
+
|
| 26 |
+
import math
|
| 27 |
+
from dataclasses import dataclass, field
|
| 28 |
+
from typing import Any, Dict, List, Optional, Tuple
|
| 29 |
+
|
| 30 |
+
import mlx.core as mx
|
| 31 |
+
import mlx.nn as nn
|
| 32 |
+
|
| 33 |
+
# RMSNorm eps for the ViT blocks. The reference constructs `nn.RMSNorm(dim)`
|
| 34 |
+
# with no eps, so PyTorch falls back to finfo(dtype).eps — 1.19e-7 in fp32 but
|
| 35 |
+
# 7.8e-3 in bf16, which are materially different normalizers. We use the fp32
|
| 36 |
+
# value, matching an upcast implementation; `projector_ln_eps` (1e-5) is given
|
| 37 |
+
# explicitly by the config and is used only for the projector's post_norm.
|
| 38 |
+
VIT_NORM_EPS = 1.1920929e-07
|
| 39 |
+
|
| 40 |
+
|
| 41 |
+
@dataclass
|
| 42 |
+
class VisionConfig:
|
| 43 |
+
vt_hidden_size: int = 1024
|
| 44 |
+
vt_intermediate_size: int = 4096
|
| 45 |
+
vt_num_hidden_layers: int = 27
|
| 46 |
+
vt_num_attention_heads: int = 12
|
| 47 |
+
qkv_hidden_size: int = 1536
|
| 48 |
+
patch_size: int = 14
|
| 49 |
+
num_channels: int = 3
|
| 50 |
+
init_pos_emb_height: int = 64
|
| 51 |
+
init_pos_emb_width: int = 64
|
| 52 |
+
init_pos_emb_time: int = 4
|
| 53 |
+
pos_emb_type: str = "divided_fixed"
|
| 54 |
+
pos_emb_interpolation_mode: str = "bilinear"
|
| 55 |
+
merge_kernel_size: Tuple[int, int] = (2, 2)
|
| 56 |
+
merge_type: str = "sd2_tpool"
|
| 57 |
+
norm_type: str = "rmsnorm"
|
| 58 |
+
mlp_type: str = "mlp2"
|
| 59 |
+
mm_projector_type: str = "patchmergerv2"
|
| 60 |
+
mm_hidden_size: int = 1024
|
| 61 |
+
text_hidden_size: int = 7168
|
| 62 |
+
projector_hidden_act: str = "gelu"
|
| 63 |
+
projector_ln_eps: float = 1e-5
|
| 64 |
+
attn_bias: bool = False
|
| 65 |
+
linear_bias: bool = False
|
| 66 |
+
patch_embed_proj_bias: bool = False
|
| 67 |
+
rope_theta_base: float = 10000.0
|
| 68 |
+
rope_max_size: int = 512
|
| 69 |
+
|
| 70 |
+
@classmethod
|
| 71 |
+
def from_dict(cls, d: Dict[str, Any]) -> "VisionConfig":
|
| 72 |
+
known = {f for f in cls.__dataclass_fields__}
|
| 73 |
+
out = {k: v for k, v in d.items() if k in known}
|
| 74 |
+
if "merge_kernel_size" in out:
|
| 75 |
+
out["merge_kernel_size"] = tuple(out["merge_kernel_size"])
|
| 76 |
+
return cls(**out)
|
| 77 |
+
|
| 78 |
+
@property
|
| 79 |
+
def head_dim(self) -> int:
|
| 80 |
+
return self.qkv_hidden_size // self.vt_num_attention_heads
|
| 81 |
+
|
| 82 |
+
|
| 83 |
+
# ------------------------------------------------------- [K3-V2] bilinear
|
| 84 |
+
|
| 85 |
+
|
| 86 |
+
def bilinear_interpolate(x: mx.array, out_h: int, out_w: int) -> mx.array:
|
| 87 |
+
"""Resize (H, W, C) -> (out_h, out_w, C), matching torch's bilinear
|
| 88 |
+
`F.interpolate(..., align_corners=False)`.
|
| 89 |
+
|
| 90 |
+
torch maps destination index d to source coordinate
|
| 91 |
+
`max(0, scale * (d + 0.5) - 0.5)` and lerps between the two neighbours,
|
| 92 |
+
clamping the upper index at the edge. Getting the half-pixel offset wrong
|
| 93 |
+
shifts every position embedding by half a patch, which is the kind of error
|
| 94 |
+
that degrades quality without ever looking obviously broken.
|
| 95 |
+
"""
|
| 96 |
+
in_h, in_w = x.shape[0], x.shape[1]
|
| 97 |
+
if (in_h, in_w) == (out_h, out_w):
|
| 98 |
+
return x
|
| 99 |
+
|
| 100 |
+
def axis_index(in_n: int, out_n: int):
|
| 101 |
+
scale = in_n / out_n
|
| 102 |
+
src = mx.maximum(
|
| 103 |
+
(mx.arange(out_n, dtype=mx.float32) + 0.5) * scale - 0.5,
|
| 104 |
+
mx.array(0.0, mx.float32),
|
| 105 |
+
)
|
| 106 |
+
i0 = mx.floor(src)
|
| 107 |
+
w1 = src - i0
|
| 108 |
+
i0 = i0.astype(mx.int32)
|
| 109 |
+
i1 = mx.minimum(i0 + 1, mx.array(in_n - 1, mx.int32))
|
| 110 |
+
return i0, i1, w1
|
| 111 |
+
|
| 112 |
+
h0, h1, wh = axis_index(in_h, out_h)
|
| 113 |
+
w0, w1i, ww = axis_index(in_w, out_w)
|
| 114 |
+
|
| 115 |
+
xf = x.astype(mx.float32)
|
| 116 |
+
top = mx.take(xf, h0, axis=0) # (out_h, in_w, C)
|
| 117 |
+
bot = mx.take(xf, h1, axis=0)
|
| 118 |
+
wh = wh.reshape(out_h, 1, 1)
|
| 119 |
+
rows = top * (1.0 - wh) + bot * wh
|
| 120 |
+
|
| 121 |
+
left = mx.take(rows, w0, axis=1) # (out_h, out_w, C)
|
| 122 |
+
right = mx.take(rows, w1i, axis=1)
|
| 123 |
+
ww = ww.reshape(1, out_w, 1)
|
| 124 |
+
return left * (1.0 - ww) + right * ww
|
| 125 |
+
|
| 126 |
+
|
| 127 |
+
def sincos_1d(dim: int, length: int) -> mx.array:
|
| 128 |
+
"""(length, dim) sin-cos table; matches get_1d_sincos_pos_embed."""
|
| 129 |
+
omega = mx.arange(dim // 2, dtype=mx.float32) / (dim / 2.0)
|
| 130 |
+
omega = 1.0 / (10000.0**omega)
|
| 131 |
+
out = mx.outer(mx.arange(length, dtype=mx.float32), omega)
|
| 132 |
+
return mx.concatenate([mx.sin(out), mx.cos(out)], axis=1)
|
| 133 |
+
|
| 134 |
+
|
| 135 |
+
# ------------------------------------------------ [K3-V1] 3D position embed
|
| 136 |
+
|
| 137 |
+
|
| 138 |
+
class Learnable2DInterpPosEmbDivided(nn.Module):
|
| 139 |
+
"""Learnable 2D grid, bilinearly resized per image, plus a sin-cos time term."""
|
| 140 |
+
|
| 141 |
+
def __init__(self, cfg: VisionConfig):
|
| 142 |
+
super().__init__()
|
| 143 |
+
self.cfg = cfg
|
| 144 |
+
# reference does nn.init.normal_(weight); a zero init would make an
|
| 145 |
+
# unloaded tower emit exactly zero, which hides a failed weight load
|
| 146 |
+
self.weight = mx.random.normal(
|
| 147 |
+
(cfg.init_pos_emb_height, cfg.init_pos_emb_width, cfg.vt_hidden_size)
|
| 148 |
+
)
|
| 149 |
+
self._time = sincos_1d(cfg.vt_hidden_size, cfg.init_pos_emb_time)
|
| 150 |
+
|
| 151 |
+
def __call__(self, x: mx.array, grid_thws: List[Tuple[int, int, int]]) -> mx.array:
|
| 152 |
+
embs = []
|
| 153 |
+
for t, h, w in grid_thws:
|
| 154 |
+
if (h, w) == self.weight.shape[:2]:
|
| 155 |
+
emb2d = self.weight.reshape(-1, self.weight.shape[-1])
|
| 156 |
+
else:
|
| 157 |
+
emb2d = bilinear_interpolate(self.weight, h, w).reshape(
|
| 158 |
+
-1, self.weight.shape[-1]
|
| 159 |
+
)
|
| 160 |
+
if t == 1:
|
| 161 |
+
emb = emb2d
|
| 162 |
+
else:
|
| 163 |
+
emb = mx.expand_dims(emb2d, 0) + mx.expand_dims(self._time[:t], 1)
|
| 164 |
+
emb = emb.reshape(-1, emb.shape[-1])
|
| 165 |
+
embs.append(emb)
|
| 166 |
+
return x + mx.concatenate(embs, axis=0).astype(x.dtype)
|
| 167 |
+
|
| 168 |
+
|
| 169 |
+
class MoonVision3dPatchEmbed(nn.Module):
|
| 170 |
+
"""Patch projection. The reference is a Conv2d whose kernel equals the patch
|
| 171 |
+
size applied to pre-extracted (L, 3, 14, 14) patches, which is exactly a
|
| 172 |
+
linear map over the flattened patch — done as a matmul here to sidestep
|
| 173 |
+
MLX's NHWC conv weight layout entirely."""
|
| 174 |
+
|
| 175 |
+
def __init__(self, cfg: VisionConfig):
|
| 176 |
+
super().__init__()
|
| 177 |
+
self.cfg = cfg
|
| 178 |
+
fan_in = cfg.num_channels * cfg.patch_size * cfg.patch_size
|
| 179 |
+
# torch Conv2d default: uniform(-1/sqrt(fan_in), 1/sqrt(fan_in))
|
| 180 |
+
bound = 1.0 / math.sqrt(fan_in)
|
| 181 |
+
self.proj_weight = mx.random.uniform(
|
| 182 |
+
low=-bound, high=bound, shape=(cfg.vt_hidden_size, fan_in)
|
| 183 |
+
)
|
| 184 |
+
self.pos_emb = Learnable2DInterpPosEmbDivided(cfg)
|
| 185 |
+
|
| 186 |
+
def __call__(self, patches: mx.array, grid_thws) -> mx.array:
|
| 187 |
+
x = patches.reshape(patches.shape[0], -1).astype(self.proj_weight.dtype)
|
| 188 |
+
x = x @ self.proj_weight.T
|
| 189 |
+
return self.pos_emb(x, grid_thws)
|
| 190 |
+
|
| 191 |
+
|
| 192 |
+
# --------------------------------------------------------- [K3-V1] 2D rope
|
| 193 |
+
|
| 194 |
+
|
| 195 |
+
class Rope2DPosEmbRepeated(nn.Module):
|
| 196 |
+
"""freqs_cis[h, w] interleaves x (width) and y (height) frequencies:
|
| 197 |
+
entry 2i is cis(w * theta^(-4i/dim)), entry 2i+1 is cis(h * ...).
|
| 198 |
+
Repeated t times so every frame shares the same spatial rope."""
|
| 199 |
+
|
| 200 |
+
def __init__(self, dim: int, max_h: int, max_w: int, theta_base: float = 10000.0):
|
| 201 |
+
super().__init__()
|
| 202 |
+
assert dim % 4 == 0, "rope dim must be divisible by 4"
|
| 203 |
+
self.dim, self.max_h, self.max_w, self.theta_base = dim, max_h, max_w, theta_base
|
| 204 |
+
self._cache: Optional[mx.array] = None
|
| 205 |
+
|
| 206 |
+
def _full(self) -> mx.array:
|
| 207 |
+
if self._cache is None:
|
| 208 |
+
n = self.max_h * self.max_w
|
| 209 |
+
flat = mx.arange(0, n, dtype=mx.float32)
|
| 210 |
+
x_pos = flat % self.max_w
|
| 211 |
+
y_pos = mx.floor(flat / self.max_w)
|
| 212 |
+
dim_range = mx.arange(0, self.dim, 4)[: self.dim // 4].astype(mx.float32)
|
| 213 |
+
freqs = 1.0 / (self.theta_base ** (dim_range / self.dim))
|
| 214 |
+
xf, yf = mx.outer(x_pos, freqs), mx.outer(y_pos, freqs)
|
| 215 |
+
x_cis = mx.cos(xf) + 1j * mx.sin(xf)
|
| 216 |
+
y_cis = mx.cos(yf) + 1j * mx.sin(yf)
|
| 217 |
+
self._cache = mx.stack([x_cis, y_cis], axis=-1).reshape(
|
| 218 |
+
self.max_h, self.max_w, -1
|
| 219 |
+
)
|
| 220 |
+
return self._cache
|
| 221 |
+
|
| 222 |
+
def __call__(self, grid_thws: List[Tuple[int, int, int]]) -> mx.array:
|
| 223 |
+
full = self._full()
|
| 224 |
+
parts = []
|
| 225 |
+
for t, h, w in grid_thws:
|
| 226 |
+
assert 1 <= h <= self.max_h and 1 <= w <= self.max_w, (h, w)
|
| 227 |
+
base = full[:h, :w].reshape(-1, self.dim // 2)
|
| 228 |
+
parts.append(mx.tile(base, (t, 1)) if t > 1 else base)
|
| 229 |
+
return mx.concatenate(parts, axis=0)
|
| 230 |
+
|
| 231 |
+
|
| 232 |
+
def apply_rope(q: mx.array, k: mx.array, freqs_cis: mx.array):
|
| 233 |
+
"""q, k: (L, heads, head_dim); freqs_cis: (L, head_dim/2) complex64."""
|
| 234 |
+
f = mx.expand_dims(freqs_cis, axis=-2)
|
| 235 |
+
|
| 236 |
+
def rot(t):
|
| 237 |
+
pair = t.astype(mx.float32).reshape(*t.shape[:-1], -1, 2)
|
| 238 |
+
c = (pair[..., 0] + 1j * pair[..., 1]) * f
|
| 239 |
+
out = mx.stack([mx.real(c), mx.imag(c)], axis=-1)
|
| 240 |
+
return out.reshape(*t.shape).astype(t.dtype)
|
| 241 |
+
|
| 242 |
+
return rot(q), rot(k)
|
| 243 |
+
|
| 244 |
+
|
| 245 |
+
# ----------------------------------------------------------- encoder
|
| 246 |
+
|
| 247 |
+
|
| 248 |
+
class MLP2(nn.Module):
|
| 249 |
+
def __init__(self, dims: List[int], bias: bool = False):
|
| 250 |
+
super().__init__()
|
| 251 |
+
self.fc0 = nn.Linear(dims[0], dims[1], bias=bias)
|
| 252 |
+
self.fc1 = nn.Linear(dims[1], dims[2], bias=bias)
|
| 253 |
+
|
| 254 |
+
def __call__(self, x: mx.array) -> mx.array:
|
| 255 |
+
# PytorchGELUTanh -- the tanh approximation, not exact erf GELU.
|
| 256 |
+
return self.fc1(nn.gelu_approx(self.fc0(x)))
|
| 257 |
+
|
| 258 |
+
|
| 259 |
+
class MoonViTEncoderLayer(nn.Module):
|
| 260 |
+
def __init__(self, cfg: VisionConfig):
|
| 261 |
+
super().__init__()
|
| 262 |
+
self.cfg = cfg
|
| 263 |
+
h, qkv = cfg.vt_hidden_size, cfg.qkv_hidden_size
|
| 264 |
+
self.num_heads = cfg.vt_num_attention_heads
|
| 265 |
+
self.head_dim = cfg.head_dim
|
| 266 |
+
self.scale = self.head_dim**-0.5
|
| 267 |
+
|
| 268 |
+
self.norm0 = nn.RMSNorm(h, eps=VIT_NORM_EPS)
|
| 269 |
+
self.norm1 = nn.RMSNorm(h, eps=VIT_NORM_EPS)
|
| 270 |
+
self.wqkv = nn.Linear(h, qkv * 3, bias=cfg.attn_bias)
|
| 271 |
+
self.wo = nn.Linear(qkv, h, bias=cfg.attn_bias) # [K3-V3] not square
|
| 272 |
+
self.mlp = MLP2([h, cfg.vt_intermediate_size, h], bias=cfg.linear_bias)
|
| 273 |
+
|
| 274 |
+
def _attend(self, x, freqs_cis, spans):
|
| 275 |
+
L = x.shape[0]
|
| 276 |
+
qkv = self.wqkv(x).reshape(L, 3, self.num_heads, self.head_dim)
|
| 277 |
+
q, k, v = qkv[:, 0], qkv[:, 1], qkv[:, 2]
|
| 278 |
+
q, k = apply_rope(q, k, freqs_cis)
|
| 279 |
+
|
| 280 |
+
# Images are packed into one sequence but must not attend across each
|
| 281 |
+
# other. Looping the (few) images costs sum(L_i^2) instead of the
|
| 282 |
+
# (sum L_i)^2 an explicit block-diagonal mask would.
|
| 283 |
+
outs = []
|
| 284 |
+
for s, e in spans:
|
| 285 |
+
qi = q[s:e].transpose(1, 0, 2)[None]
|
| 286 |
+
ki = k[s:e].transpose(1, 0, 2)[None]
|
| 287 |
+
vi = v[s:e].transpose(1, 0, 2)[None]
|
| 288 |
+
o = mx.fast.scaled_dot_product_attention(qi, ki, vi, scale=self.scale)
|
| 289 |
+
outs.append(o[0].transpose(1, 0, 2).reshape(e - s, -1))
|
| 290 |
+
return self.wo(mx.concatenate(outs, axis=0))
|
| 291 |
+
|
| 292 |
+
def __call__(self, x, freqs_cis, spans):
|
| 293 |
+
x = x + self._attend(self.norm0(x), freqs_cis, spans)
|
| 294 |
+
return x + self.mlp(self.norm1(x))
|
| 295 |
+
|
| 296 |
+
|
| 297 |
+
class MoonViT3dEncoder(nn.Module):
|
| 298 |
+
def __init__(self, cfg: VisionConfig):
|
| 299 |
+
super().__init__()
|
| 300 |
+
self.rope_2d = Rope2DPosEmbRepeated(
|
| 301 |
+
cfg.head_dim, cfg.rope_max_size, cfg.rope_max_size, cfg.rope_theta_base
|
| 302 |
+
)
|
| 303 |
+
self.blocks = [MoonViTEncoderLayer(cfg) for _ in range(cfg.vt_num_hidden_layers)]
|
| 304 |
+
self.final_layernorm = nn.RMSNorm(cfg.vt_hidden_size, eps=VIT_NORM_EPS)
|
| 305 |
+
|
| 306 |
+
def __call__(self, x: mx.array, grid_thws) -> mx.array:
|
| 307 |
+
freqs_cis = self.rope_2d(grid_thws)
|
| 308 |
+
spans, pos = [], 0
|
| 309 |
+
for t, h, w in grid_thws:
|
| 310 |
+
spans.append((pos, pos + t * h * w))
|
| 311 |
+
pos += t * h * w
|
| 312 |
+
for blk in self.blocks:
|
| 313 |
+
x = blk(x, freqs_cis, spans)
|
| 314 |
+
return self.final_layernorm(x)
|
| 315 |
+
|
| 316 |
+
|
| 317 |
+
# ---------------------------------------------------- [K3-V6] merge + project
|
| 318 |
+
|
| 319 |
+
|
| 320 |
+
def tpool_patch_merger(x: mx.array, grid_thws, merge_kernel_size=(2, 2)):
|
| 321 |
+
"""-> list of (h/2 * w/2, 4, C): mean over frames, then 2x2 spatial merge."""
|
| 322 |
+
d = x.shape[-1]
|
| 323 |
+
kh, kw = merge_kernel_size
|
| 324 |
+
out, pos = [], 0
|
| 325 |
+
for t, h, w in grid_thws:
|
| 326 |
+
seq = x[pos : pos + t * h * w]
|
| 327 |
+
pos += t * h * w
|
| 328 |
+
nh, nw = h // kh, w // kw
|
| 329 |
+
s = seq.reshape(t, nh, kh, nw, kw, d).transpose(0, 1, 3, 2, 4, 5)
|
| 330 |
+
s = mx.mean(s, axis=0) # temporal pooling
|
| 331 |
+
out.append(s.reshape(nh * nw, kh * kw, d))
|
| 332 |
+
return out
|
| 333 |
+
|
| 334 |
+
|
| 335 |
+
class PatchMergerMLPV2(nn.Module):
|
| 336 |
+
def __init__(self, cfg: VisionConfig):
|
| 337 |
+
super().__init__()
|
| 338 |
+
inner = cfg.mm_hidden_size * cfg.merge_kernel_size[0] * cfg.merge_kernel_size[1]
|
| 339 |
+
self.inner = inner
|
| 340 |
+
# nn.Sequential(Linear, GELU, Linear) in the reference -> proj.0 / proj.2
|
| 341 |
+
self.proj_0 = nn.Linear(inner, inner, bias=False)
|
| 342 |
+
self.proj_2 = nn.Linear(inner, cfg.text_hidden_size, bias=False)
|
| 343 |
+
self.post_norm = nn.RMSNorm(cfg.text_hidden_size, eps=cfg.projector_ln_eps)
|
| 344 |
+
|
| 345 |
+
def __call__(self, feats: List[mx.array]) -> List[mx.array]:
|
| 346 |
+
lengths = [f.shape[0] for f in feats]
|
| 347 |
+
x = mx.concatenate([f.reshape(f.shape[0], -1) for f in feats], axis=0)
|
| 348 |
+
# exact erf GELU here -- deliberately different from the ViT's tanh GELU
|
| 349 |
+
x = self.post_norm(self.proj_2(nn.gelu(self.proj_0(x))))
|
| 350 |
+
out, pos = [], 0
|
| 351 |
+
for n in lengths:
|
| 352 |
+
out.append(x[pos : pos + n])
|
| 353 |
+
pos += n
|
| 354 |
+
return out
|
| 355 |
+
|
| 356 |
+
|
| 357 |
+
class VisionModel(nn.Module):
|
| 358 |
+
"""vision_tower + mm_projector: pixels -> text-space embeddings."""
|
| 359 |
+
|
| 360 |
+
def __init__(self, cfg: VisionConfig):
|
| 361 |
+
super().__init__()
|
| 362 |
+
self.cfg = cfg
|
| 363 |
+
if cfg.pos_emb_type != "divided_fixed":
|
| 364 |
+
raise NotImplementedError(cfg.pos_emb_type)
|
| 365 |
+
if cfg.merge_type != "sd2_tpool":
|
| 366 |
+
raise NotImplementedError(cfg.merge_type)
|
| 367 |
+
if cfg.mm_projector_type != "patchmergerv2":
|
| 368 |
+
raise NotImplementedError(cfg.mm_projector_type)
|
| 369 |
+
if cfg.norm_type != "rmsnorm":
|
| 370 |
+
raise NotImplementedError(cfg.norm_type)
|
| 371 |
+
self.patch_embed = MoonVision3dPatchEmbed(cfg)
|
| 372 |
+
self.encoder = MoonViT3dEncoder(cfg)
|
| 373 |
+
self.mm_projector = PatchMergerMLPV2(cfg)
|
| 374 |
+
|
| 375 |
+
def __call__(self, pixel_values: mx.array, grid_thws) -> List[mx.array]:
|
| 376 |
+
grid_thws = [tuple(int(v) for v in g) for g in grid_thws]
|
| 377 |
+
x = self.patch_embed(pixel_values, grid_thws)
|
| 378 |
+
x = self.encoder(x, grid_thws)
|
| 379 |
+
merged = tpool_patch_merger(x, grid_thws, self.cfg.merge_kernel_size)
|
| 380 |
+
return self.mm_projector(merged)
|
| 381 |
+
|
| 382 |
+
def sanitize(self, weights: Dict[str, mx.array]) -> Dict[str, mx.array]:
|
| 383 |
+
out: Dict[str, mx.array] = {}
|
| 384 |
+
for k, v in weights.items():
|
| 385 |
+
if k.startswith("vision_tower."):
|
| 386 |
+
k = k[len("vision_tower.") :]
|
| 387 |
+
if k == "patch_embed.proj.weight":
|
| 388 |
+
# Conv2d (out, in, kH, kW) -> flat linear (out, in*kH*kW).
|
| 389 |
+
# torch sums over (c, i, j) in that order, so a plain
|
| 390 |
+
# reshape is the correct flattening.
|
| 391 |
+
out["patch_embed.proj_weight"] = v.reshape(v.shape[0], -1)
|
| 392 |
+
elif k == "patch_embed.pos_emb.weight":
|
| 393 |
+
out["patch_embed.pos_emb.weight"] = v
|
| 394 |
+
else:
|
| 395 |
+
out[k] = v
|
| 396 |
+
elif k.startswith("mm_projector."):
|
| 397 |
+
k = k[len("mm_projector.") :]
|
| 398 |
+
if k == "proj.0.weight":
|
| 399 |
+
out["mm_projector.proj_0.weight"] = v
|
| 400 |
+
elif k == "proj.2.weight":
|
| 401 |
+
out["mm_projector.proj_2.weight"] = v
|
| 402 |
+
else:
|
| 403 |
+
out["mm_projector." + k] = v
|
| 404 |
+
return out
|
kimi_k3_vision_processing.py
ADDED
|
@@ -0,0 +1,179 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Image processor class for Kimi-K3.
|
| 2 |
+
"""
|
| 3 |
+
|
| 4 |
+
import json
|
| 5 |
+
from typing import Any, Dict, Optional, Union
|
| 6 |
+
|
| 7 |
+
import numpy as np
|
| 8 |
+
import torch
|
| 9 |
+
from PIL import Image
|
| 10 |
+
from transformers.image_processing_utils import (BaseImageProcessor,
|
| 11 |
+
BatchFeature)
|
| 12 |
+
from transformers.utils import TensorType
|
| 13 |
+
|
| 14 |
+
from .media_utils import (MediaInput, TransparentBgConfig, _to_tensor,
|
| 15 |
+
ensure_media_type, image_to_np, navit_patchify,
|
| 16 |
+
navit_resize_image, normalize)
|
| 17 |
+
|
| 18 |
+
|
| 19 |
+
class KimiK3VisionProcessor(BaseImageProcessor):
|
| 20 |
+
model_type = "kimi_k3"
|
| 21 |
+
|
| 22 |
+
def __init__(
|
| 23 |
+
self,
|
| 24 |
+
media_proc_cfg: dict,
|
| 25 |
+
**kwargs,
|
| 26 |
+
):
|
| 27 |
+
super().__init__(**kwargs)
|
| 28 |
+
self.media_proc_cfg = media_proc_cfg
|
| 29 |
+
|
| 30 |
+
@property
|
| 31 |
+
def _transparent_bg_config(self) -> Optional[TransparentBgConfig]:
|
| 32 |
+
cfg = self.media_proc_cfg.get("transparent_bg_config")
|
| 33 |
+
if cfg is None:
|
| 34 |
+
return None
|
| 35 |
+
if isinstance(cfg, TransparentBgConfig):
|
| 36 |
+
return cfg
|
| 37 |
+
return TransparentBgConfig(**cfg)
|
| 38 |
+
|
| 39 |
+
@property
|
| 40 |
+
def _transparent_bg_fill_stage(self) -> str:
|
| 41 |
+
return self.media_proc_cfg.get("transparent_bg_fill_stage",
|
| 42 |
+
"before_resize")
|
| 43 |
+
|
| 44 |
+
def media_tokens_calculator(self, media: MediaInput):
|
| 45 |
+
media = ensure_media_type(
|
| 46 |
+
media,
|
| 47 |
+
transparent_bg_config=self._transparent_bg_config,
|
| 48 |
+
transparent_bg_fill_stage=self._transparent_bg_fill_stage,
|
| 49 |
+
)
|
| 50 |
+
ret = self.get_resize_config(media)
|
| 51 |
+
return ret['num_tokens']
|
| 52 |
+
|
| 53 |
+
@classmethod
|
| 54 |
+
def make_image_prompt(cls, width: int, height: int) -> str:
|
| 55 |
+
"""Build the K3 image placeholder with resolution info."""
|
| 56 |
+
return (f"<|media_begin|>image {width}x{height}"
|
| 57 |
+
f"<|media_content|><|media_pad|><|media_end|>")
|
| 58 |
+
|
| 59 |
+
def get_resize_config(self, media_input: MediaInput) -> dict:
|
| 60 |
+
if media_input['type'] == 'image':
|
| 61 |
+
w, h = media_input['image'].size
|
| 62 |
+
ret = navit_resize_image(
|
| 63 |
+
w, h, self.media_proc_cfg['patch_size'],
|
| 64 |
+
self.media_proc_cfg['merge_kernel_size'],
|
| 65 |
+
self.media_proc_cfg['in_patch_limit'],
|
| 66 |
+
self.media_proc_cfg['patch_limit_on_one_side'],
|
| 67 |
+
self.media_proc_cfg['fixed_output_tokens'])
|
| 68 |
+
return ret
|
| 69 |
+
else:
|
| 70 |
+
raise ValueError("Unsupported type: {}".format(
|
| 71 |
+
media_input['type']))
|
| 72 |
+
|
| 73 |
+
def resize_image(self, image: Image.Image, new_width: int, new_height: int,
|
| 74 |
+
pad_width: int, pad_height: int) -> np.ndarray:
|
| 75 |
+
image_np = image_to_np(
|
| 76 |
+
image,
|
| 77 |
+
(new_width, new_height),
|
| 78 |
+
"resize",
|
| 79 |
+
transparent_bg_config=self._transparent_bg_config,
|
| 80 |
+
transparent_bg_fill_stage=self._transparent_bg_fill_stage,
|
| 81 |
+
)
|
| 82 |
+
image_np = np.pad(
|
| 83 |
+
image_np,
|
| 84 |
+
((0, pad_height), (0, pad_width), (0, 0)),
|
| 85 |
+
mode="constant",
|
| 86 |
+
constant_values=0,
|
| 87 |
+
)
|
| 88 |
+
return image_np
|
| 89 |
+
|
| 90 |
+
def preprocess(
|
| 91 |
+
self,
|
| 92 |
+
medias: list[MediaInput],
|
| 93 |
+
return_tensors: Optional[Union[str, TensorType]] = None,
|
| 94 |
+
) -> BatchFeature:
|
| 95 |
+
"""
|
| 96 |
+
Preprocess a atom vision input (images) into model-ready tensors.
|
| 97 |
+
|
| 98 |
+
Args:
|
| 99 |
+
medias: List of MediaInput.
|
| 100 |
+
return_tensors: Desired output format ('pt', 'np', 'tf', or None).
|
| 101 |
+
|
| 102 |
+
Returns:
|
| 103 |
+
BatchFeature containing 'pixel_values' and 'grid_thws' tensors.
|
| 104 |
+
"""
|
| 105 |
+
if not isinstance(medias, list):
|
| 106 |
+
medias = [medias]
|
| 107 |
+
if medias:
|
| 108 |
+
pixel_values = []
|
| 109 |
+
for item in medias:
|
| 110 |
+
item = ensure_media_type(
|
| 111 |
+
item,
|
| 112 |
+
transparent_bg_config=self._transparent_bg_config,
|
| 113 |
+
transparent_bg_fill_stage=self._transparent_bg_fill_stage,
|
| 114 |
+
)
|
| 115 |
+
resize_config = self.get_resize_config(item)
|
| 116 |
+
new_width, new_height, pad_width, pad_height = resize_config[
|
| 117 |
+
'new_width'], resize_config['new_height'], resize_config[
|
| 118 |
+
'pad_width'], resize_config['pad_height']
|
| 119 |
+
if item['type'] == 'image':
|
| 120 |
+
image = item['image']
|
| 121 |
+
image_np = self.resize_image(image, new_width, new_height,
|
| 122 |
+
pad_width, pad_height)
|
| 123 |
+
pixel_values.append(np.expand_dims(image_np, axis=0))
|
| 124 |
+
else:
|
| 125 |
+
raise ValueError("Unsupported type: {}".format(
|
| 126 |
+
item['type']))
|
| 127 |
+
normalized_pixel_values = []
|
| 128 |
+
image_std_inv = 1.0 / np.array(self.media_proc_cfg['image_std'])
|
| 129 |
+
image_mean = np.array(self.media_proc_cfg['image_mean'])
|
| 130 |
+
for pixels in pixel_values:
|
| 131 |
+
pixels = normalize(pixels, image_mean, image_std_inv)
|
| 132 |
+
pixels_and_thw = navit_patchify(
|
| 133 |
+
pixels,
|
| 134 |
+
self.media_proc_cfg['patch_size'],
|
| 135 |
+
)
|
| 136 |
+
normalized_pixel_values.append(pixels_and_thw)
|
| 137 |
+
|
| 138 |
+
pixel_values = torch.cat([
|
| 139 |
+
_to_tensor(pixel_value['pixel_values'])
|
| 140 |
+
for pixel_value in normalized_pixel_values
|
| 141 |
+
])
|
| 142 |
+
grid_thws = torch.cat([
|
| 143 |
+
_to_tensor(pixel_value['grid_thw'],
|
| 144 |
+
dtype=torch.int64).unsqueeze(0)
|
| 145 |
+
for pixel_value in normalized_pixel_values
|
| 146 |
+
])
|
| 147 |
+
|
| 148 |
+
data = {
|
| 149 |
+
'pixel_values': pixel_values,
|
| 150 |
+
'grid_thws': grid_thws,
|
| 151 |
+
}
|
| 152 |
+
|
| 153 |
+
else:
|
| 154 |
+
data = {}
|
| 155 |
+
|
| 156 |
+
return BatchFeature(data=data, tensor_type=return_tensors)
|
| 157 |
+
|
| 158 |
+
def __repr__(self):
|
| 159 |
+
return f"KimiK3VisionProcessor(media_proc_cfg={self.media_proc_cfg})"
|
| 160 |
+
|
| 161 |
+
def to_dict(self) -> Dict[str, Any]:
|
| 162 |
+
output = super().to_dict()
|
| 163 |
+
output["media_proc_cfg"] = self.media_proc_cfg
|
| 164 |
+
if "media_processor" in output:
|
| 165 |
+
del output["media_processor"]
|
| 166 |
+
return output
|
| 167 |
+
|
| 168 |
+
@classmethod
|
| 169 |
+
def from_dict(cls, config_dict: Dict[str, Any], **kwargs):
|
| 170 |
+
config = config_dict.copy()
|
| 171 |
+
media_proc_cfg = config.pop("media_proc_cfg", {})
|
| 172 |
+
return cls(media_proc_cfg=media_proc_cfg, **config, **kwargs)
|
| 173 |
+
|
| 174 |
+
def to_json_string(self):
|
| 175 |
+
dictionary = self.to_dict()
|
| 176 |
+
for key, value in dictionary.items():
|
| 177 |
+
if hasattr(value, 'tolist'):
|
| 178 |
+
dictionary[key] = value.tolist()
|
| 179 |
+
return json.dumps(dictionary, indent=2, sort_keys=True) + "\n"
|
kimi_k3_vl/__init__.py
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from .config import ModelConfig, TextConfig, VisionConfig
|
| 2 |
+
from .kimi_k3_vl import Model, merge_image_features
|
| 3 |
+
from .language import LanguageModel
|
| 4 |
+
from .vision import VisionModel
|
| 5 |
+
|
| 6 |
+
__all__ = [
|
| 7 |
+
"ModelConfig",
|
| 8 |
+
"TextConfig",
|
| 9 |
+
"VisionConfig",
|
| 10 |
+
"Model",
|
| 11 |
+
"LanguageModel",
|
| 12 |
+
"VisionModel",
|
| 13 |
+
"merge_image_features",
|
| 14 |
+
]
|
kimi_k3_vl/config.py
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Config plumbing for the Kimi-K3 mlx-vlm wrapper."""
|
| 2 |
+
|
| 3 |
+
from dataclasses import dataclass, field
|
| 4 |
+
from typing import Any, Dict, List, Optional
|
| 5 |
+
|
| 6 |
+
|
| 7 |
+
@dataclass
|
| 8 |
+
class BaseCfg:
|
| 9 |
+
@classmethod
|
| 10 |
+
def from_dict(cls, params: Dict[str, Any]):
|
| 11 |
+
known = {f for f in cls.__dataclass_fields__}
|
| 12 |
+
return cls(**{k: v for k, v in params.items() if k in known})
|
| 13 |
+
|
| 14 |
+
|
| 15 |
+
@dataclass
|
| 16 |
+
class TextConfig(BaseCfg):
|
| 17 |
+
"""Thin holder: the real parsing lives in kimi_k3.ModelArgs, which already
|
| 18 |
+
understands K3's nested `text_config`. Kept so mlx-vlm's config machinery
|
| 19 |
+
has something with the shape it expects."""
|
| 20 |
+
|
| 21 |
+
model_type: str = "kimi_linear"
|
| 22 |
+
hidden_size: int = 7168
|
| 23 |
+
num_hidden_layers: int = 93
|
| 24 |
+
vocab_size: int = 163840
|
| 25 |
+
raw: Dict[str, Any] = field(default_factory=dict)
|
| 26 |
+
|
| 27 |
+
@classmethod
|
| 28 |
+
def from_dict(cls, params: Dict[str, Any]):
|
| 29 |
+
return cls(
|
| 30 |
+
model_type=params.get("model_type", "kimi_linear"),
|
| 31 |
+
hidden_size=params.get("hidden_size", 7168),
|
| 32 |
+
num_hidden_layers=params.get("num_hidden_layers", 93),
|
| 33 |
+
vocab_size=params.get("vocab_size", 163840),
|
| 34 |
+
raw=dict(params),
|
| 35 |
+
)
|
| 36 |
+
|
| 37 |
+
|
| 38 |
+
@dataclass
|
| 39 |
+
class VisionConfig(BaseCfg):
|
| 40 |
+
model_type: str = "moonvit3d"
|
| 41 |
+
raw: Dict[str, Any] = field(default_factory=dict)
|
| 42 |
+
|
| 43 |
+
@classmethod
|
| 44 |
+
def from_dict(cls, params: Dict[str, Any]):
|
| 45 |
+
return cls(model_type=params.get("model_type", "moonvit3d"), raw=dict(params))
|
| 46 |
+
|
| 47 |
+
|
| 48 |
+
@dataclass
|
| 49 |
+
class ModelConfig(BaseCfg):
|
| 50 |
+
text_config: TextConfig
|
| 51 |
+
vision_config: VisionConfig
|
| 52 |
+
model_type: str = "kimi_k3"
|
| 53 |
+
# <|media_pad|>. Exactly ONE of these per image in the prompt; it expands to
|
| 54 |
+
# that image's full token count. See kimi_k3_vl.merge_image_features.
|
| 55 |
+
media_placeholder_token_id: int = 163605
|
| 56 |
+
image_token_index: Optional[int] = None
|
| 57 |
+
ignore_index: int = -100
|
| 58 |
+
pad_token_id: int = 163839
|
| 59 |
+
bos_token_id: int = 163584
|
| 60 |
+
eos_token_id: Optional[Any] = 163586
|
| 61 |
+
vocab_size: int = 163840
|
| 62 |
+
quantization: Optional[Dict[str, Any]] = None
|
| 63 |
+
raw: Dict[str, Any] = field(default_factory=dict)
|
| 64 |
+
|
| 65 |
+
@classmethod
|
| 66 |
+
def from_dict(cls, params: Dict[str, Any]):
|
| 67 |
+
# TextConfig.raw carries the *whole* config: kimi_k3.ModelArgs.from_dict
|
| 68 |
+
# already knows how to unwrap K3's nested `text_config`, so reusing that
|
| 69 |
+
# path avoids a second, divergent copy of the same unwrapping rules.
|
| 70 |
+
text = TextConfig.from_dict(params.get("text_config", {}))
|
| 71 |
+
text.raw = dict(params)
|
| 72 |
+
return cls(
|
| 73 |
+
text_config=text,
|
| 74 |
+
vision_config=VisionConfig.from_dict(params.get("vision_config", {})),
|
| 75 |
+
model_type=params.get("model_type", "kimi_k3"),
|
| 76 |
+
media_placeholder_token_id=params.get("media_placeholder_token_id", 163605),
|
| 77 |
+
image_token_index=params.get("image_token_index"),
|
| 78 |
+
ignore_index=params.get("ignore_index", -100),
|
| 79 |
+
pad_token_id=params.get("pad_token_id", 163839),
|
| 80 |
+
bos_token_id=params.get("bos_token_id", 163584),
|
| 81 |
+
eos_token_id=params.get("eos_token_id", 163586),
|
| 82 |
+
vocab_size=params.get("text_config", {}).get("vocab_size", 163840),
|
| 83 |
+
quantization=params.get("quantization"),
|
| 84 |
+
raw=dict(params),
|
| 85 |
+
)
|
kimi_k3_vl/kimi_k3_vl.py
ADDED
|
@@ -0,0 +1,191 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Kimi-K3 multimodal glue for mlx-vlm: vision tower -> prompt -> text tower.
|
| 2 |
+
|
| 3 |
+
The only genuinely interesting part is `merge_image_features`.
|
| 4 |
+
|
| 5 |
+
K3's processor rewrites each `<|kimi_image_placeholder|>` in the raw text into
|
| 6 |
+
|
| 7 |
+
<|media_begin|>image {W}x{H}<|media_content|><|media_pad|><|media_end|>
|
| 8 |
+
|
| 9 |
+
so the tokenized prompt carries **exactly one** `<|media_pad|>` (163605) per
|
| 10 |
+
image, which must then EXPAND into that image's full token count. That differs
|
| 11 |
+
from the more common LLaVA-style arrangement (and from mlx-vlm's existing
|
| 12 |
+
Kimi-VL glue), where the processor has already emitted one placeholder per image
|
| 13 |
+
token and merging is a same-length scatter.
|
| 14 |
+
|
| 15 |
+
Getting this wrong is quiet rather than loud: a same-length scatter would write
|
| 16 |
+
one image token and silently drop the rest, leaving a model that still generates
|
| 17 |
+
fluent text while being effectively blind to most of the picture.
|
| 18 |
+
"""
|
| 19 |
+
|
| 20 |
+
from typing import Any, List, Optional
|
| 21 |
+
|
| 22 |
+
import mlx.core as mx
|
| 23 |
+
import mlx.nn as nn
|
| 24 |
+
|
| 25 |
+
from .config import ModelConfig
|
| 26 |
+
from .language import LanguageModel
|
| 27 |
+
from .vision import VisionModel
|
| 28 |
+
|
| 29 |
+
try:
|
| 30 |
+
from mlx_vlm.models.base import InputEmbeddingsFeatures
|
| 31 |
+
except ImportError: # allow importing the glue without mlx-vlm present
|
| 32 |
+
from dataclasses import dataclass
|
| 33 |
+
|
| 34 |
+
@dataclass
|
| 35 |
+
class InputEmbeddingsFeatures: # type: ignore[no-redef]
|
| 36 |
+
inputs_embeds: mx.array
|
| 37 |
+
attention_mask: Optional[mx.array] = None
|
| 38 |
+
position_ids: Optional[mx.array] = None
|
| 39 |
+
|
| 40 |
+
|
| 41 |
+
def merge_image_features(
|
| 42 |
+
inputs_embeds: mx.array,
|
| 43 |
+
input_ids: mx.array,
|
| 44 |
+
image_features: List[mx.array],
|
| 45 |
+
placeholder_id: int,
|
| 46 |
+
pad_token_id: Optional[int] = None,
|
| 47 |
+
):
|
| 48 |
+
"""Expand each placeholder token into its image's feature block.
|
| 49 |
+
|
| 50 |
+
inputs_embeds : (B, S, D) token embeddings
|
| 51 |
+
input_ids : (B, S)
|
| 52 |
+
image_features: list of (n_i, D), one per image, in prompt order
|
| 53 |
+
returns : (B, S', D) merged embeddings and a (B, S') attention mask,
|
| 54 |
+
where S' = S - K + sum(n_i) for K placeholders
|
| 55 |
+
|
| 56 |
+
Rows are built by concatenating the spans between placeholders, which is
|
| 57 |
+
O(images) concatenations rather than O(sequence length) scatter indices, and
|
| 58 |
+
is far easier to check than the reference's index arithmetic. With B > 1 the
|
| 59 |
+
rows are LEFT-padded to the longest merged length, matching the reference's
|
| 60 |
+
`left_padding` branch (it is what generation with a KV cache expects).
|
| 61 |
+
"""
|
| 62 |
+
B, S = input_ids.shape
|
| 63 |
+
ids = input_ids.tolist()
|
| 64 |
+
|
| 65 |
+
n_ph_total = sum(1 for row in ids for t in row if t == placeholder_id)
|
| 66 |
+
if n_ph_total != len(image_features):
|
| 67 |
+
raise ValueError(
|
| 68 |
+
f"{n_ph_total} <|media_pad|> placeholder(s) in the prompt but "
|
| 69 |
+
f"{len(image_features)} image feature block(s). K3 uses exactly one "
|
| 70 |
+
f"placeholder per image."
|
| 71 |
+
)
|
| 72 |
+
|
| 73 |
+
rows, masks, taken = [], [], 0
|
| 74 |
+
for b in range(B):
|
| 75 |
+
parts, mask_parts, prev = [], [], 0
|
| 76 |
+
for j, tok in enumerate(ids[b]):
|
| 77 |
+
if tok != placeholder_id:
|
| 78 |
+
continue
|
| 79 |
+
if j > prev:
|
| 80 |
+
parts.append(inputs_embeds[b, prev:j])
|
| 81 |
+
mask_parts.append(mx.ones((j - prev,), dtype=mx.int32))
|
| 82 |
+
feat = image_features[taken].astype(inputs_embeds.dtype)
|
| 83 |
+
taken += 1
|
| 84 |
+
parts.append(feat)
|
| 85 |
+
mask_parts.append(mx.ones((feat.shape[0],), dtype=mx.int32))
|
| 86 |
+
prev = j + 1
|
| 87 |
+
if prev < S:
|
| 88 |
+
parts.append(inputs_embeds[b, prev:])
|
| 89 |
+
mask_parts.append(mx.ones((S - prev,), dtype=mx.int32))
|
| 90 |
+
rows.append(mx.concatenate(parts, axis=0))
|
| 91 |
+
masks.append(mx.concatenate(mask_parts, axis=0))
|
| 92 |
+
|
| 93 |
+
if B == 1:
|
| 94 |
+
merged = rows[0][None]
|
| 95 |
+
mask = masks[0][None]
|
| 96 |
+
else:
|
| 97 |
+
longest = max(r.shape[0] for r in rows)
|
| 98 |
+
padded, padded_masks = [], []
|
| 99 |
+
for r, m in zip(rows, masks):
|
| 100 |
+
gap = longest - r.shape[0]
|
| 101 |
+
if gap:
|
| 102 |
+
r = mx.concatenate([mx.zeros((gap, r.shape[-1]), r.dtype), r], axis=0)
|
| 103 |
+
m = mx.concatenate([mx.zeros((gap,), mx.int32), m], axis=0)
|
| 104 |
+
padded.append(r)
|
| 105 |
+
padded_masks.append(m)
|
| 106 |
+
merged = mx.stack(padded)
|
| 107 |
+
mask = mx.stack(padded_masks)
|
| 108 |
+
|
| 109 |
+
# zero out embeddings at pad positions, as the reference does
|
| 110 |
+
if pad_token_id is not None:
|
| 111 |
+
merged = mx.where(mask[..., None] == 0, mx.zeros_like(merged), merged)
|
| 112 |
+
return merged, mask
|
| 113 |
+
|
| 114 |
+
|
| 115 |
+
class Model(nn.Module):
|
| 116 |
+
def __init__(self, config: ModelConfig):
|
| 117 |
+
super().__init__()
|
| 118 |
+
self.config = config
|
| 119 |
+
self.model_type = config.model_type
|
| 120 |
+
self.vision_tower = VisionModel(config.vision_config)
|
| 121 |
+
self.language_model = LanguageModel(config.text_config)
|
| 122 |
+
|
| 123 |
+
# ------------------------------------------------------------------ api
|
| 124 |
+
|
| 125 |
+
@property
|
| 126 |
+
def layers(self):
|
| 127 |
+
return self.language_model.model.model.layers
|
| 128 |
+
|
| 129 |
+
def make_cache(self):
|
| 130 |
+
return self.language_model.model.make_cache()
|
| 131 |
+
|
| 132 |
+
def get_input_embeddings(
|
| 133 |
+
self,
|
| 134 |
+
input_ids: Optional[mx.array] = None,
|
| 135 |
+
pixel_values: Optional[mx.array] = None,
|
| 136 |
+
**kwargs,
|
| 137 |
+
) -> InputEmbeddingsFeatures:
|
| 138 |
+
embed = self.language_model.model.model.embed_tokens
|
| 139 |
+
inputs_embeds = embed(input_ids)
|
| 140 |
+
if pixel_values is None:
|
| 141 |
+
return InputEmbeddingsFeatures(inputs_embeds=inputs_embeds)
|
| 142 |
+
|
| 143 |
+
grids = (
|
| 144 |
+
kwargs.pop("grid_thws", None)
|
| 145 |
+
or kwargs.pop("image_grid_thw", None)
|
| 146 |
+
or kwargs.pop("image_grid_hws", None)
|
| 147 |
+
)
|
| 148 |
+
if grids is None:
|
| 149 |
+
raise ValueError("grid_thws is required alongside pixel_values")
|
| 150 |
+
if isinstance(grids, mx.array):
|
| 151 |
+
grids = grids.tolist()
|
| 152 |
+
grids = [tuple(int(v) for v in g) for g in grids]
|
| 153 |
+
|
| 154 |
+
feats = kwargs.get("cached_image_features")
|
| 155 |
+
if feats is None:
|
| 156 |
+
feats = self.vision_tower(pixel_values, grids)
|
| 157 |
+
|
| 158 |
+
placeholder = kwargs.pop("image_token_id", None) or self.config.media_placeholder_token_id
|
| 159 |
+
merged, mask = merge_image_features(
|
| 160 |
+
inputs_embeds, input_ids, feats, placeholder, self.config.pad_token_id
|
| 161 |
+
)
|
| 162 |
+
return InputEmbeddingsFeatures(inputs_embeds=merged, attention_mask=mask)
|
| 163 |
+
|
| 164 |
+
def __call__(
|
| 165 |
+
self,
|
| 166 |
+
input_ids: mx.array,
|
| 167 |
+
pixel_values: Optional[mx.array] = None,
|
| 168 |
+
cache: Optional[List[Any]] = None,
|
| 169 |
+
**kwargs,
|
| 170 |
+
) -> mx.array:
|
| 171 |
+
feats = self.get_input_embeddings(input_ids, pixel_values, **kwargs)
|
| 172 |
+
embeds = feats.inputs_embeds
|
| 173 |
+
# After merging, the token axis no longer lines up with input_ids, so the
|
| 174 |
+
# text tower must be driven by the embeddings' own length.
|
| 175 |
+
ids = mx.zeros(embeds.shape[:2], dtype=mx.int32)
|
| 176 |
+
return self.language_model(ids, cache=cache, inputs_embeds=embeds)
|
| 177 |
+
|
| 178 |
+
def sanitize(self, weights):
|
| 179 |
+
"""Route source keys to the two towers.
|
| 180 |
+
|
| 181 |
+
`vision_tower.*` / `mm_projector.*` are consumed by the vision tower;
|
| 182 |
+
`language_model.*` by the text tower. Both delegate to the same sanitize
|
| 183 |
+
functions the standalone models use, so there is one implementation of
|
| 184 |
+
the key mapping rather than a wrapper-specific copy that could drift.
|
| 185 |
+
"""
|
| 186 |
+
vis = {k: v for k, v in weights.items()
|
| 187 |
+
if k.startswith(("vision_tower.", "mm_projector."))}
|
| 188 |
+
txt = {k: v for k, v in weights.items() if not k.startswith(("vision_tower.", "mm_projector."))}
|
| 189 |
+
out = {f"vision_tower.{k}": v for k, v in self.vision_tower.sanitize(vis).items()}
|
| 190 |
+
out.update({f"language_model.{k}": v for k, v in self.language_model.sanitize(txt).items()})
|
| 191 |
+
return out
|
kimi_k3_vl/language.py
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Text-tower adapter: wraps kimi_k3.Model for mlx-vlm."""
|
| 2 |
+
|
| 3 |
+
from typing import Any, Dict, List, Optional
|
| 4 |
+
|
| 5 |
+
import mlx.core as mx
|
| 6 |
+
import mlx.nn as nn
|
| 7 |
+
|
| 8 |
+
from .config import TextConfig
|
| 9 |
+
|
| 10 |
+
try:
|
| 11 |
+
from mlx_lm.models import kimi_k3 as _k3
|
| 12 |
+
except ImportError as e: # pragma: no cover
|
| 13 |
+
raise ImportError(
|
| 14 |
+
"kimi_k3 is not registered with mlx-lm. It uses mlx-lm-relative imports "
|
| 15 |
+
"(.base, .cache, .switch_layers) so it must live in mlx_lm/models/. Run "
|
| 16 |
+
"`scripts/install_model.sh` once, or copy kimi_k3.py there."
|
| 17 |
+
) from e
|
| 18 |
+
|
| 19 |
+
|
| 20 |
+
class LanguageModel(nn.Module):
|
| 21 |
+
def __init__(self, config: TextConfig):
|
| 22 |
+
super().__init__()
|
| 23 |
+
args = _k3.ModelArgs.from_dict(config.raw)
|
| 24 |
+
self.args = args
|
| 25 |
+
self.model = _k3.Model(args)
|
| 26 |
+
|
| 27 |
+
def __call__(
|
| 28 |
+
self,
|
| 29 |
+
inputs: mx.array,
|
| 30 |
+
cache: Optional[List[Any]] = None,
|
| 31 |
+
inputs_embeds: Optional[mx.array] = None,
|
| 32 |
+
**kwargs,
|
| 33 |
+
) -> mx.array:
|
| 34 |
+
return self.model(inputs, cache=cache, inputs_embeds=inputs_embeds)
|
| 35 |
+
|
| 36 |
+
@property
|
| 37 |
+
def layers(self):
|
| 38 |
+
return self.model.model.layers
|
| 39 |
+
|
| 40 |
+
def make_cache(self):
|
| 41 |
+
return self.model.make_cache()
|
| 42 |
+
|
| 43 |
+
def sanitize(self, weights: Dict[str, mx.array]) -> Dict[str, mx.array]:
|
| 44 |
+
# kimi_k3.Model.sanitize already strips `language_model.` and drops the
|
| 45 |
+
# vision keys; prefix the result to sit under this module.
|
| 46 |
+
return {f"model.{k}": v for k, v in self.model.sanitize(weights).items()}
|
kimi_k3_vl/vision.py
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Vision-tower adapter: wraps kimi_k3_vision.VisionModel for mlx-vlm."""
|
| 2 |
+
|
| 3 |
+
import importlib.util
|
| 4 |
+
import os
|
| 5 |
+
import sys
|
| 6 |
+
from typing import Dict, List, Tuple
|
| 7 |
+
|
| 8 |
+
import mlx.core as mx
|
| 9 |
+
import mlx.nn as nn
|
| 10 |
+
|
| 11 |
+
from .config import VisionConfig as WrapperVisionConfig
|
| 12 |
+
|
| 13 |
+
|
| 14 |
+
def _load_vision_module():
|
| 15 |
+
"""kimi_k3_vision.py ships alongside the weights; import it from wherever
|
| 16 |
+
this package lives so a downloaded repo works without installation."""
|
| 17 |
+
if "kimi_k3_vision" in sys.modules:
|
| 18 |
+
return sys.modules["kimi_k3_vision"]
|
| 19 |
+
try:
|
| 20 |
+
import kimi_k3_vision # noqa: F401
|
| 21 |
+
|
| 22 |
+
return sys.modules["kimi_k3_vision"]
|
| 23 |
+
except ImportError:
|
| 24 |
+
pass
|
| 25 |
+
for d in (os.path.dirname(os.path.dirname(os.path.abspath(__file__))),
|
| 26 |
+
os.path.dirname(os.path.abspath(__file__))):
|
| 27 |
+
p = os.path.join(d, "kimi_k3_vision.py")
|
| 28 |
+
if os.path.exists(p):
|
| 29 |
+
spec = importlib.util.spec_from_file_location("kimi_k3_vision", p)
|
| 30 |
+
mod = importlib.util.module_from_spec(spec)
|
| 31 |
+
sys.modules["kimi_k3_vision"] = mod
|
| 32 |
+
spec.loader.exec_module(mod)
|
| 33 |
+
return mod
|
| 34 |
+
raise ImportError("kimi_k3_vision.py not found next to kimi_k3_vl/")
|
| 35 |
+
|
| 36 |
+
|
| 37 |
+
_v = _load_vision_module()
|
| 38 |
+
|
| 39 |
+
|
| 40 |
+
class VisionModel(nn.Module):
|
| 41 |
+
def __init__(self, config: WrapperVisionConfig):
|
| 42 |
+
super().__init__()
|
| 43 |
+
self.cfg = _v.VisionConfig.from_dict(config.raw)
|
| 44 |
+
self.tower = _v.VisionModel(self.cfg)
|
| 45 |
+
|
| 46 |
+
def __call__(self, pixel_values: mx.array, grid_thws) -> List[mx.array]:
|
| 47 |
+
"""pixel_values: (L, 3, patch, patch) pre-extracted patches.
|
| 48 |
+
Returns one (n_i, text_hidden) array per image."""
|
| 49 |
+
return self.tower(pixel_values, grid_thws)
|
| 50 |
+
|
| 51 |
+
def sanitize(self, weights: Dict[str, mx.array]) -> Dict[str, mx.array]:
|
| 52 |
+
return {f"tower.{k}": v for k, v in self.tower.sanitize(weights).items()}
|
media_utils.py
ADDED
|
@@ -0,0 +1,376 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import base64
|
| 2 |
+
import functools
|
| 3 |
+
import io
|
| 4 |
+
import math
|
| 5 |
+
from dataclasses import dataclass
|
| 6 |
+
from typing import Literal, TypedDict
|
| 7 |
+
|
| 8 |
+
import numpy as np
|
| 9 |
+
from PIL import Image
|
| 10 |
+
|
| 11 |
+
|
| 12 |
+
class ImageInput(TypedDict):
|
| 13 |
+
type: Literal['image']
|
| 14 |
+
image: Image.Image
|
| 15 |
+
|
| 16 |
+
|
| 17 |
+
MediaInput = ImageInput
|
| 18 |
+
|
| 19 |
+
|
| 20 |
+
@dataclass
|
| 21 |
+
class TransparentBgConfig:
|
| 22 |
+
"""The config of the transparent background."""
|
| 23 |
+
|
| 24 |
+
pattern: Literal["white", "black", "gray", "chessboard"] = "black"
|
| 25 |
+
"""The pattern of the transparent background."""
|
| 26 |
+
|
| 27 |
+
chessboard_square_size: int = 16
|
| 28 |
+
"""The size of the squares in the chessboard background."""
|
| 29 |
+
|
| 30 |
+
chessboard_square_on_top_left: bool = True
|
| 31 |
+
"""Whether to start the chessboard with a white square on the top left."""
|
| 32 |
+
|
| 33 |
+
chessboard_white_value: int = 255
|
| 34 |
+
"""The value of the white pixels in the background."""
|
| 35 |
+
|
| 36 |
+
chessboard_gray_value: int = 200
|
| 37 |
+
"""The value of the gray pixels in the background."""
|
| 38 |
+
|
| 39 |
+
|
| 40 |
+
@functools.lru_cache(maxsize=256)
|
| 41 |
+
def _create_chessboard_background(
|
| 42 |
+
height: int,
|
| 43 |
+
width: int,
|
| 44 |
+
square_size: int,
|
| 45 |
+
square_on_top_left: bool,
|
| 46 |
+
white_value: int,
|
| 47 |
+
gray_value: int,
|
| 48 |
+
) -> np.ndarray:
|
| 49 |
+
"""Create a chessboard background."""
|
| 50 |
+
bg = np.ones((height, width, 3), dtype=np.uint8) * white_value
|
| 51 |
+
for y in range(0, height, square_size):
|
| 52 |
+
for x in range(0, width, square_size):
|
| 53 |
+
if (y // square_size + x // square_size) % 2 == (
|
| 54 |
+
1 if square_on_top_left else 0):
|
| 55 |
+
bg[y:y + square_size, x:x + square_size] = gray_value
|
| 56 |
+
return bg
|
| 57 |
+
|
| 58 |
+
|
| 59 |
+
def fill_transparent_bg_with(
|
| 60 |
+
image: Image.Image,
|
| 61 |
+
transparent_bg_config: TransparentBgConfig | None = None,
|
| 62 |
+
) -> Image.Image:
|
| 63 |
+
"""Composite a (possibly) transparent image onto a configured background.
|
| 64 |
+
|
| 65 |
+
When ``transparent_bg_config`` is ``None``, the image is simply converted
|
| 66 |
+
to RGB (preserving the historical behavior). Otherwise the alpha channel
|
| 67 |
+
is alpha-composited over a background generated according to the config.
|
| 68 |
+
"""
|
| 69 |
+
if transparent_bg_config is None:
|
| 70 |
+
return image.convert("RGB")
|
| 71 |
+
|
| 72 |
+
if image.mode == "RGB":
|
| 73 |
+
return image
|
| 74 |
+
|
| 75 |
+
has_alpha = "A" in image.getbands() or "transparency" in image.info
|
| 76 |
+
if not has_alpha:
|
| 77 |
+
return image.convert("RGB")
|
| 78 |
+
|
| 79 |
+
img = np.array(image.convert("RGBA"))
|
| 80 |
+
height, width = img.shape[:2]
|
| 81 |
+
bg_pattern = transparent_bg_config.pattern
|
| 82 |
+
if bg_pattern == "white":
|
| 83 |
+
bg = np.full((height, width, 3), 255, dtype=np.uint8)
|
| 84 |
+
elif bg_pattern == "black":
|
| 85 |
+
bg = np.zeros((height, width, 3), dtype=np.uint8)
|
| 86 |
+
elif bg_pattern == "gray":
|
| 87 |
+
bg = np.full((height, width, 3), 128, dtype=np.uint8)
|
| 88 |
+
elif bg_pattern == "chessboard":
|
| 89 |
+
bg = _create_chessboard_background(
|
| 90 |
+
height,
|
| 91 |
+
width,
|
| 92 |
+
transparent_bg_config.chessboard_square_size,
|
| 93 |
+
transparent_bg_config.chessboard_square_on_top_left,
|
| 94 |
+
transparent_bg_config.chessboard_white_value,
|
| 95 |
+
transparent_bg_config.chessboard_gray_value,
|
| 96 |
+
)
|
| 97 |
+
else:
|
| 98 |
+
raise ValueError(f"Invalid background pattern: {bg_pattern}")
|
| 99 |
+
|
| 100 |
+
alpha = img[:, :, 3]
|
| 101 |
+
img_rgb = img[:, :, :3]
|
| 102 |
+
alpha_normalized = alpha.astype(np.float32) / 255.0
|
| 103 |
+
alpha_3d = np.stack([alpha_normalized] * 3, axis=2)
|
| 104 |
+
result = alpha_3d * img_rgb + (1 - alpha_3d) * bg
|
| 105 |
+
result = result.astype(np.uint8)
|
| 106 |
+
return Image.fromarray(result)
|
| 107 |
+
|
| 108 |
+
|
| 109 |
+
def navit_resize_image(
|
| 110 |
+
width: int,
|
| 111 |
+
height: int,
|
| 112 |
+
patch_size: int,
|
| 113 |
+
merge_kernel_size: int,
|
| 114 |
+
in_patch_limit: int,
|
| 115 |
+
patch_limit_on_one_side: int,
|
| 116 |
+
fixed_output_tokens: int | None,
|
| 117 |
+
):
|
| 118 |
+
# Apply the patch limits.
|
| 119 |
+
s1 = math.sqrt(
|
| 120 |
+
in_patch_limit /
|
| 121 |
+
(max(1.0, width // patch_size) * max(1.0, height // patch_size)))
|
| 122 |
+
s2 = patch_limit_on_one_side * patch_size / width
|
| 123 |
+
s3 = patch_limit_on_one_side * patch_size / height
|
| 124 |
+
scale = min(1.0, s1, s2, s3)
|
| 125 |
+
new_w, new_h = max(1, int(width * scale)), max(1, int(height * scale))
|
| 126 |
+
new_w = min(new_w, patch_limit_on_one_side * patch_size)
|
| 127 |
+
new_h = min(new_h, patch_limit_on_one_side * patch_size)
|
| 128 |
+
|
| 129 |
+
# Calculate the padding to make the height and width divisible by the merge kernel size and patch size.
|
| 130 |
+
factor = merge_kernel_size * patch_size
|
| 131 |
+
|
| 132 |
+
pad_height = (factor - new_h % factor) % factor
|
| 133 |
+
pad_width = (factor - new_w % factor) % factor
|
| 134 |
+
|
| 135 |
+
if fixed_output_tokens is not None:
|
| 136 |
+
num_tokens = fixed_output_tokens
|
| 137 |
+
else:
|
| 138 |
+
# Calculate new dimensions after padding and patching
|
| 139 |
+
token_height = (new_h + pad_height) // factor
|
| 140 |
+
token_width = (new_w + pad_width) // factor
|
| 141 |
+
|
| 142 |
+
assert token_height * merge_kernel_size <= patch_limit_on_one_side, (
|
| 143 |
+
f"token_height {token_height} * merge_kernel_size {merge_kernel_size} > patch_limit_on_one_side {patch_limit_on_one_side}"
|
| 144 |
+
)
|
| 145 |
+
assert token_width * merge_kernel_size <= patch_limit_on_one_side, (
|
| 146 |
+
f"token_width {token_width} * merge_kernel_size {merge_kernel_size} > patch_limit_on_one_side {patch_limit_on_one_side}"
|
| 147 |
+
)
|
| 148 |
+
|
| 149 |
+
num_tokens = token_height * token_width
|
| 150 |
+
return {
|
| 151 |
+
"num_tokens": num_tokens,
|
| 152 |
+
"new_width": new_w,
|
| 153 |
+
"new_height": new_h,
|
| 154 |
+
"pad_width": pad_width,
|
| 155 |
+
"pad_height": pad_height,
|
| 156 |
+
"sampled_nframes": 1,
|
| 157 |
+
}
|
| 158 |
+
|
| 159 |
+
|
| 160 |
+
def _to_pil(
|
| 161 |
+
data: str | bytes | Image.Image,
|
| 162 |
+
transparent_bg_config: TransparentBgConfig | None = None,
|
| 163 |
+
to_rgb: bool = True,
|
| 164 |
+
) -> Image.Image:
|
| 165 |
+
"""Load an image and (optionally) composite its transparent background.
|
| 166 |
+
|
| 167 |
+
Args:
|
| 168 |
+
data: A PIL Image, a base64 ``data:`` URL, a file path, or raw bytes.
|
| 169 |
+
transparent_bg_config: The config used to fill the transparent
|
| 170 |
+
background. ``None`` keeps the historical behavior of converting
|
| 171 |
+
to RGB without compositing.
|
| 172 |
+
to_rgb: If ``False`` the image is returned as-is (the
|
| 173 |
+
``transparent_bg_config`` is ignored). The caller is then
|
| 174 |
+
expected to call :func:`fill_transparent_bg_with` later — e.g.
|
| 175 |
+
after a resize.
|
| 176 |
+
"""
|
| 177 |
+
if isinstance(data, Image.Image):
|
| 178 |
+
image = data
|
| 179 |
+
elif isinstance(data, str):
|
| 180 |
+
if data.startswith("data:"):
|
| 181 |
+
raw_base64 = data.split(",")[1]
|
| 182 |
+
image = Image.open(io.BytesIO(base64.b64decode(raw_base64)))
|
| 183 |
+
else:
|
| 184 |
+
image = Image.open(data)
|
| 185 |
+
elif isinstance(data, bytes):
|
| 186 |
+
image = Image.open(io.BytesIO(data))
|
| 187 |
+
else:
|
| 188 |
+
raise ValueError(f"Unsupported data type: {type(data)}")
|
| 189 |
+
|
| 190 |
+
if not to_rgb:
|
| 191 |
+
return image
|
| 192 |
+
|
| 193 |
+
return fill_transparent_bg_with(image, transparent_bg_config)
|
| 194 |
+
|
| 195 |
+
|
| 196 |
+
def ensure_media_type(
|
| 197 |
+
media: MediaInput,
|
| 198 |
+
transparent_bg_config: TransparentBgConfig | None = None,
|
| 199 |
+
transparent_bg_fill_stage: Literal["before_resize",
|
| 200 |
+
"after_resize"] = "before_resize",
|
| 201 |
+
) -> MediaInput:
|
| 202 |
+
if media['type'] == 'image':
|
| 203 |
+
media['image'] = _to_pil(
|
| 204 |
+
media['image'],
|
| 205 |
+
transparent_bg_config=transparent_bg_config,
|
| 206 |
+
to_rgb=transparent_bg_fill_stage == "before_resize",
|
| 207 |
+
)
|
| 208 |
+
return media
|
| 209 |
+
else:
|
| 210 |
+
raise ValueError(f"Unsupported media type: {media['type']}")
|
| 211 |
+
|
| 212 |
+
|
| 213 |
+
def image_to_np(
|
| 214 |
+
image: Image.Image,
|
| 215 |
+
resize_to: tuple[int, int] | None = None,
|
| 216 |
+
mode: str = "resize",
|
| 217 |
+
raise_error_for_ill_resize: bool = True,
|
| 218 |
+
transparent_bg_config: TransparentBgConfig | None = None,
|
| 219 |
+
transparent_bg_fill_stage: Literal["before_resize",
|
| 220 |
+
"after_resize"] = "before_resize",
|
| 221 |
+
) -> np.ndarray:
|
| 222 |
+
"""Convert an image to a numpy array.
|
| 223 |
+
|
| 224 |
+
Args:
|
| 225 |
+
content: The image to convert.
|
| 226 |
+
resize_to: The size to resize the image to.
|
| 227 |
+
mode: The mode to resize the image to.
|
| 228 |
+
raise_error_for_ill_resize: Whether to raise an error for ill-sized resize.
|
| 229 |
+
transparent_bg_config: The config of the transparent background. Only
|
| 230 |
+
used when ``transparent_bg_fill_stage == "after_resize"`` (the
|
| 231 |
+
caller is responsible for filling before resize otherwise).
|
| 232 |
+
transparent_bg_fill_stage: When to composite the transparent
|
| 233 |
+
background — before or after the resize step.
|
| 234 |
+
|
| 235 |
+
Returns:
|
| 236 |
+
A numpy array.
|
| 237 |
+
"""
|
| 238 |
+
assert isinstance(image, Image.Image), "image must be a PIL Image"
|
| 239 |
+
if resize_to is not None:
|
| 240 |
+
if mode == "resize":
|
| 241 |
+
image = image.resize(resize_to, resample=Image.Resampling.BICUBIC)
|
| 242 |
+
if transparent_bg_fill_stage == "after_resize":
|
| 243 |
+
image = fill_transparent_bg_with(image, transparent_bg_config)
|
| 244 |
+
|
| 245 |
+
elif mode == "rescale_and_pad_to_center":
|
| 246 |
+
scale = min(resize_to[0] / image.width,
|
| 247 |
+
resize_to[1] / image.height, 1.0)
|
| 248 |
+
new_width = round(image.width * scale)
|
| 249 |
+
new_height = round(image.height * scale)
|
| 250 |
+
if new_width == 0 or new_height == 0:
|
| 251 |
+
if raise_error_for_ill_resize:
|
| 252 |
+
raise ValueError(
|
| 253 |
+
f"Invalid resize to: {resize_to}, from image size: {image.size}"
|
| 254 |
+
)
|
| 255 |
+
else:
|
| 256 |
+
return np.zeros((resize_to[1], resize_to[0], 3),
|
| 257 |
+
dtype=np.uint8)
|
| 258 |
+
|
| 259 |
+
image = image.resize((new_width, new_height),
|
| 260 |
+
resample=Image.Resampling.BICUBIC)
|
| 261 |
+
if transparent_bg_fill_stage == "after_resize":
|
| 262 |
+
image = fill_transparent_bg_with(image, transparent_bg_config)
|
| 263 |
+
padding_left = (resize_to[0] - new_width) // 2
|
| 264 |
+
padding_right = resize_to[0] - new_width - padding_left
|
| 265 |
+
padding_top = (resize_to[1] - new_height) // 2
|
| 266 |
+
padding_bottom = resize_to[1] - new_height - padding_top
|
| 267 |
+
image = np.asarray(image)
|
| 268 |
+
image = np.pad(
|
| 269 |
+
image,
|
| 270 |
+
((padding_top, padding_bottom), (padding_left, padding_right),
|
| 271 |
+
(0, 0)),
|
| 272 |
+
mode="constant",
|
| 273 |
+
constant_values=0,
|
| 274 |
+
)
|
| 275 |
+
assert image.shape == (resize_to[1], resize_to[0], 3)
|
| 276 |
+
|
| 277 |
+
elif mode == "rescale_and_pad_to_rightbottom":
|
| 278 |
+
scale = min(resize_to[0] / image.width,
|
| 279 |
+
resize_to[1] / image.height, 1.0)
|
| 280 |
+
new_width = round(image.width * scale)
|
| 281 |
+
new_height = round(image.height * scale)
|
| 282 |
+
if new_width == 0 or new_height == 0:
|
| 283 |
+
if raise_error_for_ill_resize:
|
| 284 |
+
raise ValueError(
|
| 285 |
+
f"Invalid resize to: {resize_to}, from image size: {image.size}"
|
| 286 |
+
)
|
| 287 |
+
else:
|
| 288 |
+
return np.zeros((resize_to[1], resize_to[0], 3),
|
| 289 |
+
dtype=np.uint8)
|
| 290 |
+
|
| 291 |
+
image = image.resize((new_width, new_height),
|
| 292 |
+
resample=Image.Resampling.BICUBIC)
|
| 293 |
+
if transparent_bg_fill_stage == "after_resize":
|
| 294 |
+
image = fill_transparent_bg_with(image, transparent_bg_config)
|
| 295 |
+
padding_right = resize_to[0] - new_width
|
| 296 |
+
padding_bottom = resize_to[1] - new_height
|
| 297 |
+
image = np.asarray(image)
|
| 298 |
+
image = np.pad(
|
| 299 |
+
image,
|
| 300 |
+
((0, padding_bottom), (0, padding_right), (0, 0)),
|
| 301 |
+
mode="constant",
|
| 302 |
+
constant_values=0,
|
| 303 |
+
)
|
| 304 |
+
assert image.shape == (resize_to[1], resize_to[0], 3)
|
| 305 |
+
|
| 306 |
+
else:
|
| 307 |
+
raise ValueError(f"Invalid mode: {mode}")
|
| 308 |
+
|
| 309 |
+
if isinstance(image, Image.Image):
|
| 310 |
+
return np.asarray(image)
|
| 311 |
+
else:
|
| 312 |
+
return image
|
| 313 |
+
|
| 314 |
+
|
| 315 |
+
def navit_patchify(pixel_values: np.ndarray,
|
| 316 |
+
patch_size: int) -> dict[str, np.ndarray]:
|
| 317 |
+
"""Reshape the pixel values to a navit shape.
|
| 318 |
+
|
| 319 |
+
Args:
|
| 320 |
+
pixel_values: np.ndarray, shape (t, h, w, c)
|
| 321 |
+
patch_size: int
|
| 322 |
+
|
| 323 |
+
Returns:
|
| 324 |
+
dict[str, np.ndarray]
|
| 325 |
+
- patches: np.ndarray, shape (t * h//patch_size * w//patch_size, c, patch_size, patch_size)
|
| 326 |
+
- grid_thw: np.ndarray, (t, h//patch_size, w//patch_size)
|
| 327 |
+
"""
|
| 328 |
+
T, H, W, C = pixel_values.shape
|
| 329 |
+
assert C == 3, "pixel_values must have 3 channels"
|
| 330 |
+
|
| 331 |
+
patches = pixel_values.reshape(T, H // patch_size, patch_size,
|
| 332 |
+
W // patch_size, patch_size, C)
|
| 333 |
+
# (T, H//patch_size, W//patch_size, C, patch_size, patch_size)
|
| 334 |
+
patches = patches.transpose(0, 1, 3, 5, 2, 4)
|
| 335 |
+
patches = patches.reshape(-1, C, patch_size, patch_size)
|
| 336 |
+
grid_thw = np.array([T, H // patch_size, W // patch_size])
|
| 337 |
+
return {"pixel_values": patches, "grid_thw": grid_thw}
|
| 338 |
+
|
| 339 |
+
|
| 340 |
+
def normalize(x: np.ndarray,
|
| 341 |
+
mean,
|
| 342 |
+
std_inv,
|
| 343 |
+
pixels_dtype: np.dtype = np.float32) -> np.ndarray:
|
| 344 |
+
"""Normalize the image.
|
| 345 |
+
|
| 346 |
+
Args:
|
| 347 |
+
x: The image to normalize. The shape is (..., 3). The dtype is uint8. The range is [0, 255].
|
| 348 |
+
mean: The mean of the image.
|
| 349 |
+
std_inv: The inverse of the std of the image.
|
| 350 |
+
pixels_dtype: The dtype of the image.
|
| 351 |
+
Returns:
|
| 352 |
+
The normalized image. The shape is (..., 3). The dtype is determined by the pixels_dtype.
|
| 353 |
+
"""
|
| 354 |
+
x = (x / 255.0).astype(pixels_dtype)
|
| 355 |
+
x -= mean
|
| 356 |
+
x *= std_inv
|
| 357 |
+
return x
|
| 358 |
+
|
| 359 |
+
|
| 360 |
+
def _to_tensor(data, **kwargs):
|
| 361 |
+
import torch
|
| 362 |
+
|
| 363 |
+
if isinstance(data, np.ndarray):
|
| 364 |
+
return torch.from_numpy(data).to(**kwargs)
|
| 365 |
+
elif isinstance(data, torch.Tensor):
|
| 366 |
+
return data.to(**kwargs)
|
| 367 |
+
elif isinstance(data, list):
|
| 368 |
+
return [_to_tensor(item, **kwargs) for item in data]
|
| 369 |
+
elif isinstance(data, tuple):
|
| 370 |
+
return tuple(_to_tensor(item, **kwargs) for item in data)
|
| 371 |
+
elif isinstance(data, dict):
|
| 372 |
+
return {k: _to_tensor(v, **kwargs) for k, v in data.items()}
|
| 373 |
+
elif data is None:
|
| 374 |
+
return None
|
| 375 |
+
else:
|
| 376 |
+
raise ValueError(f"Unsupported data type: {type(data)}")
|
model-00003-of-00075.safetensors
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:ee2a6f6b3b8818de3fa93332eb9d34557be603e7f7d2bb4630da4f4016b91c8a
|
| 3 |
+
size 5436028813
|
model-00006-of-00075.safetensors
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:6448dfa1de5cd56e634b6c5acdca9ccc1e7559b73f96609738565e7074626336
|
| 3 |
+
size 6105541561
|
model-00009-of-00075.safetensors
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:a0fe6e59ab5b98b11e7f9c592e92827ae1e74802aaa99a165b45e5c0d37632e5
|
| 3 |
+
size 6105541618
|
model-00011-of-00075.safetensors
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:dcd286a358ee62cc9a4bae5584adb6ed295f0f07c1429388cd0f204563cf984b
|
| 3 |
+
size 5379479293
|
model-00012-of-00075.safetensors
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:14fd63b64a66c8bc9749b5f9e1a9b1bd044d0c1531d37b15aff966ad68242f96
|
| 3 |
+
size 6309626568
|
model-00014-of-00075.safetensors
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:415f3fb772cf0cfe8a03b3d2edb3b073e69ad045286aa694d4b4929dabada170
|
| 3 |
+
size 6330481117
|
model-00017-of-00075.safetensors
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:b97fa6ceeb9b0b9a03a973942ba88c1ab2d452a4bfa74feb4498728d6509351c
|
| 3 |
+
size 6330481113
|
model-00018-of-00075.safetensors
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:f391b109cd133843386191e999944c534bd463e3c8ae70292c916e186b9789db
|
| 3 |
+
size 6330481117
|
model-00020-of-00075.safetensors
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:fd6c6bb53d90d0702e46999d4bd81cba9b960ca410a1f3c924c356b759f6a8f1
|
| 3 |
+
size 6253076934
|
model-00025-of-00075.safetensors
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:36ac781716d59e87c5062adbee45106affb467ce107fb7d32725d9b0915da767
|
| 3 |
+
size 6105541618
|
model-00029-of-00075.safetensors
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:7b6553530aed30872566fe901550585d0dffefe725017b21d7170680d4ee1b58
|
| 3 |
+
size 6330481117
|
model-00031-of-00075.safetensors
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:0cbd1012010bafcb00271c19414156c9d74668c677490aab480ee9c0c8de9555
|
| 3 |
+
size 5411251298
|
model-00032-of-00075.safetensors
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:d1c8bcd6a40a00656f30f78f0efeae50916ede029c40eaafa83599c87d98ca65
|
| 3 |
+
size 6277854537
|
model-00034-of-00075.safetensors
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:1a0efc2be3d2a3eb00ced9ff009e75f607b8ba2a81469d4a66eb4c670de91c71
|
| 3 |
+
size 6330481117
|
model-00037-of-00075.safetensors
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:3b85a77ab16156cea9916598e385c350511169777d2f96503566ae29de5ba3f6
|
| 3 |
+
size 6330481117
|
model-00038-of-00075.safetensors
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:88bca405e82263f293ec8589927a9947cd6853a818f46c00fefc76d959f9231e
|
| 3 |
+
size 6105541618
|
model-00041-of-00075.safetensors
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:2dcd9787752bbab263c8c3ad9858417080f839353eda5c0ad28434ecb59a4e04
|
| 3 |
+
size 6105541618
|
model-00042-of-00075.safetensors
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:450bb4e324453add694f6240e70a7392cadb651f58993be14e301ffba1298362
|
| 3 |
+
size 6330481113
|
model-00044-of-00075.safetensors
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:44d6e2abe1472823323f64b3121f7caec9a4cf60defd2f45575362dd53e118fe
|
| 3 |
+
size 6309626462
|
model-00047-of-00075.safetensors
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:074f2e094d6529f2e299d8fa2a3da9a908c8bf1a1333d56e30db1bea874c6698
|
| 3 |
+
size 5411251308
|
model-00048-of-00075.safetensors
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:80889374efb3add94a2abb8f370cc4c1eaca8724b1800210ea5a5490883f2ca1
|
| 3 |
+
size 6277854585
|
model-00053-of-00075.safetensors
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:d1eb7ee303b48d504ec1603a28732775692a7ba63b8ee8894c9f19ceca7d9e1e
|
| 3 |
+
size 6330481117
|
model-00055-of-00075.safetensors
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:1f0341a8bfa50479923e1f3daa907e9a53d6933f37ccd5eba6e7856942b497f4
|
| 3 |
+
size 5379479249
|
model-00056-of-00075.safetensors
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:2312406654989559f52b41069b9c8ffb6cd77f3c9efef98be07a07992f90147e
|
| 3 |
+
size 6534566067
|
model-00061-of-00075.safetensors
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:28b201cb736c90c8e31231bf5a0e4955a6a6412befeeb586262787566f8436f1
|
| 3 |
+
size 6330481117
|
model-00064-of-00075.safetensors
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:7271ea6542fcfb833be60c5562066aef5bc0c42d98a1633c7cb28b0d573964b1
|
| 3 |
+
size 6277854585
|
model-00068-of-00075.safetensors
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:29ea631bae5696c2d12a6629c0a272178ae0dcd5317d258615f6950f6b1c95a1
|
| 3 |
+
size 6253076934
|
model-00070-of-00075.safetensors
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:d82aaa0ec50044b5f246da82f776ae4fd8ce763f19cf3059e485bdc852a31104
|
| 3 |
+
size 6105541614
|
model-00073-of-00075.safetensors
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:fb3b5df114d34f45e4edb9fb3dbd39c96c02f90fbdb223def58c72ef1279d89d
|
| 3 |
+
size 6105541618
|
model-00075-of-00075.safetensors
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:da4abec65dd10c84a43b425a45e9c867557eaedab7735991e0f20b05df72e359
|
| 3 |
+
size 2225850591
|
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,38 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"media_proc_cfg": {
|
| 3 |
+
"in_patch_limit": 65536,
|
| 4 |
+
"patch_size": 14,
|
| 5 |
+
"image_mean": [
|
| 6 |
+
0.5,
|
| 7 |
+
0.5,
|
| 8 |
+
0.5
|
| 9 |
+
],
|
| 10 |
+
"image_std": [
|
| 11 |
+
0.5,
|
| 12 |
+
0.5,
|
| 13 |
+
0.5
|
| 14 |
+
],
|
| 15 |
+
"merge_kernel_size": 2,
|
| 16 |
+
"fixed_output_tokens": null,
|
| 17 |
+
"patch_limit_on_one_side": 512,
|
| 18 |
+
"in_patch_limit_each_frame": 16384,
|
| 19 |
+
"in_patch_limit_video": 655360,
|
| 20 |
+
"sample_fps": 8.0,
|
| 21 |
+
"max_num_frames_each_video": null,
|
| 22 |
+
"temporal_merge_kernel_size": 4,
|
| 23 |
+
"timestamp_mode": "hh:mm:ss.fff",
|
| 24 |
+
"transparent_bg_config": {
|
| 25 |
+
"pattern": "chessboard",
|
| 26 |
+
"chessboard_square_size": 8,
|
| 27 |
+
"chessboard_square_on_top_left": true,
|
| 28 |
+
"chessboard_white_value": 255,
|
| 29 |
+
"chessboard_gray_value": 180
|
| 30 |
+
},
|
| 31 |
+
"transparent_bg_fill_stage": "after_resize",
|
| 32 |
+
"config_type": "media_proc.processors.moonvit.MoonViTMediaProcessorConfig"
|
| 33 |
+
},
|
| 34 |
+
"auto_map": {
|
| 35 |
+
"AutoProcessor": "kimi_k3_processor.KimiK3Processor",
|
| 36 |
+
"AutoImageProcessor": "kimi_k3_vision_processing.KimiK3VisionProcessor"
|
| 37 |
+
}
|
| 38 |
+
}
|
tokenization_kimi.py
ADDED
|
@@ -0,0 +1,408 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
from logging import getLogger
|
| 3 |
+
from pathlib import Path
|
| 4 |
+
from shutil import copyfile
|
| 5 |
+
from typing import Dict, Iterator, List, Optional, Tuple, Union, cast
|
| 6 |
+
|
| 7 |
+
import tiktoken
|
| 8 |
+
from tiktoken.load import load_tiktoken_bpe
|
| 9 |
+
from tokenizers import AddedToken
|
| 10 |
+
from transformers.convert_slow_tokenizer import bytes_to_unicode
|
| 11 |
+
from transformers.tokenization_utils import PreTrainedTokenizer
|
| 12 |
+
|
| 13 |
+
try:
|
| 14 |
+
from .encoding_k3 import build_chat_segments, is_batched_conversation
|
| 15 |
+
except ImportError: # pragma: no cover - supports direct file execution/import.
|
| 16 |
+
from encoding_k3 import build_chat_segments, is_batched_conversation
|
| 17 |
+
|
| 18 |
+
logger = getLogger(__name__)
|
| 19 |
+
VOCAB_FILES_NAMES = {"vocab_file": "tiktoken.model"}
|
| 20 |
+
|
| 21 |
+
|
| 22 |
+
class TikTokenTokenizer(PreTrainedTokenizer):
|
| 23 |
+
"""
|
| 24 |
+
Tokenizing and encoding/decoding text using the Tiktoken tokenizer. See megatron/tokenizer/tiktoken_tokenizer.py.
|
| 25 |
+
|
| 26 |
+
This tokenizer inherits from [`PreTrainedTokenizer`] which contains most of the main methods. Users should refer to
|
| 27 |
+
this superclass for more information regarding those methods.
|
| 28 |
+
|
| 29 |
+
Args:
|
| 30 |
+
vocab_file (`str`):
|
| 31 |
+
The path to the Tiktoken model file.
|
| 32 |
+
bos_token (`str` or `tokenizers.AddedToken`, *optional*, defaults to `"<|begin_of_text|>",`):
|
| 33 |
+
The beginning of sequence token that was used during pretraining. Can be used a sequence classifier token.
|
| 34 |
+
eos_token (`str` or `tokenizers.AddedToken`, *optional*, defaults to `"<|end_of_text|>"`):
|
| 35 |
+
The end of sequence token.
|
| 36 |
+
unk_token (`str` or `tokenizers.AddedToken`, *optional*, defaults to `"<|reserved_special_token_249|>"`):
|
| 37 |
+
The unknown token. A token that is not in the vocabulary cannot be converted to an ID and is set to be this
|
| 38 |
+
token instead. The second to last item in special_tokens.
|
| 39 |
+
pad_token (`str` or `tokenizers.AddedToken`, *optional*, defaults to `"<|reserved_special_token_250|>"`):
|
| 40 |
+
The token used for padding, for example when batching sequences of different lengths.
|
| 41 |
+
additional_special_tokens (list of `str`, *optional*):
|
| 42 |
+
A tuple or a list of additional tokens, which will be marked as `special`, meaning that they will be
|
| 43 |
+
skipped when decoding if `skip_special_tokens` is set to `True`.
|
| 44 |
+
"""
|
| 45 |
+
|
| 46 |
+
vocab_files_names = VOCAB_FILES_NAMES
|
| 47 |
+
|
| 48 |
+
model_input_names = ["input_ids", "attention_mask"]
|
| 49 |
+
|
| 50 |
+
special_tokens: Dict[str, int]
|
| 51 |
+
|
| 52 |
+
num_reserved_special_tokens = 256
|
| 53 |
+
|
| 54 |
+
pat_str = "|".join([
|
| 55 |
+
r"""[\p{Han}]+""",
|
| 56 |
+
r"""[^\r\n\p{L}\p{N}]?[\p{Lu}\p{Lt}\p{Lm}\p{Lo}\p{M}&&[^\p{Han}]]*[\p{Ll}\p{Lm}\p{Lo}\p{M}&&[^\p{Han}]]+(?i:'s|'t|'re|'ve|'m|'ll|'d)?""",
|
| 57 |
+
r"""[^\r\n\p{L}\p{N}]?[\p{Lu}\p{Lt}\p{Lm}\p{Lo}\p{M}&&[^\p{Han}]]+[\p{Ll}\p{Lm}\p{Lo}\p{M}&&[^\p{Han}]]*(?i:'s|'t|'re|'ve|'m|'ll|'d)?""",
|
| 58 |
+
r"""\p{N}{1,3}""",
|
| 59 |
+
r""" ?[^\s\p{L}\p{N}]+[\r\n]*""",
|
| 60 |
+
r"""\s*[\r\n]+""",
|
| 61 |
+
r"""\s+(?!\S)""",
|
| 62 |
+
r"""\s+""",
|
| 63 |
+
])
|
| 64 |
+
|
| 65 |
+
def __init__(
|
| 66 |
+
self,
|
| 67 |
+
vocab_file,
|
| 68 |
+
bos_token: Union[str, AddedToken] = "[BOS]",
|
| 69 |
+
eos_token: Union[str, AddedToken] = "[EOS]",
|
| 70 |
+
unk_token: Union[str, AddedToken, None] = None,
|
| 71 |
+
pad_token: Union[str, AddedToken, None] = None,
|
| 72 |
+
additional_special_tokens: List[str] = None,
|
| 73 |
+
added_tokens_decoder: Optional[dict] = None,
|
| 74 |
+
**kwargs,
|
| 75 |
+
):
|
| 76 |
+
assert os.path.isfile(vocab_file), vocab_file
|
| 77 |
+
|
| 78 |
+
if additional_special_tokens is None:
|
| 79 |
+
additional_special_tokens = [
|
| 80 |
+
"<|im_end|>",
|
| 81 |
+
"<|im_user|>",
|
| 82 |
+
"<|im_assistant|>",
|
| 83 |
+
"<|start_header_id|>",
|
| 84 |
+
"<|end_header_id|>",
|
| 85 |
+
"[EOT]",
|
| 86 |
+
"<|im_system|>",
|
| 87 |
+
"<|im_middle|>",
|
| 88 |
+
]
|
| 89 |
+
|
| 90 |
+
if added_tokens_decoder:
|
| 91 |
+
special_tokens_mapping = {
|
| 92 |
+
i: added_tokens_decoder[i].content
|
| 93 |
+
for i in added_tokens_decoder
|
| 94 |
+
}
|
| 95 |
+
else:
|
| 96 |
+
special_tokens_mapping = {}
|
| 97 |
+
|
| 98 |
+
self.vocab_file = vocab_file
|
| 99 |
+
mergeable_ranks = load_tiktoken_bpe(vocab_file)
|
| 100 |
+
num_base_tokens = len(mergeable_ranks)
|
| 101 |
+
self.special_tokens = {
|
| 102 |
+
special_tokens_mapping.get(i, f"<|reserved_token_{i}|>"): i
|
| 103 |
+
for i in range(num_base_tokens, num_base_tokens +
|
| 104 |
+
self.num_reserved_special_tokens)
|
| 105 |
+
}
|
| 106 |
+
|
| 107 |
+
self.model = tiktoken.Encoding(
|
| 108 |
+
name=Path(vocab_file).name,
|
| 109 |
+
pat_str=self.pat_str,
|
| 110 |
+
mergeable_ranks=mergeable_ranks,
|
| 111 |
+
special_tokens=self.special_tokens,
|
| 112 |
+
)
|
| 113 |
+
logger.info(f"Reloaded tiktoken model from {vocab_file}")
|
| 114 |
+
|
| 115 |
+
self.n_words: int = self.model.n_vocab
|
| 116 |
+
# BOS / EOS token IDs
|
| 117 |
+
self.bos_id: int = self.special_tokens[str(bos_token)]
|
| 118 |
+
self.eos_id: int = self.special_tokens[str(eos_token)]
|
| 119 |
+
logger.info(
|
| 120 |
+
f"#words: {self.n_words} - BOS ID: {self.bos_id} - EOS ID: {self.eos_id}"
|
| 121 |
+
)
|
| 122 |
+
|
| 123 |
+
self.pad_id: int = self.special_tokens[str(pad_token)]
|
| 124 |
+
self.unk_id: int = self.special_tokens[str(unk_token)]
|
| 125 |
+
|
| 126 |
+
self.byte_encoder = bytes_to_unicode()
|
| 127 |
+
self.byte_decoder = {v: k for k, v in self.byte_encoder.items()}
|
| 128 |
+
|
| 129 |
+
self.decoder = {}
|
| 130 |
+
for i in range(self.n_words):
|
| 131 |
+
# Taken from https://gist.github.com/xenova/a452a6474428de0182b17605a98631ee
|
| 132 |
+
decoding = ''.join([
|
| 133 |
+
self.byte_encoder[ord(char)] for char in
|
| 134 |
+
self.model.decode_single_token_bytes(i).decode('latin-1')
|
| 135 |
+
])
|
| 136 |
+
self.decoder[i] = decoding
|
| 137 |
+
|
| 138 |
+
self.encoder = {}
|
| 139 |
+
for i in range(self.n_words):
|
| 140 |
+
if i in self.decoder:
|
| 141 |
+
self.encoder[self.decoder[i]] = i
|
| 142 |
+
|
| 143 |
+
super().__init__(
|
| 144 |
+
bos_token=bos_token,
|
| 145 |
+
eos_token=eos_token,
|
| 146 |
+
unk_token=unk_token,
|
| 147 |
+
pad_token=pad_token,
|
| 148 |
+
additional_special_tokens=additional_special_tokens,
|
| 149 |
+
added_tokens_decoder=added_tokens_decoder,
|
| 150 |
+
**kwargs,
|
| 151 |
+
)
|
| 152 |
+
self.all_special_ids_set = set(self.all_special_ids)
|
| 153 |
+
|
| 154 |
+
def _encode_text_piece(self, text: str,
|
| 155 |
+
allow_special_tokens: bool = True) -> List[int]:
|
| 156 |
+
# The tiktoken tokenizer can handle <=400k chars without
|
| 157 |
+
# pyo3_runtime.PanicException.
|
| 158 |
+
TIKTOKEN_MAX_ENCODE_CHARS = 400_000
|
| 159 |
+
|
| 160 |
+
# https://github.com/openai/tiktoken/issues/195
|
| 161 |
+
# Here we iterate over subsequences and split if we exceed the limit
|
| 162 |
+
# of max consecutive non-whitespace or whitespace characters.
|
| 163 |
+
MAX_NO_WHITESPACES_CHARS = 25_000
|
| 164 |
+
|
| 165 |
+
t: List[int] = []
|
| 166 |
+
for i in range(0, len(text), TIKTOKEN_MAX_ENCODE_CHARS):
|
| 167 |
+
for substr in self._split_whitespaces_or_nonwhitespaces(
|
| 168 |
+
text[i:i + TIKTOKEN_MAX_ENCODE_CHARS],
|
| 169 |
+
MAX_NO_WHITESPACES_CHARS,
|
| 170 |
+
):
|
| 171 |
+
if allow_special_tokens:
|
| 172 |
+
t.extend(
|
| 173 |
+
# structural markers: encode <|...|> as their special token IDs
|
| 174 |
+
self.model.encode(
|
| 175 |
+
substr,
|
| 176 |
+
allowed_special="all",
|
| 177 |
+
))
|
| 178 |
+
else:
|
| 179 |
+
t.extend(
|
| 180 |
+
# user/tool text: encode any <|...|> as ordinary BPE tokens (never as control tokens)
|
| 181 |
+
self.model.encode(
|
| 182 |
+
substr,
|
| 183 |
+
disallowed_special=(),
|
| 184 |
+
))
|
| 185 |
+
|
| 186 |
+
return t
|
| 187 |
+
|
| 188 |
+
def encode(self,
|
| 189 |
+
text: str,
|
| 190 |
+
allow_special_tokens: bool = True,
|
| 191 |
+
**kwargs) -> List[int]:
|
| 192 |
+
"""
|
| 193 |
+
Encodes a string into a list of token IDs.
|
| 194 |
+
|
| 195 |
+
Args:
|
| 196 |
+
text (str): The input string to be encoded.
|
| 197 |
+
|
| 198 |
+
Returns:
|
| 199 |
+
list[int]: A list of token IDs.
|
| 200 |
+
"""
|
| 201 |
+
# If there are other args, we should call super().encode because there are a lot of code
|
| 202 |
+
# to handle those args. supper().encode finally will call _tokenize and _convert_token_to_id.
|
| 203 |
+
# NOTE: our encode method is not compatible with the super().encode method,
|
| 204 |
+
# e.g. split_special_tokens' default is True in our encode method.
|
| 205 |
+
if len(kwargs) > 0:
|
| 206 |
+
logger.warning(f"Calling super().encode with {kwargs}")
|
| 207 |
+
return super().encode(text, **kwargs)
|
| 208 |
+
|
| 209 |
+
assert type(text) is str
|
| 210 |
+
return self._encode_text_piece(text,
|
| 211 |
+
allow_special_tokens=allow_special_tokens)
|
| 212 |
+
|
| 213 |
+
def decode(self, token_ids: Union[int, List[int]], **kwargs) -> str:
|
| 214 |
+
"""
|
| 215 |
+
Decodes a list of token IDs into a string.
|
| 216 |
+
|
| 217 |
+
Args:
|
| 218 |
+
token_ids (List[int]): The list of token IDs to be decoded.
|
| 219 |
+
|
| 220 |
+
Returns:
|
| 221 |
+
str: The decoded string.
|
| 222 |
+
"""
|
| 223 |
+
# If there are other args, we should call super().decode because there are a lot of code
|
| 224 |
+
# to handle those args. supper().encode finally will call convert_tokens_to_string and _convert_id_to_token.
|
| 225 |
+
if len(kwargs) > 0:
|
| 226 |
+
return super().decode(token_ids, **kwargs)
|
| 227 |
+
|
| 228 |
+
if type(token_ids) is int:
|
| 229 |
+
token_ids = [token_ids]
|
| 230 |
+
|
| 231 |
+
return self.model.decode(cast(List[int], token_ids))
|
| 232 |
+
|
| 233 |
+
@staticmethod
|
| 234 |
+
def _split_whitespaces_or_nonwhitespaces(
|
| 235 |
+
s: str, max_consecutive_slice_len: int) -> Iterator[str]:
|
| 236 |
+
"""
|
| 237 |
+
Splits the string `s` so that each substring contains no more than `max_consecutive_slice_len`
|
| 238 |
+
consecutive whitespaces or consecutive non-whitespaces.
|
| 239 |
+
"""
|
| 240 |
+
current_slice_len = 0
|
| 241 |
+
current_slice_is_space = s[0].isspace() if len(s) > 0 else False
|
| 242 |
+
slice_start = 0
|
| 243 |
+
|
| 244 |
+
for i in range(len(s)):
|
| 245 |
+
is_now_space = s[i].isspace()
|
| 246 |
+
|
| 247 |
+
if current_slice_is_space ^ is_now_space:
|
| 248 |
+
current_slice_len = 1
|
| 249 |
+
current_slice_is_space = is_now_space
|
| 250 |
+
else:
|
| 251 |
+
current_slice_len += 1
|
| 252 |
+
if current_slice_len > max_consecutive_slice_len:
|
| 253 |
+
yield s[slice_start:i]
|
| 254 |
+
slice_start = i
|
| 255 |
+
current_slice_len = 1
|
| 256 |
+
yield s[slice_start:]
|
| 257 |
+
|
| 258 |
+
def _encode_chat_segments(self, segments) -> List[int]:
|
| 259 |
+
token_ids: List[int] = []
|
| 260 |
+
for segment in segments:
|
| 261 |
+
token_ids.extend(
|
| 262 |
+
self._encode_text_piece(
|
| 263 |
+
segment.text,
|
| 264 |
+
allow_special_tokens=segment.allow_special,
|
| 265 |
+
))
|
| 266 |
+
return token_ids
|
| 267 |
+
|
| 268 |
+
@staticmethod
|
| 269 |
+
def _truncate(ids: List[int],
|
| 270 |
+
truncation: bool = False,
|
| 271 |
+
max_length: Optional[int] = None) -> List[int]:
|
| 272 |
+
if truncation and max_length is not None:
|
| 273 |
+
return ids[:max_length]
|
| 274 |
+
return ids
|
| 275 |
+
|
| 276 |
+
def _format_chat_token_output(self,
|
| 277 |
+
encoded_inputs: List[List[int]],
|
| 278 |
+
*,
|
| 279 |
+
is_batched: bool,
|
| 280 |
+
padding=False,
|
| 281 |
+
truncation: bool = False,
|
| 282 |
+
max_length: Optional[int] = None,
|
| 283 |
+
return_tensors=None,
|
| 284 |
+
return_dict: bool = False):
|
| 285 |
+
encoded_inputs = [
|
| 286 |
+
self._truncate(ids, truncation=truncation, max_length=max_length)
|
| 287 |
+
for ids in encoded_inputs
|
| 288 |
+
]
|
| 289 |
+
|
| 290 |
+
needs_batch_encoding = (
|
| 291 |
+
is_batched or padding or return_tensors is not None or return_dict)
|
| 292 |
+
if not needs_batch_encoding:
|
| 293 |
+
return encoded_inputs[0]
|
| 294 |
+
|
| 295 |
+
features = [{
|
| 296 |
+
"input_ids": ids,
|
| 297 |
+
"attention_mask": [1] * len(ids)
|
| 298 |
+
} for ids in encoded_inputs]
|
| 299 |
+
batch = self.pad(features,
|
| 300 |
+
padding=padding,
|
| 301 |
+
max_length=max_length if padding else None,
|
| 302 |
+
return_attention_mask=True,
|
| 303 |
+
return_tensors=return_tensors)
|
| 304 |
+
|
| 305 |
+
if return_dict:
|
| 306 |
+
return batch
|
| 307 |
+
if is_batched:
|
| 308 |
+
return batch["input_ids"]
|
| 309 |
+
return batch["input_ids"][0] if return_tensors is None else batch[
|
| 310 |
+
"input_ids"]
|
| 311 |
+
|
| 312 |
+
""" ----- Below are the abstract methods required by PreTrainedTokenizer ----- """
|
| 313 |
+
|
| 314 |
+
@property
|
| 315 |
+
def vocab_size(self) -> int:
|
| 316 |
+
return self.n_words
|
| 317 |
+
|
| 318 |
+
def get_vocab(self) -> Dict[str, int]:
|
| 319 |
+
return self.encoder
|
| 320 |
+
|
| 321 |
+
def _tokenize(self, text: str, **kwargs) -> List[str]:
|
| 322 |
+
return [self.decoder[t] for t in self.encode(text)]
|
| 323 |
+
|
| 324 |
+
def _convert_token_to_id(self, token: str) -> int:
|
| 325 |
+
return self.encoder.get(token, self.unk_id)
|
| 326 |
+
|
| 327 |
+
def _convert_id_to_token(self, index: int) -> str:
|
| 328 |
+
return self.decoder.get(index)
|
| 329 |
+
|
| 330 |
+
@staticmethod
|
| 331 |
+
def clean_up_tokenization(out_string: str) -> str:
|
| 332 |
+
return out_string
|
| 333 |
+
|
| 334 |
+
def convert_tokens_to_string(self, tokens: List[str]) -> str:
|
| 335 |
+
text = ''.join(tokens)
|
| 336 |
+
text = bytearray([self.byte_decoder[c]
|
| 337 |
+
for c in text]).decode('utf-8', 'replace')
|
| 338 |
+
return text
|
| 339 |
+
|
| 340 |
+
def save_vocabulary(self,
|
| 341 |
+
save_directory: str,
|
| 342 |
+
filename_prefix: Optional[str] = None) -> Tuple[str]:
|
| 343 |
+
if not os.path.isdir(save_directory):
|
| 344 |
+
raise ValueError(
|
| 345 |
+
f"vocabulary path ({save_directory}) should be a directory")
|
| 346 |
+
out_vocab_file = os.path.join(
|
| 347 |
+
save_directory,
|
| 348 |
+
(filename_prefix + "-" if filename_prefix else "") +
|
| 349 |
+
VOCAB_FILES_NAMES["vocab_file"])
|
| 350 |
+
|
| 351 |
+
if os.path.abspath(self.vocab_file) != os.path.abspath(
|
| 352 |
+
out_vocab_file) and os.path.isfile(self.vocab_file):
|
| 353 |
+
copyfile(self.vocab_file, out_vocab_file)
|
| 354 |
+
|
| 355 |
+
return (out_vocab_file, )
|
| 356 |
+
|
| 357 |
+
def apply_chat_template(self,
|
| 358 |
+
conversation,
|
| 359 |
+
tools: Optional[list[dict]] = None,
|
| 360 |
+
tokenize: bool = False,
|
| 361 |
+
add_generation_prompt: bool = True,
|
| 362 |
+
thinking: bool = True,
|
| 363 |
+
padding=False,
|
| 364 |
+
truncation: bool = False,
|
| 365 |
+
max_length: Optional[int] = None,
|
| 366 |
+
return_tensors=None,
|
| 367 |
+
return_dict: bool = False,
|
| 368 |
+
**kwargs):
|
| 369 |
+
# Tokenizer-level rendering reorders tool result messages to match
|
| 370 |
+
# assistant tool_calls, normalizes per-call arguments and response
|
| 371 |
+
# schema, then encodes the resulting XTML structure segment-by-segment.
|
| 372 |
+
is_batched = is_batched_conversation(conversation)
|
| 373 |
+
conversations = conversation if is_batched else [conversation]
|
| 374 |
+
image_prompts = kwargs.pop("image_prompts", None)
|
| 375 |
+
if is_batched and image_prompts is not None:
|
| 376 |
+
raise ValueError("image_prompts is only supported for one chat.")
|
| 377 |
+
|
| 378 |
+
# by default set thinking effort to max
|
| 379 |
+
kwargs.setdefault("thinking_effort", "max")
|
| 380 |
+
|
| 381 |
+
segment_batches = [
|
| 382 |
+
build_chat_segments(
|
| 383 |
+
messages,
|
| 384 |
+
tools=tools,
|
| 385 |
+
add_generation_prompt=add_generation_prompt,
|
| 386 |
+
thinking=thinking,
|
| 387 |
+
image_prompts=image_prompts,
|
| 388 |
+
**kwargs,
|
| 389 |
+
) for messages in conversations
|
| 390 |
+
]
|
| 391 |
+
|
| 392 |
+
if not tokenize:
|
| 393 |
+
rendered = ["".join(segment.text for segment in segments)
|
| 394 |
+
for segments in segment_batches]
|
| 395 |
+
return rendered if is_batched else rendered[0]
|
| 396 |
+
|
| 397 |
+
encoded_inputs = [
|
| 398 |
+
self._encode_chat_segments(segments) for segments in segment_batches
|
| 399 |
+
]
|
| 400 |
+
return self._format_chat_token_output(
|
| 401 |
+
encoded_inputs,
|
| 402 |
+
is_batched=is_batched,
|
| 403 |
+
padding=padding,
|
| 404 |
+
truncation=truncation,
|
| 405 |
+
max_length=max_length,
|
| 406 |
+
return_tensors=return_tensors,
|
| 407 |
+
return_dict=return_dict,
|
| 408 |
+
)
|
tokenizer_config.json
ADDED
|
@@ -0,0 +1,157 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"added_tokens_decoder": {
|
| 3 |
+
"163584": {
|
| 4 |
+
"content": "[BOS]",
|
| 5 |
+
"lstrip": false,
|
| 6 |
+
"normalized": false,
|
| 7 |
+
"rstrip": false,
|
| 8 |
+
"single_word": false,
|
| 9 |
+
"special": true
|
| 10 |
+
},
|
| 11 |
+
"163585": {
|
| 12 |
+
"content": "[EOS]",
|
| 13 |
+
"lstrip": false,
|
| 14 |
+
"normalized": false,
|
| 15 |
+
"rstrip": false,
|
| 16 |
+
"single_word": false,
|
| 17 |
+
"special": true
|
| 18 |
+
},
|
| 19 |
+
"163586": {
|
| 20 |
+
"content": "<|end_of_msg|>",
|
| 21 |
+
"lstrip": false,
|
| 22 |
+
"normalized": false,
|
| 23 |
+
"rstrip": false,
|
| 24 |
+
"single_word": false,
|
| 25 |
+
"special": true
|
| 26 |
+
},
|
| 27 |
+
"163587": {
|
| 28 |
+
"content": "<|open|>",
|
| 29 |
+
"lstrip": false,
|
| 30 |
+
"normalized": false,
|
| 31 |
+
"rstrip": false,
|
| 32 |
+
"single_word": false,
|
| 33 |
+
"special": false
|
| 34 |
+
},
|
| 35 |
+
"163588": {
|
| 36 |
+
"content": "<|close|>",
|
| 37 |
+
"lstrip": false,
|
| 38 |
+
"normalized": false,
|
| 39 |
+
"rstrip": false,
|
| 40 |
+
"single_word": false,
|
| 41 |
+
"special": false
|
| 42 |
+
},
|
| 43 |
+
"163589": {
|
| 44 |
+
"content": "<|sep|>",
|
| 45 |
+
"lstrip": false,
|
| 46 |
+
"normalized": false,
|
| 47 |
+
"rstrip": false,
|
| 48 |
+
"single_word": false,
|
| 49 |
+
"special": false
|
| 50 |
+
},
|
| 51 |
+
"163590": {
|
| 52 |
+
"content": "[start_header_id]",
|
| 53 |
+
"lstrip": false,
|
| 54 |
+
"normalized": false,
|
| 55 |
+
"rstrip": false,
|
| 56 |
+
"single_word": false,
|
| 57 |
+
"special": true
|
| 58 |
+
},
|
| 59 |
+
"163591": {
|
| 60 |
+
"content": "[end_header_id]",
|
| 61 |
+
"lstrip": false,
|
| 62 |
+
"normalized": false,
|
| 63 |
+
"rstrip": false,
|
| 64 |
+
"single_word": false,
|
| 65 |
+
"special": true
|
| 66 |
+
},
|
| 67 |
+
"163593": {
|
| 68 |
+
"content": "[EOT]",
|
| 69 |
+
"lstrip": false,
|
| 70 |
+
"normalized": false,
|
| 71 |
+
"rstrip": false,
|
| 72 |
+
"single_word": false,
|
| 73 |
+
"special": true
|
| 74 |
+
},
|
| 75 |
+
"163602": {
|
| 76 |
+
"content": "<|media_begin|>",
|
| 77 |
+
"lstrip": false,
|
| 78 |
+
"normalized": false,
|
| 79 |
+
"rstrip": false,
|
| 80 |
+
"single_word": false,
|
| 81 |
+
"special": true
|
| 82 |
+
},
|
| 83 |
+
"163603": {
|
| 84 |
+
"content": "<|media_content|>",
|
| 85 |
+
"lstrip": false,
|
| 86 |
+
"normalized": false,
|
| 87 |
+
"rstrip": false,
|
| 88 |
+
"single_word": false,
|
| 89 |
+
"special": true
|
| 90 |
+
},
|
| 91 |
+
"163604": {
|
| 92 |
+
"content": "<|media_end|>",
|
| 93 |
+
"lstrip": false,
|
| 94 |
+
"normalized": false,
|
| 95 |
+
"rstrip": false,
|
| 96 |
+
"single_word": false,
|
| 97 |
+
"special": true
|
| 98 |
+
},
|
| 99 |
+
"163605": {
|
| 100 |
+
"content": "<|media_pad|>",
|
| 101 |
+
"lstrip": false,
|
| 102 |
+
"normalized": false,
|
| 103 |
+
"rstrip": false,
|
| 104 |
+
"single_word": false,
|
| 105 |
+
"special": true
|
| 106 |
+
},
|
| 107 |
+
"163649": {
|
| 108 |
+
"content": "<osagent_mode>",
|
| 109 |
+
"lstrip": false,
|
| 110 |
+
"normalized": false,
|
| 111 |
+
"rstrip": false,
|
| 112 |
+
"single_word": false,
|
| 113 |
+
"special": true
|
| 114 |
+
},
|
| 115 |
+
"163838": {
|
| 116 |
+
"content": "[UNK]",
|
| 117 |
+
"lstrip": false,
|
| 118 |
+
"normalized": false,
|
| 119 |
+
"rstrip": false,
|
| 120 |
+
"single_word": false,
|
| 121 |
+
"special": true
|
| 122 |
+
},
|
| 123 |
+
"163839": {
|
| 124 |
+
"content": "[PAD]",
|
| 125 |
+
"lstrip": false,
|
| 126 |
+
"normalized": false,
|
| 127 |
+
"rstrip": false,
|
| 128 |
+
"single_word": false,
|
| 129 |
+
"special": true
|
| 130 |
+
}
|
| 131 |
+
},
|
| 132 |
+
"additional_special_tokens": [
|
| 133 |
+
"<|end_of_msg|>",
|
| 134 |
+
"[start_header_id]",
|
| 135 |
+
"[end_header_id]",
|
| 136 |
+
"[EOT]",
|
| 137 |
+
"<|media_begin|>",
|
| 138 |
+
"<|media_content|>",
|
| 139 |
+
"<|media_end|>",
|
| 140 |
+
"<|media_pad|>",
|
| 141 |
+
"<osagent_mode>"
|
| 142 |
+
],
|
| 143 |
+
"bos_token": "[BOS]",
|
| 144 |
+
"clean_up_tokenization_spaces": false,
|
| 145 |
+
"eos_token": "[EOS]",
|
| 146 |
+
"extra_special_tokens": {},
|
| 147 |
+
"model_max_length": 1000000000000000019884624838656,
|
| 148 |
+
"pad_token": "[PAD]",
|
| 149 |
+
"tokenizer_class": "TikTokenTokenizer",
|
| 150 |
+
"unk_token": "[UNK]",
|
| 151 |
+
"auto_map": {
|
| 152 |
+
"AutoTokenizer": [
|
| 153 |
+
"tokenization_kimi.TikTokenTokenizer",
|
| 154 |
+
null
|
| 155 |
+
]
|
| 156 |
+
}
|
| 157 |
+
}
|