Image-Text-to-Text
MLX
Safetensors
inkling_mm_model
inkling
Mixture of Experts
multimodal
text-generation
apple-silicon
conversational
Instructions to use mlx-community/Inkling-Small-mlx-3bit with libraries, inference providers, notebooks, and local apps. Follow these links to get started.
- Libraries
- MLX
How to use mlx-community/Inkling-Small-mlx-3bit with MLX:
# Make sure mlx-vlm is installed # pip install --upgrade mlx-vlm from mlx_vlm import load, generate from mlx_vlm.prompt_utils import apply_chat_template from mlx_vlm.utils import load_config # Load the model model, processor = load("mlx-community/Inkling-Small-mlx-3bit") config = load_config("mlx-community/Inkling-Small-mlx-3bit") # Prepare input image = ["http://images.cocodataset.org/val2017/000000039769.jpg"] prompt = "Describe this image." # Apply chat template formatted_prompt = apply_chat_template( processor, config, prompt, num_images=1 ) # Generate output output = generate(model, processor, formatted_prompt, image) print(output) - Notebooks
- Google Colab
- Kaggle
- Local Apps Settings
- LM Studio
- Pi
How to use mlx-community/Inkling-Small-mlx-3bit with Pi:
Start the MLX server
# Install MLX LM: uv tool install mlx-lm # Start a local OpenAI-compatible server: mlx_lm.server --model "mlx-community/Inkling-Small-mlx-3bit"
Configure the model in Pi
# Install Pi: npm install -g @mariozechner/pi-coding-agent # Add to ~/.pi/agent/models.json: { "providers": { "mlx-lm": { "baseUrl": "http://localhost:8080/v1", "api": "openai-completions", "apiKey": "none", "models": [ { "id": "mlx-community/Inkling-Small-mlx-3bit" } ] } } }Run Pi
# Start Pi in your project directory: pi
- Hermes Agent new
How to use mlx-community/Inkling-Small-mlx-3bit with Hermes Agent:
Start the MLX server
# Install MLX LM: uv tool install mlx-lm # Start a local OpenAI-compatible server: mlx_lm.server --model "mlx-community/Inkling-Small-mlx-3bit"
Configure Hermes
# Install Hermes: curl -fsSL https://hermes-agent.nousresearch.com/install.sh | bash hermes setup # Point Hermes at the local server: hermes config set model.provider custom hermes config set model.base_url http://127.0.0.1:8080/v1 hermes config set model.default mlx-community/Inkling-Small-mlx-3bit
Run Hermes
hermes
- OpenClaw new
How to use mlx-community/Inkling-Small-mlx-3bit with OpenClaw:
Start the MLX server
# Install MLX LM: uv tool install mlx-lm # Start a local OpenAI-compatible server: mlx_lm.server --model "mlx-community/Inkling-Small-mlx-3bit"
Configure OpenClaw
# Install OpenClaw: npm install -g openclaw@latest # Register the local server and set it as the default model: openclaw onboard --non-interactive --mode local \ --auth-choice custom-api-key \ --custom-base-url http://127.0.0.1:8080/v1 \ --custom-model-id "mlx-community/Inkling-Small-mlx-3bit" \ --custom-provider-id mlx-lm \ --custom-compatibility openai \ --custom-text-input \ --accept-risk \ --skip-health
Run OpenClaw
openclaw agent --local --agent main --message "Hello from Hugging Face"
Add files using upload-large-folder tool
Browse filesThis view is limited to 50 files because it contains too many changes. See raw diff
- .gitattributes +1 -0
- README.md +109 -1
- THIRD_PARTY_NOTICES.md +7 -0
- chat_template.jinja +129 -0
- config.json +119 -0
- inkling_mlx/LICENSE +13 -0
- inkling_mlx/__init__.py +14 -0
- inkling_mlx/attention.py +131 -0
- inkling_mlx/audio.py +34 -0
- inkling_mlx/cache.py +66 -0
- inkling_mlx/common.py +63 -0
- inkling_mlx/config.py +207 -0
- inkling_mlx/convert.py +254 -0
- inkling_mlx/convert_cli.py +47 -0
- inkling_mlx/generate.py +90 -0
- inkling_mlx/layers.py +47 -0
- inkling_mlx/load.py +69 -0
- inkling_mlx/model.py +79 -0
- inkling_mlx/moe.py +117 -0
- inkling_mlx/processing.py +188 -0
- inkling_mlx/text.py +50 -0
- inkling_mlx/vision.py +126 -0
- model-00001-of-00023.safetensors +3 -0
- model-00002-of-00023.safetensors +3 -0
- model-00003-of-00023.safetensors +3 -0
- model-00004-of-00023.safetensors +3 -0
- model-00005-of-00023.safetensors +3 -0
- model-00006-of-00023.safetensors +3 -0
- model-00007-of-00023.safetensors +3 -0
- model-00008-of-00023.safetensors +3 -0
- model-00009-of-00023.safetensors +3 -0
- model-00010-of-00023.safetensors +3 -0
- model-00011-of-00023.safetensors +3 -0
- model-00012-of-00023.safetensors +3 -0
- model-00013-of-00023.safetensors +3 -0
- model-00014-of-00023.safetensors +3 -0
- model-00015-of-00023.safetensors +3 -0
- model-00016-of-00023.safetensors +3 -0
- model-00017-of-00023.safetensors +3 -0
- model-00018-of-00023.safetensors +3 -0
- model-00019-of-00023.safetensors +3 -0
- model-00020-of-00023.safetensors +3 -0
- model-00021-of-00023.safetensors +3 -0
- model-00022-of-00023.safetensors +3 -0
- model-00023-of-00023.safetensors +3 -0
- model.safetensors.index.json +0 -0
- processor_config.json +46 -0
- special_tokens_map.json +22 -0
- tiktoken/tokenizer.model +3 -0
- tokenizer.json +3 -0
.gitattributes
CHANGED
|
@@ -33,3 +33,4 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
|
|
| 33 |
*.zip filter=lfs diff=lfs merge=lfs -text
|
| 34 |
*.zst filter=lfs diff=lfs merge=lfs -text
|
| 35 |
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
|
|
|
|
|
| 33 |
*.zip filter=lfs diff=lfs merge=lfs -text
|
| 34 |
*.zst filter=lfs diff=lfs merge=lfs -text
|
| 35 |
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
| 36 |
+
tokenizer.json filter=lfs diff=lfs merge=lfs -text
|
README.md
CHANGED
|
@@ -1,3 +1,111 @@
|
|
| 1 |
---
|
| 2 |
-
license:
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3 |
---
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
---
|
| 2 |
+
license: apache-2.0
|
| 3 |
+
library_name: mlx
|
| 4 |
+
tags: [mlx, inkling, moe, multimodal, text-generation, image-text-to-text, apple-silicon]
|
| 5 |
+
base_model: thinkingmachines/Inkling-Small
|
| 6 |
+
pipeline_tag: image-text-to-text
|
| 7 |
---
|
| 8 |
+
|
| 9 |
+
# Inkling-Small-mlx-3bit
|
| 10 |
+
|
| 11 |
+
An **MLX 3bit** build of [`thinkingmachines/Inkling-Small`](https://huggingface.co/thinkingmachines/Inkling-Small)
|
| 12 |
+
— 264 B total parameters, **~12 B active**; 42 layers, 256 routed experts (top-6) + 2 shared;
|
| 13 |
+
natively multimodal (image + audio in, text out).
|
| 14 |
+
|
| 15 |
+
Sized to run **fully resident on a 128 GB Mac** — no expert-offload, no expert pruning, no
|
| 16 |
+
layer streaming. The whole model sits in unified memory and decodes at conversational speed.
|
| 17 |
+
|
| 18 |
+
> Why this model is the interesting one for Apple Silicon: speed follows the **active**
|
| 19 |
+
> parameter count, not how hard you compress. Inkling-Small activates ~12 B params per token,
|
| 20 |
+
> so this tier reads roughly 9.8 GB per token. The 975 B Inkling, by contrast, needs SSD
|
| 21 |
+
> expert-offload even at 2-bit and manages ~0.2–0.4 tok/s.
|
| 22 |
+
|
| 23 |
+
## Tiers
|
| 24 |
+
|
| 25 |
+
Pick the tier that fits your machine. Sizes are on-disk and **measured from the built
|
| 26 |
+
artifacts**; peak/speed columns are filled in only where a benchmark has actually run.
|
| 27 |
+
|
| 28 |
+
| tier | target Mac | recipe | on-disk | peak | load | prefill tok/s |
|
| 29 |
+
|---|---|---|---|---|---|---|
|
| 30 |
+
| [4bit](https://huggingface.co/mlx-community/Inkling-Small-mlx-4bit) | 192 GB | experts 4-bit, non-expert 8-bit | 153.5 GB | — | — | — |
|
| 31 |
+
| **3bit** ← **this repo** | 128 GB | experts 3-bit, non-expert 8-bit | 120.9 GB | — | — | — |
|
| 32 |
+
| [2bit](https://huggingface.co/mlx-community/Inkling-Small-mlx-2bit) | 96 GB | experts 2-bit, non-expert 8-bit | 88.4 GB | — | — | — |
|
| 33 |
+
|
| 34 |
+
> **Speeds are not yet measured.** On-disk sizes above are real (taken from the built artifacts), but load/peak/tok-s columns stay empty until `serving/bench_mlx.py` has run on the target hardware — we would rather ship a blank column than a guess. Run it yourself with the command below and please open a discussion with your numbers.
|
| 35 |
+
|
| 36 |
+
## Why MLX on Apple Silicon
|
| 37 |
+
|
| 38 |
+
MLX is Apple's array framework for Apple Silicon. For a big sparse MoE like this one, the
|
| 39 |
+
practical differences from a CPU/GPU-split runtime are:
|
| 40 |
+
|
| 41 |
+
| | what it means here |
|
| 42 |
+
|---|---|
|
| 43 |
+
| **Unified memory** | The GPU reads the same DRAM as the CPU, so a 120 GB build needs 120 GB of *system* memory — not VRAM plus a host copy. There is no PCIe transfer per layer, which is what makes a >100 GB model practical on a desktop at all. |
|
| 44 |
+
| **Lazy, mmap'd loading** | `mx.load` memory-maps safetensors, so weights page in on demand instead of being read and copied up front. |
|
| 45 |
+
| **Native quantized matmul** | Quantized weights are multiplied in their packed form (`quantized_matmul`, and `gather_qmm` for MoE expert gathers) rather than dequantized to fp16 first — so low-bit tiers save bandwidth at *runtime*, not just on disk. |
|
| 46 |
+
| **Per-module precision** | `nn.quantize(..., class_predicate=...)` lets one checkpoint mix bit-widths per tensor class, which is exactly how these tiers keep attention and the router high-precision while the experts go low. |
|
| 47 |
+
| **Small dependency surface** | `pip install mlx mlx-lm` and a single Python model file. No compile step, no separate server binary. |
|
| 48 |
+
|
| 49 |
+
Honest limits: MLX's ecosystem is younger than llama.cpp's, it has no importance-matrix
|
| 50 |
+
("i-quant") style calibrated quantization yet, and it runs on Apple Silicon only. If you want a
|
| 51 |
+
GGUF build instead, one exists at
|
| 52 |
+
[`unsloth/Inkling-Small-GGUF`](https://huggingface.co/unsloth/Inkling-Small-GGUF).
|
| 53 |
+
|
| 54 |
+
## Memory notes
|
| 55 |
+
|
| 56 |
+
`peak` (once measured) is peak unified-memory use, not file size — leave headroom for the KV
|
| 57 |
+
cache and the OS. Two things matter on a machine near its limit:
|
| 58 |
+
|
| 59 |
+
* **Raise the Metal wired limit.** It defaults to ~75% of RAM, which is below this tier's
|
| 60 |
+
footprint on a 128 GB machine:
|
| 61 |
+
```bash
|
| 62 |
+
sudo sysctl iogpu.wired_limit_mb=180000 # ~180 GB on a 192 GB Mac; scale to your RAM
|
| 63 |
+
```
|
| 64 |
+
* **macOS will kill a process that stays too large for too long** (jetsam), even while memory
|
| 65 |
+
pressure looks fine. If long generations die silently, move down a tier.
|
| 66 |
+
|
| 67 |
+
## Quantization recipe
|
| 68 |
+
|
| 69 |
+
`experts_only`: the routed experts (and the vision/audio matmuls) carry the tier's bit-width,
|
| 70 |
+
while **attention, token/output embeddings, RMSNorms, the router gate, the per-layer
|
| 71 |
+
short-convolutions and the relative-position bias stay high-precision (8-bit)**.
|
| 72 |
+
|
| 73 |
+
That asymmetry is the point, and it is nearly free: non-expert weights are only **5.9 B of the
|
| 74 |
+
264 B params (~6 GB at 8-bit)**, yet they are read **in full on every token**, while just 6 of
|
| 75 |
+
256 experts are. Bits spent there cost ~2% of the file and protect the paths every token
|
| 76 |
+
depends on. Expert precision degrades *gracefully*; attention and router precision degrade
|
| 77 |
+
*globally*.
|
| 78 |
+
|
| 79 |
+
## Usage
|
| 80 |
+
|
| 81 |
+
```python
|
| 82 |
+
# pip install mlx mlx-lm transformers (+ scipy for audio, pillow for images)
|
| 83 |
+
# this repo bundles the inkling_mlx/ loader, so there is nothing else to install
|
| 84 |
+
from inkling_mlx.load import load
|
| 85 |
+
from inkling_mlx.generate import greedy_generate
|
| 86 |
+
from transformers import AutoTokenizer
|
| 87 |
+
|
| 88 |
+
path = "./Inkling-Small-mlx-3bit"
|
| 89 |
+
model, config = load(path)
|
| 90 |
+
tok = AutoTokenizer.from_pretrained(path, trust_remote_code=True)
|
| 91 |
+
ids = tok("The capital of France is")["input_ids"]
|
| 92 |
+
print(tok.decode(greedy_generate(model, config, ids, max_new_tokens=64)))
|
| 93 |
+
```
|
| 94 |
+
|
| 95 |
+
Images are cut into 40 px patches (one soft-token each) and audio is d-mel encoded at 20
|
| 96 |
+
tokens/s; both go through `InklingProcessor`. See the source repo for a multimodal runner.
|
| 97 |
+
|
| 98 |
+
### Benchmark it yourself
|
| 99 |
+
|
| 100 |
+
```bash
|
| 101 |
+
python serving/bench_mlx.py --model ./Inkling-Small-mlx-3bit --tier 3bit
|
| 102 |
+
```
|
| 103 |
+
|
| 104 |
+
Reports load time, prefill tok/s, decode tok/s at several context lengths, and peak memory.
|
| 105 |
+
|
| 106 |
+
## Attribution
|
| 107 |
+
|
| 108 |
+
- Base model: [`thinkingmachines/Inkling-Small`](https://huggingface.co/thinkingmachines/Inkling-Small), Apache-2.0.
|
| 109 |
+
- The bundled `inkling_mlx/` loader is vendored from
|
| 110 |
+
[PipeNetwork/inkling-mlx](https://github.com/PipeNetwork/inkling-mlx), Apache-2.0.
|
| 111 |
+
- Quantized with `mlx`; see the recipe above for exactly which tensors were touched.
|
THIRD_PARTY_NOTICES.md
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Third-party notices
|
| 2 |
+
|
| 3 |
+
This project vendors the `inkling_mlx/` model package from
|
| 4 |
+
**PipeNetwork/inkling-mlx** (https://github.com/PipeNetwork/inkling-mlx), licensed under **Apache-2.0**
|
| 5 |
+
(Copyright 2026 PipeNetwork). Vendored commit: `b6dc0ac9219b33e4e667bf08cafcdc2651fd9884`.
|
| 6 |
+
The package's LICENSE is preserved at `inkling_mlx/LICENSE`. Our conversion
|
| 7 |
+
tooling (convert/, models/, serving/, publish/) is separate.
|
chat_template.jinja
ADDED
|
@@ -0,0 +1,129 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{%- set effort_map = {"none": 0.0, "minimal": 0.1, "low": 0.2, "medium": 0.7, "high": 0.9, "max": 0.99} -%}
|
| 2 |
+
{%- set role_token = {"user": "<|message_user|>", "assistant": "<|message_model|>", "system": "<|message_system|>", "tool": "<|message_tool|>"} -%}
|
| 3 |
+
|
| 4 |
+
{%- macro emit_thinking_effort() -%}
|
| 5 |
+
{%- set eff = reasoning_effort if reasoning_effort is defined and reasoning_effort is not none else 0.9 -%}
|
| 6 |
+
{%- if eff is string -%}
|
| 7 |
+
{%- set key = eff | trim -%}
|
| 8 |
+
{%- if key not in effort_map -%}
|
| 9 |
+
{{- raise_exception("Unknown reasoning_effort: " ~ eff) -}}
|
| 10 |
+
{%- endif -%}
|
| 11 |
+
{%- set num = effort_map[key] -%}
|
| 12 |
+
{%- else -%}
|
| 13 |
+
{%- set num = eff | float -%}
|
| 14 |
+
{%- endif -%}
|
| 15 |
+
{%- if num < 0.0 or num > 0.99 -%}
|
| 16 |
+
{{- raise_exception("reasoning_effort must be in [0.0, 0.99]") -}}
|
| 17 |
+
{%- endif -%}
|
| 18 |
+
{{- "<|message_system|><|content_text|>Thinking effort level: " -}}
|
| 19 |
+
{%- if num == 0.0 -%}0{%- else -%}{{ num }}{%- endif -%}
|
| 20 |
+
{{- "<|end_message|>" -}}
|
| 21 |
+
{%- endmacro -%}
|
| 22 |
+
|
| 23 |
+
{%- if tools -%}
|
| 24 |
+
{%- set tool_state = namespace(specs=[]) -%}
|
| 25 |
+
{%- for tool in tools -%}
|
| 26 |
+
{%- set fn = tool.function if tool.function is defined else tool -%}
|
| 27 |
+
{%- set spec = {
|
| 28 |
+
"description": (fn.description if fn.description is defined and fn.description else ""),
|
| 29 |
+
"name": fn.name,
|
| 30 |
+
"parameters": (fn.parameters if fn.parameters is defined and fn.parameters else {}),
|
| 31 |
+
"type": (tool.type if tool.type is defined and tool.type else "function"),
|
| 32 |
+
} -%}
|
| 33 |
+
{%- set tool_state.specs = tool_state.specs + [spec] -%}
|
| 34 |
+
{%- endfor -%}
|
| 35 |
+
{{- "<|message_system|>tool_declare<|content_xml|>" -}}
|
| 36 |
+
{{- tool_state.specs | tojson(sort_keys=true, separators=(",", ":")) -}}
|
| 37 |
+
{{- "<|end_message|>" -}}
|
| 38 |
+
{%- endif -%}
|
| 39 |
+
|
| 40 |
+
{%- set state = namespace(effort_emitted=false) -%}
|
| 41 |
+
{%- for message in messages -%}
|
| 42 |
+
{%- if message.role not in role_token -%}
|
| 43 |
+
{{- raise_exception("Unknown message role: " ~ message.role) -}}
|
| 44 |
+
{%- endif -%}
|
| 45 |
+
{%- if not state.effort_emitted and message.role != "system" -%}
|
| 46 |
+
{{- emit_thinking_effort() -}}
|
| 47 |
+
{%- set state.effort_emitted = true -%}
|
| 48 |
+
{%- endif -%}
|
| 49 |
+
|
| 50 |
+
{%- set rtok = role_token[message.role] -%}
|
| 51 |
+
|
| 52 |
+
{%- if message.role == "tool" -%}
|
| 53 |
+
{%- set tool_name_state = namespace(name="") -%}
|
| 54 |
+
{%- if message.name is defined and message.name -%}
|
| 55 |
+
{%- set tool_name_state.name = message.name -%}
|
| 56 |
+
{%- elif message.tool_call_id is defined and message.tool_call_id -%}
|
| 57 |
+
{%- for prev in messages -%}
|
| 58 |
+
{%- if prev.role == "assistant" and prev.tool_calls -%}
|
| 59 |
+
{%- for tc in prev.tool_calls -%}
|
| 60 |
+
{%- if tc.id is defined and tc.id == message.tool_call_id and tc.function.name is defined -%}
|
| 61 |
+
{%- set tool_name_state.name = tc.function.name -%}
|
| 62 |
+
{%- endif -%}
|
| 63 |
+
{%- endfor -%}
|
| 64 |
+
{%- endif -%}
|
| 65 |
+
{%- endfor -%}
|
| 66 |
+
{%- endif -%}
|
| 67 |
+
{{- rtok -}}
|
| 68 |
+
{%- if tool_name_state.name -%}{{- tool_name_state.name -}}{%- endif -%}
|
| 69 |
+
{{- "<|content_text|>" -}}
|
| 70 |
+
{%- if message.content is string -%}{{- message.content -}}{%- endif -%}
|
| 71 |
+
{{- "<|end_message|>" -}}
|
| 72 |
+
|
| 73 |
+
{%- else -%}
|
| 74 |
+
{%- if message.role == "assistant" and message.reasoning_content is defined and message.reasoning_content -%}
|
| 75 |
+
{{- "<|message_model|><|content_thinking|>" ~ message.reasoning_content ~ "<|end_message|>" -}}
|
| 76 |
+
{%- endif -%}
|
| 77 |
+
|
| 78 |
+
{%- if message.content is string -%}
|
| 79 |
+
{{- rtok ~ "<|content_text|>" ~ message.content ~ "<|end_message|>" -}}
|
| 80 |
+
{%- elif message.content -%}
|
| 81 |
+
{%- for part in message.content -%}
|
| 82 |
+
{%- if part is string -%}
|
| 83 |
+
{{- rtok ~ "<|content_text|>" ~ part ~ "<|end_message|>" -}}
|
| 84 |
+
{%- elif part.type is not defined or part.type in ("text", "input_text") -%}
|
| 85 |
+
{%- set text_part = (part.text if part.text is defined and part.text is string else "") -%}
|
| 86 |
+
{{- rtok ~ "<|content_text|>" ~ text_part ~ "<|end_message|>" -}}
|
| 87 |
+
{%- elif part.type in ("image", "input_image", "image_url") -%}
|
| 88 |
+
{{- rtok ~ "<|content_image|><|unused_200054|><|end_message|>" -}}
|
| 89 |
+
{%- elif part.type in ("audio", "input_audio", "audio_url") -%}
|
| 90 |
+
{{- rtok ~ "<|content_audio_input|><|unused_200053|><|audio_end|><|end_message|>" -}}
|
| 91 |
+
{%- else -%}
|
| 92 |
+
{{- raise_exception("Unsupported content part type: " ~ part.type) -}}
|
| 93 |
+
{%- endif -%}
|
| 94 |
+
{%- endfor -%}
|
| 95 |
+
{%- endif -%}
|
| 96 |
+
|
| 97 |
+
{%- if message.role == "assistant" and message.tool_calls -%}
|
| 98 |
+
{%- for tc in message.tool_calls -%}
|
| 99 |
+
{%- set fn = tc.function -%}
|
| 100 |
+
{%- if fn.name is not defined or fn.name is not string -%}
|
| 101 |
+
{{- raise_exception("tool call function name must be a string") -}}
|
| 102 |
+
{%- endif -%}
|
| 103 |
+
{%- set args = fn.arguments if fn.arguments is defined and fn.arguments else {} -%}
|
| 104 |
+
{%- if args is string -%}
|
| 105 |
+
{{- raise_exception("tool call arguments must be a parsed object, not a JSON string; canonicalize upstream") -}}
|
| 106 |
+
{%- endif -%}
|
| 107 |
+
{%- if args is not mapping -%}
|
| 108 |
+
{{- raise_exception("tool call arguments must be an object") -}}
|
| 109 |
+
{%- endif -%}
|
| 110 |
+
{{- "<|message_model|>" ~ fn.name ~ "<|content_invoke_tool_json|>" -}}
|
| 111 |
+
{{- '{"name":' ~ (fn.name | tojson(sort_keys=true, separators=(",", ":"))) ~ ',"args":' -}}
|
| 112 |
+
{{- (args | tojson(sort_keys=true, separators=(",", ":"))) -}}
|
| 113 |
+
{{- "}<|end_message|>" -}}
|
| 114 |
+
{%- endfor -%}
|
| 115 |
+
{%- endif -%}
|
| 116 |
+
|
| 117 |
+
{%- if message.role == "assistant" -%}
|
| 118 |
+
{{- "<|content_model_end_sampling|>" -}}
|
| 119 |
+
{%- endif -%}
|
| 120 |
+
{%- endif -%}
|
| 121 |
+
{%- endfor -%}
|
| 122 |
+
|
| 123 |
+
{%- if not state.effort_emitted -%}
|
| 124 |
+
{{- emit_thinking_effort() -}}
|
| 125 |
+
{%- endif -%}
|
| 126 |
+
|
| 127 |
+
{%- if add_generation_prompt -%}
|
| 128 |
+
{{- "<|message_model|>" -}}
|
| 129 |
+
{%- endif -%}
|
config.json
ADDED
|
@@ -0,0 +1,119 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"architectures": [
|
| 3 |
+
"InklingForConditionalGeneration"
|
| 4 |
+
],
|
| 5 |
+
"model_type": "inkling_mm_model",
|
| 6 |
+
"eos_token_id": 200006,
|
| 7 |
+
"text_config": {
|
| 8 |
+
"model_max_length": 1048576,
|
| 9 |
+
"torch_dtype": "bfloat16",
|
| 10 |
+
"hidden_size": 4096,
|
| 11 |
+
"num_hidden_layers": 42,
|
| 12 |
+
"vocab_size": 201024,
|
| 13 |
+
"num_attention_heads": 32,
|
| 14 |
+
"num_key_value_heads": 8,
|
| 15 |
+
"head_dim": 128,
|
| 16 |
+
"d_rel": 16,
|
| 17 |
+
"rel_extent": 1024,
|
| 18 |
+
"q_bias": false,
|
| 19 |
+
"o_bias": false,
|
| 20 |
+
"log_scaling_n_floor": 128000,
|
| 21 |
+
"log_scaling_alpha": 0.1,
|
| 22 |
+
"rms_norm_eps": 1e-06,
|
| 23 |
+
"use_embed_norm": true,
|
| 24 |
+
"local_layer_ids": [
|
| 25 |
+
0,
|
| 26 |
+
1,
|
| 27 |
+
2,
|
| 28 |
+
3,
|
| 29 |
+
4,
|
| 30 |
+
6,
|
| 31 |
+
7,
|
| 32 |
+
8,
|
| 33 |
+
9,
|
| 34 |
+
10,
|
| 35 |
+
12,
|
| 36 |
+
13,
|
| 37 |
+
14,
|
| 38 |
+
15,
|
| 39 |
+
16,
|
| 40 |
+
18,
|
| 41 |
+
19,
|
| 42 |
+
20,
|
| 43 |
+
21,
|
| 44 |
+
22,
|
| 45 |
+
24,
|
| 46 |
+
25,
|
| 47 |
+
26,
|
| 48 |
+
27,
|
| 49 |
+
28,
|
| 50 |
+
30,
|
| 51 |
+
31,
|
| 52 |
+
32,
|
| 53 |
+
33,
|
| 54 |
+
34,
|
| 55 |
+
36,
|
| 56 |
+
37,
|
| 57 |
+
38,
|
| 58 |
+
39,
|
| 59 |
+
40
|
| 60 |
+
],
|
| 61 |
+
"dense_mlp_idx": 2,
|
| 62 |
+
"use_sconv": true,
|
| 63 |
+
"sconv_kernel_size": 4,
|
| 64 |
+
"unpadded_vocab_size": 200058,
|
| 65 |
+
"logits_mup_width_multiplier": 16.0,
|
| 66 |
+
"final_logit_softcapping": null,
|
| 67 |
+
"swa_head_dim": 128,
|
| 68 |
+
"swa_num_attention_heads": 32,
|
| 69 |
+
"swa_num_key_value_heads": 8,
|
| 70 |
+
"sliding_window_size": 512,
|
| 71 |
+
"n_routed_experts": 256,
|
| 72 |
+
"num_experts_per_tok": 6,
|
| 73 |
+
"n_shared_experts": 2,
|
| 74 |
+
"shared_expert_sink": true,
|
| 75 |
+
"dense_intermediate_size": 16384,
|
| 76 |
+
"intermediate_size": 2048,
|
| 77 |
+
"route_scale": 8.0,
|
| 78 |
+
"use_gate_bias": true,
|
| 79 |
+
"gate_activation": "sigmoid",
|
| 80 |
+
"norm_after_topk": true,
|
| 81 |
+
"use_global_scale": true
|
| 82 |
+
},
|
| 83 |
+
"audio_config": {
|
| 84 |
+
"decoder_dmodel": 4096,
|
| 85 |
+
"n_mel_bins": 80,
|
| 86 |
+
"mel_vocab_size": 16,
|
| 87 |
+
"bias": false,
|
| 88 |
+
"dmel_min_value": -7.0,
|
| 89 |
+
"dmel_max_value": 2.0,
|
| 90 |
+
"use_audio_norm": true,
|
| 91 |
+
"audio_mode": "dmel"
|
| 92 |
+
},
|
| 93 |
+
"vision_config": {
|
| 94 |
+
"vision_encoder_type": "hmlp",
|
| 95 |
+
"decoder_dmodel": 4096,
|
| 96 |
+
"patch_size": 40,
|
| 97 |
+
"temporal_patch_size": 2,
|
| 98 |
+
"n_channels": 3,
|
| 99 |
+
"n_layers": 4,
|
| 100 |
+
"use_vision_norm": true
|
| 101 |
+
},
|
| 102 |
+
"mtp_config": {
|
| 103 |
+
"num_nextn_predict_layers": 8,
|
| 104 |
+
"chain_hidden_post_norm": false,
|
| 105 |
+
"local_layer_ids": [
|
| 106 |
+
0,
|
| 107 |
+
2,
|
| 108 |
+
4,
|
| 109 |
+
5,
|
| 110 |
+
6,
|
| 111 |
+
7
|
| 112 |
+
]
|
| 113 |
+
},
|
| 114 |
+
"quantization": {
|
| 115 |
+
"group_size": 64,
|
| 116 |
+
"bits": 3,
|
| 117 |
+
"recipe": "experts_only"
|
| 118 |
+
}
|
| 119 |
+
}
|
inkling_mlx/LICENSE
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
Copyright 2026 PipeNetwork
|
| 2 |
+
|
| 3 |
+
Licensed under the Apache License, Version 2.0 (the "License");
|
| 4 |
+
you may not use this file except in compliance with the License.
|
| 5 |
+
You may obtain a copy of the License at
|
| 6 |
+
|
| 7 |
+
http://www.apache.org/licenses/LICENSE-2.0
|
| 8 |
+
|
| 9 |
+
Unless required by applicable law or agreed to in writing, software
|
| 10 |
+
distributed under the License is distributed on an "AS IS" BASIS,
|
| 11 |
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
| 12 |
+
See the License for the specific language governing permissions and
|
| 13 |
+
limitations under the License.
|
inkling_mlx/__init__.py
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""MLX port of thinkingmachines/Inkling (975B MoE, natively multimodal)."""
|
| 2 |
+
|
| 3 |
+
from .config import AudioConfig, InklingConfig, TextConfig, VisionConfig
|
| 4 |
+
from .model import InklingForConditionalGeneration
|
| 5 |
+
from .text import TextModel
|
| 6 |
+
|
| 7 |
+
__all__ = [
|
| 8 |
+
"InklingConfig",
|
| 9 |
+
"TextConfig",
|
| 10 |
+
"VisionConfig",
|
| 11 |
+
"AudioConfig",
|
| 12 |
+
"InklingForConditionalGeneration",
|
| 13 |
+
"TextModel",
|
| 14 |
+
]
|
inkling_mlx/attention.py
ADDED
|
@@ -0,0 +1,131 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Inkling attention: hybrid local/global, per-head q/k RMSNorm, relative-position
|
| 2 |
+
logits bias, optional log-scaling, and short-convolution on k/v.
|
| 3 |
+
|
| 4 |
+
Mirrors ``InklingAttention`` + ``InklingRelativeLogits`` from transformers PR #47347.
|
| 5 |
+
This implementation is prefill-oriented (full-sequence, no KV cache); an incremental
|
| 6 |
+
cache (including the 4 per-layer conv states) can be layered on top later.
|
| 7 |
+
"""
|
| 8 |
+
|
| 9 |
+
from __future__ import annotations
|
| 10 |
+
|
| 11 |
+
import math
|
| 12 |
+
|
| 13 |
+
import mlx.core as mx
|
| 14 |
+
import mlx.nn as nn
|
| 15 |
+
|
| 16 |
+
from .common import RMSNorm, ShortConvolution
|
| 17 |
+
from .config import TextConfig
|
| 18 |
+
|
| 19 |
+
NEG_INF = -1e30
|
| 20 |
+
|
| 21 |
+
|
| 22 |
+
class RelativeLogits(nn.Module):
|
| 23 |
+
"""Hidden-state-conditioned relative position bias.
|
| 24 |
+
|
| 25 |
+
``proj`` is a bank of bias-vs-distance profiles ``[d_rel, rel_extent]``. Each
|
| 26 |
+
query's ``d_rel`` relative-state vector mixes them into one bias value per
|
| 27 |
+
backward distance; the bias is zero outside ``0 <= distance < rel_extent``.
|
| 28 |
+
"""
|
| 29 |
+
|
| 30 |
+
def __init__(self, d_rel: int, rel_extent: int):
|
| 31 |
+
super().__init__()
|
| 32 |
+
self.rel_extent = rel_extent
|
| 33 |
+
self.proj = mx.zeros((d_rel, rel_extent))
|
| 34 |
+
|
| 35 |
+
def __call__(self, relative_states, q_pos, kv_pos):
|
| 36 |
+
# relative_states: [B, Lq, heads, d_rel]
|
| 37 |
+
# rel_logits: [B, Lq, heads, rel_extent] -> [B, heads, Lq, rel_extent]
|
| 38 |
+
rel_logits = mx.swapaxes(relative_states @ self.proj, 1, 2)
|
| 39 |
+
B, H, Lq, _ = rel_logits.shape
|
| 40 |
+
distance = q_pos[:, None] - kv_pos[None, :] # [Lq, Lkv]
|
| 41 |
+
gather = mx.clip(distance, 0, self.rel_extent - 1) # [Lq, Lkv]
|
| 42 |
+
gather = mx.broadcast_to(gather[None, None], (B, H, Lq, gather.shape[-1]))
|
| 43 |
+
bias = mx.take_along_axis(rel_logits, gather, axis=-1) # [B, H, Lq, Lkv]
|
| 44 |
+
valid = (distance >= 0) & (distance < self.rel_extent) # [Lq, Lkv]
|
| 45 |
+
return mx.where(valid[None, None], bias, 0.0)
|
| 46 |
+
|
| 47 |
+
|
| 48 |
+
class Attention(nn.Module):
|
| 49 |
+
def __init__(self, config: TextConfig, layer_idx: int):
|
| 50 |
+
super().__init__()
|
| 51 |
+
self.config = config
|
| 52 |
+
self.layer_idx = layer_idx
|
| 53 |
+
self.is_sliding = config.layer_types[layer_idx] == "hybrid_sliding"
|
| 54 |
+
|
| 55 |
+
self.head_dim = config.swa_head_dim if self.is_sliding else config.head_dim
|
| 56 |
+
self.num_heads = config.swa_num_attention_heads if self.is_sliding else config.num_attention_heads
|
| 57 |
+
self.num_kv_heads = config.swa_num_key_value_heads if self.is_sliding else config.num_key_value_heads
|
| 58 |
+
self.n_rep = self.num_heads // self.num_kv_heads
|
| 59 |
+
self.sliding_window = config.sliding_window_size if self.is_sliding else None
|
| 60 |
+
self.rel_extent = config.sliding_window_size if self.is_sliding else config.rel_extent
|
| 61 |
+
self.d_rel = config.d_rel
|
| 62 |
+
# q/k are per-head RMS-normalized, hence 1/d rather than 1/sqrt(d)
|
| 63 |
+
self.scaling = 1.0 / self.head_dim
|
| 64 |
+
|
| 65 |
+
h = config.hidden_size
|
| 66 |
+
self.wq_du = nn.Linear(h, self.num_heads * self.head_dim, bias=False)
|
| 67 |
+
self.wk_dv = nn.Linear(h, self.num_kv_heads * self.head_dim, bias=False)
|
| 68 |
+
self.wv_dv = nn.Linear(h, self.num_kv_heads * self.head_dim, bias=False)
|
| 69 |
+
self.wr_du = nn.Linear(h, self.num_heads * self.d_rel, bias=False)
|
| 70 |
+
self.wo_ud = nn.Linear(self.num_heads * self.head_dim, h, bias=False)
|
| 71 |
+
|
| 72 |
+
self.k_sconv = ShortConvolution(self.num_kv_heads * self.head_dim, config.sconv_kernel_size)
|
| 73 |
+
self.v_sconv = ShortConvolution(self.num_kv_heads * self.head_dim, config.sconv_kernel_size)
|
| 74 |
+
self.q_norm = RMSNorm(self.head_dim, eps=config.rms_norm_eps)
|
| 75 |
+
self.k_norm = RMSNorm(self.head_dim, eps=config.rms_norm_eps)
|
| 76 |
+
self.rel_logits_proj = RelativeLogits(self.d_rel, self.rel_extent)
|
| 77 |
+
|
| 78 |
+
def __call__(self, hidden_states, start_pos=0, kv_cache=None,
|
| 79 |
+
k_conv=None, v_conv=None, conv_mask=None):
|
| 80 |
+
B, L, _ = hidden_states.shape
|
| 81 |
+
|
| 82 |
+
q = self.wq_du(hidden_states)
|
| 83 |
+
k = self.k_sconv(self.wk_dv(hidden_states), mask=conv_mask, cache=k_conv)
|
| 84 |
+
v = self.v_sconv(self.wv_dv(hidden_states), mask=conv_mask, cache=v_conv)
|
| 85 |
+
rel = self.wr_du(hidden_states)
|
| 86 |
+
|
| 87 |
+
q = self.q_norm(q.reshape(B, L, self.num_heads, self.head_dim))
|
| 88 |
+
k = self.k_norm(k.reshape(B, L, self.num_kv_heads, self.head_dim))
|
| 89 |
+
v = v.reshape(B, L, self.num_kv_heads, self.head_dim)
|
| 90 |
+
|
| 91 |
+
# -> [B, heads, L, head_dim]
|
| 92 |
+
q = q.transpose(0, 2, 1, 3)
|
| 93 |
+
k = k.transpose(0, 2, 1, 3)
|
| 94 |
+
v = v.transpose(0, 2, 1, 3)
|
| 95 |
+
|
| 96 |
+
q_pos = mx.arange(L) + start_pos
|
| 97 |
+
if kv_cache is not None:
|
| 98 |
+
k, v = kv_cache.update(k, v) # full history
|
| 99 |
+
kv_pos = mx.arange(k.shape[2])
|
| 100 |
+
|
| 101 |
+
rel = rel.reshape(B, L, self.num_heads, self.d_rel)
|
| 102 |
+
position_bias = self.rel_logits_proj(rel, q_pos, kv_pos) # [B, heads, Lq, Lkv]
|
| 103 |
+
|
| 104 |
+
# log-scaling (global layers only; no-op for context <= n_floor)
|
| 105 |
+
if not self.is_sliding and self.config.log_scaling_n_floor is not None:
|
| 106 |
+
n_floor = self.config.log_scaling_n_floor
|
| 107 |
+
eff_n = (q_pos + 1).astype(mx.float32)
|
| 108 |
+
tau = 1.0 + self.config.log_scaling_alpha * mx.log(
|
| 109 |
+
mx.maximum(eff_n / n_floor, 1.0)
|
| 110 |
+
)
|
| 111 |
+
tau_q = tau.reshape(1, 1, -1, 1)
|
| 112 |
+
q = (q.astype(mx.float32) * tau_q).astype(q.dtype)
|
| 113 |
+
position_bias = (position_bias.astype(mx.float32) * tau_q).astype(position_bias.dtype)
|
| 114 |
+
|
| 115 |
+
# Fused SDPA: O = softmax(scale·QKᵀ + mask)·V, softmax in fp32, native GQA
|
| 116 |
+
# (k/v passed un-tiled). mask = relative-position bias + causal/sliding, additive.
|
| 117 |
+
mask = position_bias + self._causal_mask(q_pos, kv_pos) # [B, heads, Lq, Lkv]
|
| 118 |
+
out = mx.fast.scaled_dot_product_attention(
|
| 119 |
+
q, k, v, scale=self.scaling, mask=mask.astype(q.dtype)
|
| 120 |
+
) # [B, heads, Lq, head_dim]
|
| 121 |
+
|
| 122 |
+
out = out.transpose(0, 2, 1, 3).reshape(B, L, self.num_heads * self.head_dim)
|
| 123 |
+
return self.wo_ud(out)
|
| 124 |
+
|
| 125 |
+
def _causal_mask(self, q_pos, kv_pos):
|
| 126 |
+
distance = q_pos[:, None] - kv_pos[None, :] # [Lq, Lkv]
|
| 127 |
+
allowed = distance >= 0
|
| 128 |
+
if self.sliding_window is not None:
|
| 129 |
+
allowed = allowed & (distance < self.sliding_window)
|
| 130 |
+
mask = mx.where(allowed, 0.0, NEG_INF)
|
| 131 |
+
return mask[None, None].astype(mx.float32)
|
inkling_mlx/audio.py
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Inkling audio tower: discrete dMel-token embedding + norm.
|
| 2 |
+
|
| 3 |
+
Each audio frame is ``n_mel_bins`` discretized bins (values in ``[0, mel_vocab_size)``);
|
| 4 |
+
each bin is embedded from its own slice of a shared table (offset ``bin * mel_vocab_size``)
|
| 5 |
+
and the per-bin embeddings are summed. Mirrors ``InklingAudioModel`` /
|
| 6 |
+
``InklingAudioModelEmbeddings``. Checkpoint keys: ``audio.encoder.weight`` (the
|
| 7 |
+
``[n_mel_bins*mel_vocab_size, hidden]`` table) and ``audio.final_norm.weight``.
|
| 8 |
+
"""
|
| 9 |
+
|
| 10 |
+
from __future__ import annotations
|
| 11 |
+
|
| 12 |
+
import mlx.core as mx
|
| 13 |
+
import mlx.nn as nn
|
| 14 |
+
|
| 15 |
+
from .common import RMSNorm
|
| 16 |
+
from .config import AudioConfig
|
| 17 |
+
|
| 18 |
+
|
| 19 |
+
class AudioModel(nn.Module):
|
| 20 |
+
def __init__(self, config: AudioConfig):
|
| 21 |
+
super().__init__()
|
| 22 |
+
self.config = config
|
| 23 |
+
self.encoder = nn.Embedding(
|
| 24 |
+
config.n_mel_bins * config.mel_vocab_size, config.text_hidden_size
|
| 25 |
+
)
|
| 26 |
+
self.final_norm = RMSNorm(config.text_hidden_size, eps=config.rms_norm_eps)
|
| 27 |
+
# non-persistent: arange(n_mel_bins) * mel_vocab_size
|
| 28 |
+
self._offsets = mx.arange(config.n_mel_bins) * config.mel_vocab_size
|
| 29 |
+
|
| 30 |
+
def __call__(self, audio_input_ids: mx.array) -> mx.array:
|
| 31 |
+
# audio_input_ids: [..., n_mel_bins] with values in [0, mel_vocab_size)
|
| 32 |
+
embeds = self.encoder(audio_input_ids + self._offsets) # [..., n_mel_bins, hidden]
|
| 33 |
+
embeds = embeds.sum(axis=-2) # [..., hidden]
|
| 34 |
+
return self.final_norm(embeds)
|
inkling_mlx/cache.py
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Incremental caches for Inkling generation.
|
| 2 |
+
|
| 3 |
+
Two kinds of per-layer state must persist across decode steps:
|
| 4 |
+
|
| 5 |
+
* ``KVCache`` — the appended key/value tensors for attention.
|
| 6 |
+
* ``ConvCache`` — the last ``kernel-1`` inputs of each depthwise short-convolution
|
| 7 |
+
(there are 4 per layer: k, v, post-attn, post-mlp).
|
| 8 |
+
|
| 9 |
+
A ``LayerCache`` bundles one KVCache + the 4 ConvCaches; ``make_cache`` builds one
|
| 10 |
+
per decoder layer. Absolute key positions are always ``arange(kv_len)`` because the
|
| 11 |
+
cache holds every key from position 0 (KVCache keeps the full history — correct for
|
| 12 |
+
both global and sliding layers, since the sliding-window constraint is enforced by
|
| 13 |
+
the attention mask).
|
| 14 |
+
"""
|
| 15 |
+
|
| 16 |
+
from __future__ import annotations
|
| 17 |
+
|
| 18 |
+
import mlx.core as mx
|
| 19 |
+
|
| 20 |
+
|
| 21 |
+
class ConvCache:
|
| 22 |
+
"""Holds the last ``kernel-1`` inputs of a short convolution."""
|
| 23 |
+
|
| 24 |
+
__slots__ = ("state",)
|
| 25 |
+
|
| 26 |
+
def __init__(self):
|
| 27 |
+
self.state = None # [B, kernel-1, C] or None
|
| 28 |
+
|
| 29 |
+
|
| 30 |
+
class KVCache:
|
| 31 |
+
"""Appends keys/values along the sequence axis (full history)."""
|
| 32 |
+
|
| 33 |
+
__slots__ = ("keys", "values")
|
| 34 |
+
|
| 35 |
+
def __init__(self):
|
| 36 |
+
self.keys = None # [B, heads, T, d]
|
| 37 |
+
self.values = None
|
| 38 |
+
|
| 39 |
+
@property
|
| 40 |
+
def offset(self) -> int:
|
| 41 |
+
return 0 if self.keys is None else self.keys.shape[2]
|
| 42 |
+
|
| 43 |
+
def update(self, k: mx.array, v: mx.array):
|
| 44 |
+
if self.keys is None:
|
| 45 |
+
self.keys, self.values = k, v
|
| 46 |
+
else:
|
| 47 |
+
self.keys = mx.concatenate([self.keys, k], axis=2)
|
| 48 |
+
self.values = mx.concatenate([self.values, v], axis=2)
|
| 49 |
+
return self.keys, self.values
|
| 50 |
+
|
| 51 |
+
|
| 52 |
+
class LayerCache:
|
| 53 |
+
__slots__ = ("kv", "k_conv", "v_conv", "attn_conv", "mlp_conv")
|
| 54 |
+
|
| 55 |
+
def __init__(self):
|
| 56 |
+
self.kv = KVCache()
|
| 57 |
+
self.k_conv = ConvCache()
|
| 58 |
+
self.v_conv = ConvCache()
|
| 59 |
+
self.attn_conv = ConvCache()
|
| 60 |
+
self.mlp_conv = ConvCache()
|
| 61 |
+
|
| 62 |
+
|
| 63 |
+
def make_cache(model) -> list[LayerCache]:
|
| 64 |
+
"""One LayerCache per text decoder layer."""
|
| 65 |
+
n = len(model.model.llm.layers) if hasattr(model, "model") else len(model.layers)
|
| 66 |
+
return [LayerCache() for _ in range(n)]
|
inkling_mlx/common.py
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Shared low-level modules for the Inkling MLX port."""
|
| 2 |
+
|
| 3 |
+
from __future__ import annotations
|
| 4 |
+
|
| 5 |
+
import mlx.core as mx
|
| 6 |
+
import mlx.nn as nn
|
| 7 |
+
|
| 8 |
+
|
| 9 |
+
class RMSNorm(nn.Module):
|
| 10 |
+
"""Llama-style RMSNorm (compute in fp32, weight is a gain).
|
| 11 |
+
|
| 12 |
+
Matches ``LlamaRMSNorm``: ``x_fp32 * rsqrt(mean(x^2) + eps) * weight``.
|
| 13 |
+
"""
|
| 14 |
+
|
| 15 |
+
def __init__(self, dims: int, eps: float = 1e-6):
|
| 16 |
+
super().__init__()
|
| 17 |
+
self.weight = mx.ones((dims,))
|
| 18 |
+
self.eps = eps
|
| 19 |
+
|
| 20 |
+
def __call__(self, x: mx.array) -> mx.array:
|
| 21 |
+
return mx.fast.rms_norm(x, self.weight, self.eps)
|
| 22 |
+
|
| 23 |
+
|
| 24 |
+
class ShortConvolution(nn.Module):
|
| 25 |
+
"""Depthwise causal 1-D convolution with a residual add, computed in fp32.
|
| 26 |
+
|
| 27 |
+
Mirrors ``InklingShortConvolution``: a per-channel (groups == channels) causal
|
| 28 |
+
conv1d of ``kernel_size`` taps, no bias, no activation, then ``out + input``.
|
| 29 |
+
The reference keeps this module in fp32 regardless of the model dtype
|
| 30 |
+
(``_keep_in_fp32_modules_strict``), so we upcast here too.
|
| 31 |
+
|
| 32 |
+
Weight layout (MLX ``conv1d``): ``[channels, kernel_size, 1]``.
|
| 33 |
+
"""
|
| 34 |
+
|
| 35 |
+
def __init__(self, channels: int, kernel_size: int):
|
| 36 |
+
super().__init__()
|
| 37 |
+
self.channels = channels
|
| 38 |
+
self.kernel_size = kernel_size
|
| 39 |
+
# [C_out, K, C_in // groups] with groups == channels -> [C, K, 1]
|
| 40 |
+
self.weight = mx.zeros((channels, kernel_size, 1))
|
| 41 |
+
|
| 42 |
+
def __call__(self, x: mx.array, mask: mx.array | None = None, cache=None) -> mx.array:
|
| 43 |
+
# x: [batch, seq, channels]
|
| 44 |
+
in_dtype = x.dtype
|
| 45 |
+
xf = x.astype(mx.float32)
|
| 46 |
+
residual = xf
|
| 47 |
+
if mask is not None:
|
| 48 |
+
xf = xf * mask.astype(mx.float32)
|
| 49 |
+
k = self.kernel_size
|
| 50 |
+
B, seq, C = xf.shape
|
| 51 |
+
w = self.weight.astype(mx.float32)
|
| 52 |
+
if cache is not None:
|
| 53 |
+
# left-context = cached last (k-1) inputs (zeros on the first call);
|
| 54 |
+
# a "valid" conv over [left, xf] yields exactly `seq` causal outputs.
|
| 55 |
+
left = cache.state if cache.state is not None else mx.zeros((B, k - 1, C), dtype=mx.float32)
|
| 56 |
+
x_in = mx.concatenate([left, xf], axis=1)
|
| 57 |
+
out = mx.conv1d(x_in, w, padding=0, groups=self.channels)
|
| 58 |
+
cache.state = x_in[:, -(k - 1):, :]
|
| 59 |
+
else:
|
| 60 |
+
# causal: left-pad by (k-1), keep first `seq` outputs (== zero left-context)
|
| 61 |
+
out = mx.conv1d(xf, w, padding=k - 1, groups=self.channels)[:, :seq, :]
|
| 62 |
+
out = out + residual
|
| 63 |
+
return out.astype(in_dtype)
|
inkling_mlx/config.py
ADDED
|
@@ -0,0 +1,207 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Configuration for the Inkling multimodal model (MLX port).
|
| 2 |
+
|
| 3 |
+
Mirrors ``thinkingmachines/Inkling`` ``config.json`` and the transformers PR #47347
|
| 4 |
+
reference (``InklingConfig`` / ``InklingTextConfig`` / ``InklingVisionConfig`` /
|
| 5 |
+
``InklingAudioConfig``). We parse the *checkpoint* config layout (top-level
|
| 6 |
+
``text_config`` / ``vision_config`` / ``audio_config`` / ``mtp_config``), not the
|
| 7 |
+
flattened transformers layout.
|
| 8 |
+
"""
|
| 9 |
+
|
| 10 |
+
from __future__ import annotations
|
| 11 |
+
|
| 12 |
+
from dataclasses import dataclass, field
|
| 13 |
+
from typing import Any
|
| 14 |
+
|
| 15 |
+
|
| 16 |
+
def _get(d: dict, *names, default=None):
|
| 17 |
+
for n in names:
|
| 18 |
+
if n in d and d[n] is not None:
|
| 19 |
+
return d[n]
|
| 20 |
+
return default
|
| 21 |
+
|
| 22 |
+
|
| 23 |
+
@dataclass
|
| 24 |
+
class TextConfig:
|
| 25 |
+
hidden_size: int = 6144
|
| 26 |
+
num_hidden_layers: int = 66
|
| 27 |
+
vocab_size: int = 201024
|
| 28 |
+
unpadded_vocab_size: int | None = 200058
|
| 29 |
+
|
| 30 |
+
# global (full) attention
|
| 31 |
+
num_attention_heads: int = 64
|
| 32 |
+
num_key_value_heads: int = 8
|
| 33 |
+
head_dim: int = 128
|
| 34 |
+
# sliding-window attention
|
| 35 |
+
swa_num_attention_heads: int = 64
|
| 36 |
+
swa_num_key_value_heads: int = 16
|
| 37 |
+
swa_head_dim: int = 128
|
| 38 |
+
sliding_window_size: int = 512
|
| 39 |
+
|
| 40 |
+
# relative-position logits
|
| 41 |
+
d_rel: int = 16
|
| 42 |
+
rel_extent: int = 1024
|
| 43 |
+
log_scaling_n_floor: int | None = 128000
|
| 44 |
+
log_scaling_alpha: float = 0.1
|
| 45 |
+
|
| 46 |
+
rms_norm_eps: float = 1e-6
|
| 47 |
+
use_embed_norm: bool = True
|
| 48 |
+
|
| 49 |
+
# short convolution
|
| 50 |
+
sconv_kernel_size: int = 4
|
| 51 |
+
|
| 52 |
+
# dense vs MoE MLP
|
| 53 |
+
dense_mlp_idx: int = 2
|
| 54 |
+
dense_intermediate_size: int = 24576 # dense MLP intermediate
|
| 55 |
+
moe_intermediate_size: int = 3072 # per-expert intermediate
|
| 56 |
+
|
| 57 |
+
# MoE routing
|
| 58 |
+
n_routed_experts: int = 256
|
| 59 |
+
num_experts_per_tok: int = 6
|
| 60 |
+
n_shared_experts: int = 2
|
| 61 |
+
shared_expert_sink: bool = True
|
| 62 |
+
route_scale: float = 8.0
|
| 63 |
+
use_gate_bias: bool = True
|
| 64 |
+
norm_after_topk: bool = True
|
| 65 |
+
use_global_scale: bool = True
|
| 66 |
+
|
| 67 |
+
logits_mup_width_multiplier: float = 24.0
|
| 68 |
+
hidden_act: str = "silu"
|
| 69 |
+
|
| 70 |
+
max_position_embeddings: int = 1048576
|
| 71 |
+
|
| 72 |
+
# which layer indices use sliding-window ("local") attention
|
| 73 |
+
local_layer_ids: list[int] = field(default_factory=list)
|
| 74 |
+
|
| 75 |
+
# MTP head (dropped for inference)
|
| 76 |
+
num_mtp_layers: int | None = None
|
| 77 |
+
|
| 78 |
+
@property
|
| 79 |
+
def layer_types(self) -> list[str]:
|
| 80 |
+
local = set(self.local_layer_ids)
|
| 81 |
+
return [
|
| 82 |
+
"hybrid_sliding" if i in local else "hybrid"
|
| 83 |
+
for i in range(self.num_hidden_layers)
|
| 84 |
+
]
|
| 85 |
+
|
| 86 |
+
@property
|
| 87 |
+
def mlp_layer_types(self) -> list[str]:
|
| 88 |
+
return [
|
| 89 |
+
"dense" if i < self.dense_mlp_idx else "sparse"
|
| 90 |
+
for i in range(self.num_hidden_layers)
|
| 91 |
+
]
|
| 92 |
+
|
| 93 |
+
@classmethod
|
| 94 |
+
def from_dict(cls, tc: dict) -> "TextConfig":
|
| 95 |
+
return cls(
|
| 96 |
+
hidden_size=_get(tc, "hidden_size", default=6144),
|
| 97 |
+
num_hidden_layers=_get(tc, "num_hidden_layers", default=66),
|
| 98 |
+
vocab_size=_get(tc, "vocab_size", default=201024),
|
| 99 |
+
unpadded_vocab_size=_get(tc, "unpadded_vocab_size"),
|
| 100 |
+
num_attention_heads=_get(tc, "num_attention_heads", default=64),
|
| 101 |
+
num_key_value_heads=_get(tc, "num_key_value_heads", default=8),
|
| 102 |
+
head_dim=_get(tc, "head_dim", default=128),
|
| 103 |
+
swa_num_attention_heads=_get(tc, "swa_num_attention_heads", default=64),
|
| 104 |
+
swa_num_key_value_heads=_get(tc, "swa_num_key_value_heads", default=16),
|
| 105 |
+
swa_head_dim=_get(tc, "swa_head_dim", default=128),
|
| 106 |
+
sliding_window_size=_get(tc, "sliding_window_size", default=512),
|
| 107 |
+
d_rel=_get(tc, "d_rel", default=16),
|
| 108 |
+
rel_extent=_get(tc, "rel_extent", default=1024),
|
| 109 |
+
log_scaling_n_floor=_get(tc, "log_scaling_n_floor"),
|
| 110 |
+
log_scaling_alpha=_get(tc, "log_scaling_alpha", default=0.1),
|
| 111 |
+
rms_norm_eps=_get(tc, "rms_norm_eps", default=1e-6),
|
| 112 |
+
use_embed_norm=_get(tc, "use_embed_norm", default=True),
|
| 113 |
+
sconv_kernel_size=_get(tc, "sconv_kernel_size", default=4),
|
| 114 |
+
dense_mlp_idx=_get(tc, "dense_mlp_idx", default=2),
|
| 115 |
+
dense_intermediate_size=_get(tc, "dense_intermediate_size", default=24576),
|
| 116 |
+
# checkpoint labels the *MoE* intermediate as `intermediate_size`
|
| 117 |
+
moe_intermediate_size=_get(tc, "intermediate_size", default=3072),
|
| 118 |
+
n_routed_experts=_get(tc, "n_routed_experts", default=256),
|
| 119 |
+
num_experts_per_tok=_get(tc, "num_experts_per_tok", default=6),
|
| 120 |
+
n_shared_experts=_get(tc, "n_shared_experts", default=2),
|
| 121 |
+
shared_expert_sink=_get(tc, "shared_expert_sink", default=True),
|
| 122 |
+
route_scale=_get(tc, "route_scale", default=8.0),
|
| 123 |
+
use_gate_bias=_get(tc, "use_gate_bias", default=True),
|
| 124 |
+
norm_after_topk=_get(tc, "norm_after_topk", default=True),
|
| 125 |
+
use_global_scale=_get(tc, "use_global_scale", default=True),
|
| 126 |
+
logits_mup_width_multiplier=_get(tc, "logits_mup_width_multiplier", default=24.0),
|
| 127 |
+
max_position_embeddings=_get(tc, "model_max_length", "max_position_embeddings", default=1048576),
|
| 128 |
+
local_layer_ids=list(_get(tc, "local_layer_ids", default=[]) or []),
|
| 129 |
+
)
|
| 130 |
+
|
| 131 |
+
|
| 132 |
+
@dataclass
|
| 133 |
+
class VisionConfig:
|
| 134 |
+
text_hidden_size: int = 6144
|
| 135 |
+
patch_size: int = 40
|
| 136 |
+
temporal_patch_size: int = 2
|
| 137 |
+
num_channels: int = 3
|
| 138 |
+
n_layers: int = 4
|
| 139 |
+
rms_norm_eps: float = 1e-6
|
| 140 |
+
use_vision_norm: bool = True
|
| 141 |
+
|
| 142 |
+
@classmethod
|
| 143 |
+
def from_dict(cls, vc: dict, text_hidden: int) -> "VisionConfig":
|
| 144 |
+
return cls(
|
| 145 |
+
text_hidden_size=text_hidden,
|
| 146 |
+
patch_size=_get(vc, "patch_size", default=40),
|
| 147 |
+
temporal_patch_size=_get(vc, "temporal_patch_size", default=2),
|
| 148 |
+
num_channels=_get(vc, "n_channels", "num_channels", default=3),
|
| 149 |
+
n_layers=_get(vc, "n_layers", "num_hidden_layers", default=4),
|
| 150 |
+
rms_norm_eps=_get(vc, "rms_norm_eps", default=1e-6),
|
| 151 |
+
use_vision_norm=_get(vc, "use_vision_norm", default=True),
|
| 152 |
+
)
|
| 153 |
+
|
| 154 |
+
|
| 155 |
+
@dataclass
|
| 156 |
+
class AudioConfig:
|
| 157 |
+
text_hidden_size: int = 6144
|
| 158 |
+
n_mel_bins: int = 80
|
| 159 |
+
mel_vocab_size: int = 16
|
| 160 |
+
rms_norm_eps: float = 1e-6
|
| 161 |
+
|
| 162 |
+
@classmethod
|
| 163 |
+
def from_dict(cls, ac: dict, text_hidden: int) -> "AudioConfig":
|
| 164 |
+
return cls(
|
| 165 |
+
text_hidden_size=text_hidden,
|
| 166 |
+
n_mel_bins=_get(ac, "n_mel_bins", default=80),
|
| 167 |
+
mel_vocab_size=_get(ac, "mel_vocab_size", default=16),
|
| 168 |
+
rms_norm_eps=_get(ac, "rms_norm_eps", default=1e-6),
|
| 169 |
+
)
|
| 170 |
+
|
| 171 |
+
|
| 172 |
+
@dataclass
|
| 173 |
+
class InklingConfig:
|
| 174 |
+
text: TextConfig
|
| 175 |
+
vision: VisionConfig
|
| 176 |
+
audio: AudioConfig
|
| 177 |
+
image_token_id: int = 200054
|
| 178 |
+
audio_token_id: int = 200053
|
| 179 |
+
image_bos_token_id: int = 200005
|
| 180 |
+
audio_bos_token_id: int = 200020
|
| 181 |
+
eos_token_id: int = 200006
|
| 182 |
+
model_type: str = "inkling_mm_model"
|
| 183 |
+
|
| 184 |
+
@classmethod
|
| 185 |
+
def from_dict(cls, cfg: dict) -> "InklingConfig":
|
| 186 |
+
tc = dict(cfg.get("text_config", {}))
|
| 187 |
+
mtp = cfg.get("mtp_config") or {}
|
| 188 |
+
if mtp.get("num_nextn_predict_layers") is not None:
|
| 189 |
+
tc.setdefault("num_mtp_layers", mtp.get("num_nextn_predict_layers"))
|
| 190 |
+
text = TextConfig.from_dict(tc)
|
| 191 |
+
vision = VisionConfig.from_dict(cfg.get("vision_config", {}) or {}, text.hidden_size)
|
| 192 |
+
audio = AudioConfig.from_dict(cfg.get("audio_config", {}) or {}, text.hidden_size)
|
| 193 |
+
return cls(
|
| 194 |
+
text=text,
|
| 195 |
+
vision=vision,
|
| 196 |
+
audio=audio,
|
| 197 |
+
image_token_id=_get(cfg, "image_token_id", default=200054),
|
| 198 |
+
audio_token_id=_get(cfg, "audio_token_id", default=200053),
|
| 199 |
+
image_bos_token_id=_get(cfg, "image_bos_token_id", default=200005),
|
| 200 |
+
audio_bos_token_id=_get(cfg, "audio_bos_token_id", default=200020),
|
| 201 |
+
eos_token_id=_get(cfg, "eos_token_id", default=200006),
|
| 202 |
+
model_type=_get(cfg, "model_type", default="inkling_mm_model"),
|
| 203 |
+
)
|
| 204 |
+
|
| 205 |
+
@property
|
| 206 |
+
def raw(self) -> dict[str, Any]:
|
| 207 |
+
return {"model_type": self.model_type}
|
inkling_mlx/convert.py
ADDED
|
@@ -0,0 +1,254 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Streaming HF -> MLX conversion + quantization for Inkling.
|
| 2 |
+
|
| 3 |
+
The model is far too large (~1.9 TB bf16) to instantiate in RAM, so we convert
|
| 4 |
+
tensor-by-tensor: read each source shard (mmap), remap the name, apply the layout
|
| 5 |
+
transform, optionally affine-quantize the weight, and write output shards. Affine
|
| 6 |
+
quantization has no cross-tensor dependency, so per-tensor streaming is exactly
|
| 7 |
+
equivalent to ``nn.quantize(model)``.
|
| 8 |
+
|
| 9 |
+
Name/layout transforms vs. the checkpoint:
|
| 10 |
+
* ``*_sconv.weight`` [C,1,K] -> [C,K,1] (MLX conv1d layout)
|
| 11 |
+
* ``mlp.w13_dn`` [2I,H] -> gate_proj/up_proj (split dense fused gate+up)
|
| 12 |
+
* ``experts.w13_weight`` [E,2I,H] -> gate_proj/up_proj (split)
|
| 13 |
+
* ``experts.w2_weight`` [E,H,I] -> down_proj (identity)
|
| 14 |
+
* ``shared_experts.shared_w13`` [2,2I,H] -> gate_proj/up_proj (split)
|
| 15 |
+
* ``model.mtp.*`` dropped (inference-irrelevant)
|
| 16 |
+
* everything else: identity
|
| 17 |
+
"""
|
| 18 |
+
|
| 19 |
+
from __future__ import annotations
|
| 20 |
+
|
| 21 |
+
import glob
|
| 22 |
+
import json
|
| 23 |
+
import os
|
| 24 |
+
import re
|
| 25 |
+
import shutil
|
| 26 |
+
|
| 27 |
+
import mlx.core as mx
|
| 28 |
+
|
| 29 |
+
_LAYER_RE = re.compile(r"model\.llm\.layers\.(\d+)\.")
|
| 30 |
+
_N_SHARED = 2 # Inkling: 2 shared experts (router rows after the routed ones)
|
| 31 |
+
|
| 32 |
+
|
| 33 |
+
def prune_moe_tensor(name: str, w: mx.array, keep, dense_mlp_idx: int) -> mx.array:
|
| 34 |
+
"""Subset a MoE tensor to the kept experts (REAP). ``keep`` maps sparse-layer
|
| 35 |
+
index -> array of kept routed-expert indices. Applied BEFORE map_name/transform.
|
| 36 |
+
Routed experts + the router (gate) are subset; shared experts are untouched."""
|
| 37 |
+
m = _LAYER_RE.search(name)
|
| 38 |
+
if m is None:
|
| 39 |
+
return w
|
| 40 |
+
L = int(m.group(1))
|
| 41 |
+
if L < dense_mlp_idx: # dense layer — no routed experts
|
| 42 |
+
return w
|
| 43 |
+
kidx = mx.array(keep[L - dense_mlp_idx]) # [K]
|
| 44 |
+
if name.endswith(("experts.w13_weight", "experts.w2_weight")) and "shared" not in name:
|
| 45 |
+
return w[kidx] # [E, ...] -> [K, ...]
|
| 46 |
+
if name.endswith("mlp.gate.weight"): # [n_routed + n_shared, hidden]
|
| 47 |
+
n_routed = w.shape[0] - _N_SHARED # rows: [routed... , shared...]
|
| 48 |
+
return mx.concatenate([w[kidx], w[n_routed:]], axis=0) # [K + n_shared, hidden]
|
| 49 |
+
if name.endswith("mlp.gate.bias"): # [n_routed] correction bias
|
| 50 |
+
return w[kidx]
|
| 51 |
+
return w # shared_experts.*, gate.global_scale, etc.
|
| 52 |
+
|
| 53 |
+
|
| 54 |
+
def map_name(name: str):
|
| 55 |
+
"""HF checkpoint tensor name -> list of (out_name, kind) for the MLX model."""
|
| 56 |
+
if name.startswith("model.mtp."):
|
| 57 |
+
return [] # drop MTP head
|
| 58 |
+
|
| 59 |
+
if name.endswith(("k_sconv.weight", "v_sconv.weight", "attn_sconv.weight", "mlp_sconv.weight")):
|
| 60 |
+
return [(name, "sconv")]
|
| 61 |
+
|
| 62 |
+
# dense MLP fused gate+up / down
|
| 63 |
+
if name.endswith("mlp.w13_dn.weight"):
|
| 64 |
+
base = name[: -len("w13_dn.weight")]
|
| 65 |
+
return [(base + "gate_proj.weight", "w13_gate"), (base + "up_proj.weight", "w13_up")]
|
| 66 |
+
if name.endswith("mlp.w2_md.weight"):
|
| 67 |
+
return [(name[: -len("w2_md.weight")] + "down_proj.weight", "identity")]
|
| 68 |
+
|
| 69 |
+
# routed experts fused
|
| 70 |
+
if name.endswith("experts.w13_weight"):
|
| 71 |
+
base = name[: -len("w13_weight")]
|
| 72 |
+
return [(base + "gate_proj.weight", "w13_gate"), (base + "up_proj.weight", "w13_up")]
|
| 73 |
+
if name.endswith("experts.w2_weight"):
|
| 74 |
+
return [(name[: -len("w2_weight")] + "down_proj.weight", "identity")]
|
| 75 |
+
|
| 76 |
+
# shared experts fused
|
| 77 |
+
if name.endswith("shared_experts.shared_w13_weight"):
|
| 78 |
+
base = name[: -len("shared_w13_weight")]
|
| 79 |
+
return [(base + "gate_proj.weight", "w13_gate"), (base + "up_proj.weight", "w13_up")]
|
| 80 |
+
if name.endswith("shared_experts.shared_w2_weight"):
|
| 81 |
+
return [(name[: -len("shared_w2_weight")] + "down_proj.weight", "identity")]
|
| 82 |
+
|
| 83 |
+
return [(name, "identity")]
|
| 84 |
+
|
| 85 |
+
|
| 86 |
+
def transform(w: mx.array, kind: str) -> mx.array:
|
| 87 |
+
if kind == "identity":
|
| 88 |
+
return w
|
| 89 |
+
if kind == "sconv":
|
| 90 |
+
# [C, 1, K] -> [C, K, 1]
|
| 91 |
+
return mx.swapaxes(w, 1, 2)
|
| 92 |
+
if kind in ("w13_gate", "w13_up"):
|
| 93 |
+
# The checkpoint stores gate/up INTERLEAVED row-wise: [g0, u0, g1, u1, ...]
|
| 94 |
+
# (SGLang `deinterleave_w13`). De-interleave: gate = rows 0::2, up = rows 1::2.
|
| 95 |
+
# A contiguous [:half]/[half:] split scrambles gate<->up in every MLP.
|
| 96 |
+
n = w.shape[-2] // 2
|
| 97 |
+
g = w.reshape(*w.shape[:-2], n, 2, w.shape[-1])
|
| 98 |
+
return g[..., 0, :] if kind == "w13_gate" else g[..., 1, :]
|
| 99 |
+
raise ValueError(kind)
|
| 100 |
+
|
| 101 |
+
|
| 102 |
+
# ---- quantization target predicate (must be identical in convert and load) ----
|
| 103 |
+
|
| 104 |
+
# Quant "recipes" — which module leaves get affine-quantized.
|
| 105 |
+
# uniform : everything (attention, MLP/experts, embed/unembed, audio, vision)
|
| 106 |
+
# experts_only : ONLY the MLP/expert matmuls (+ audio/vision); attention and
|
| 107 |
+
# embed/unembed stay bf16. Inkling attention dominates 4-bit error
|
| 108 |
+
# (~58% per layer vs ~15% for experts), so this keeps a 4-bit-sized
|
| 109 |
+
# build coherent while the ~927 B experts still fit in 512 GB.
|
| 110 |
+
_RECIPES = {
|
| 111 |
+
"uniform": {"wq_du", "wk_dv", "wv_dv", "wr_du", "wo_ud",
|
| 112 |
+
"gate_proj", "up_proj", "down_proj", "embed", "unembed", "encoder"},
|
| 113 |
+
"experts_only": {"gate_proj", "up_proj", "down_proj", "encoder"},
|
| 114 |
+
}
|
| 115 |
+
|
| 116 |
+
|
| 117 |
+
def is_quant_target(out_name: str, quant_axis_size: int, group_size: int, recipe: str = "uniform") -> bool:
|
| 118 |
+
"""Whether ``out_name`` (a converted param path) should be affine-quantized."""
|
| 119 |
+
if not out_name.endswith(".weight"):
|
| 120 |
+
return False
|
| 121 |
+
leaf = out_name[: -len(".weight")].rsplit(".", 1)[-1]
|
| 122 |
+
leaves = _RECIPES[recipe]
|
| 123 |
+
# vision projection layers (linear_0 .. linear_3) — quantized in both recipes
|
| 124 |
+
is_vision_linear = leaf.startswith("linear_") and ".visual." in out_name
|
| 125 |
+
if leaf not in leaves and not is_vision_linear:
|
| 126 |
+
return False
|
| 127 |
+
# router gate stays fp (leaf == "gate", excluded above); norms/sconv excluded by leaf
|
| 128 |
+
# can only group-quantize when the input dim is a multiple of group_size
|
| 129 |
+
return quant_axis_size % group_size == 0
|
| 130 |
+
|
| 131 |
+
|
| 132 |
+
# ------------------------------ streaming driver ------------------------------
|
| 133 |
+
|
| 134 |
+
_SHARD_CAP_BYTES = 5_000_000_000 # ~5 GB per output shard
|
| 135 |
+
|
| 136 |
+
|
| 137 |
+
def _process_tensor(name, w, bits, group_size, out_dtype, recipe="uniform", keep=None, dmi=2):
|
| 138 |
+
"""Yield (out_name, array) pairs for one source tensor."""
|
| 139 |
+
if keep is not None:
|
| 140 |
+
w = prune_moe_tensor(name, w, keep, dmi) # REAP: subset to kept experts
|
| 141 |
+
for out_name, kind in map_name(name):
|
| 142 |
+
wt = transform(w, kind)
|
| 143 |
+
quantize = bits is not None and is_quant_target(out_name, wt.shape[-1], group_size, recipe)
|
| 144 |
+
if quantize:
|
| 145 |
+
qw, scales, biases = mx.quantize(wt, group_size=group_size, bits=bits)
|
| 146 |
+
base = out_name[: -len(".weight")]
|
| 147 |
+
yield out_name, qw
|
| 148 |
+
yield base + ".scales", scales
|
| 149 |
+
yield base + ".biases", biases
|
| 150 |
+
else:
|
| 151 |
+
# keep norms/router/sconv/rel-proj in fp32-safe dtype; matmul weights in out_dtype
|
| 152 |
+
keep_hi = wt.dtype == mx.float32 and (".global_scale" in out_name or ".bias" in out_name
|
| 153 |
+
or out_name.endswith(("_norm.weight", "norm.weight")))
|
| 154 |
+
yield out_name, wt.astype(mx.float32 if keep_hi else out_dtype)
|
| 155 |
+
|
| 156 |
+
|
| 157 |
+
def convert_model(src: str, dst: str, bits=None, group_size: int = 64, out_dtype=mx.bfloat16,
|
| 158 |
+
recipe: str = "uniform", keep_path=None):
|
| 159 |
+
"""Stream-convert an Inkling checkpoint from ``src`` to ``dst``.
|
| 160 |
+
|
| 161 |
+
``bits=None`` -> plain dtype cast (bf16). ``bits in {4,6,8}`` -> affine quant.
|
| 162 |
+
``recipe`` selects which modules are quantized (see ``_RECIPES``).
|
| 163 |
+
``keep_path`` (REAP): npz with ``keep`` [n_sparse_layers, K] + ``dense_mlp_idx`` ->
|
| 164 |
+
prune each MoE layer to its K kept experts and set ``n_routed_experts=K`` in config.
|
| 165 |
+
Processes one source shard at a time; never holds the whole model in RAM.
|
| 166 |
+
"""
|
| 167 |
+
os.makedirs(dst, exist_ok=True)
|
| 168 |
+
keep = dmi = new_ne = None
|
| 169 |
+
if keep_path is not None:
|
| 170 |
+
import numpy as np
|
| 171 |
+
kd = np.load(keep_path)
|
| 172 |
+
keep = kd["keep"]; dmi = int(kd["dense_mlp_idx"]); new_ne = int(kd["K"])
|
| 173 |
+
index = json.load(open(os.path.join(src, "model.safetensors.index.json")))
|
| 174 |
+
weight_map = index["weight_map"]
|
| 175 |
+
|
| 176 |
+
shard_to_names: dict[str, list[str]] = {}
|
| 177 |
+
for n, s in weight_map.items():
|
| 178 |
+
shard_to_names.setdefault(s, []).append(n)
|
| 179 |
+
|
| 180 |
+
out_index: dict[str, str] = {}
|
| 181 |
+
buffer: dict[str, mx.array] = {}
|
| 182 |
+
buffer_bytes = 0
|
| 183 |
+
out_shard_id = 0
|
| 184 |
+
total_out_shards_placeholder = "{:05d}"
|
| 185 |
+
|
| 186 |
+
def flush(final=False):
|
| 187 |
+
nonlocal buffer, buffer_bytes, out_shard_id
|
| 188 |
+
if not buffer:
|
| 189 |
+
return
|
| 190 |
+
out_shard_id += 1
|
| 191 |
+
fname = f"model-{total_out_shards_placeholder.format(out_shard_id)}.safetensors"
|
| 192 |
+
mx.save_safetensors(os.path.join(dst, fname), buffer, metadata={"format": "mlx"})
|
| 193 |
+
for k in buffer:
|
| 194 |
+
out_index[k] = fname
|
| 195 |
+
buffer = {}
|
| 196 |
+
buffer_bytes = 0
|
| 197 |
+
|
| 198 |
+
for shard in sorted(shard_to_names):
|
| 199 |
+
path = os.path.join(src, shard)
|
| 200 |
+
tensors = mx.load(path) # mmap
|
| 201 |
+
for name in shard_to_names[shard]:
|
| 202 |
+
w = tensors[name]
|
| 203 |
+
for out_name, arr in _process_tensor(name, w, bits, group_size, out_dtype, recipe, keep, dmi):
|
| 204 |
+
mx.eval(arr)
|
| 205 |
+
buffer[out_name] = arr
|
| 206 |
+
buffer_bytes += arr.nbytes
|
| 207 |
+
if buffer_bytes >= _SHARD_CAP_BYTES:
|
| 208 |
+
flush()
|
| 209 |
+
del tensors
|
| 210 |
+
flush(final=True)
|
| 211 |
+
|
| 212 |
+
# rename shards with correct total, build index.json
|
| 213 |
+
_finalize_index(dst, out_index, out_shard_id)
|
| 214 |
+
_write_config(src, dst, bits, group_size, recipe, new_ne)
|
| 215 |
+
_copy_aux(src, dst)
|
| 216 |
+
return dst
|
| 217 |
+
|
| 218 |
+
|
| 219 |
+
def _finalize_index(dst, out_index, n_shards):
|
| 220 |
+
# rewrite shard filenames to model-XXXXX-of-YYYYY.safetensors
|
| 221 |
+
remap = {}
|
| 222 |
+
for i in range(1, n_shards + 1):
|
| 223 |
+
old = f"model-{i:05d}.safetensors"
|
| 224 |
+
new = f"model-{i:05d}-of-{n_shards:05d}.safetensors"
|
| 225 |
+
if old != new and os.path.exists(os.path.join(dst, old)):
|
| 226 |
+
os.rename(os.path.join(dst, old), os.path.join(dst, new))
|
| 227 |
+
remap[old] = new
|
| 228 |
+
weight_map = {k: remap[v] for k, v in out_index.items()}
|
| 229 |
+
total = sum(os.path.getsize(os.path.join(dst, f)) for f in set(weight_map.values()))
|
| 230 |
+
with open(os.path.join(dst, "model.safetensors.index.json"), "w") as f:
|
| 231 |
+
json.dump({"metadata": {"total_size": total}, "weight_map": weight_map}, f, indent=2)
|
| 232 |
+
|
| 233 |
+
|
| 234 |
+
def _write_config(src, dst, bits, group_size, recipe="uniform", new_ne=None):
|
| 235 |
+
cfg = json.load(open(os.path.join(src, "config.json")))
|
| 236 |
+
if bits is not None:
|
| 237 |
+
cfg["quantization"] = {"group_size": group_size, "bits": bits, "recipe": recipe}
|
| 238 |
+
if new_ne is not None: # REAP: fewer routed experts
|
| 239 |
+
cfg["text_config"]["n_routed_experts"] = new_ne
|
| 240 |
+
cfg.setdefault("reap", {})["kept_experts"] = new_ne
|
| 241 |
+
with open(os.path.join(dst, "config.json"), "w") as f:
|
| 242 |
+
json.dump(cfg, f, indent=2)
|
| 243 |
+
|
| 244 |
+
|
| 245 |
+
def _copy_aux(src, dst):
|
| 246 |
+
for pat in ("tokenizer*", "special_tokens_map.json", "*.tiktoken", "tiktoken",
|
| 247 |
+
"chat_template.jinja", "processor_config.json"):
|
| 248 |
+
for p in glob.glob(os.path.join(src, pat)):
|
| 249 |
+
base = os.path.basename(p)
|
| 250 |
+
target = os.path.join(dst, base)
|
| 251 |
+
if os.path.isdir(p):
|
| 252 |
+
shutil.copytree(p, target, dirs_exist_ok=True)
|
| 253 |
+
else:
|
| 254 |
+
shutil.copy2(p, target)
|
inkling_mlx/convert_cli.py
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""CLI: convert/quantize an Inkling checkpoint to MLX.
|
| 2 |
+
|
| 3 |
+
Examples:
|
| 4 |
+
python -m inkling_mlx.convert_cli --src /path/Inkling-src --dst out-bf16
|
| 5 |
+
python -m inkling_mlx.convert_cli --src /path/Inkling-src --dst out-4bit --bits 4
|
| 6 |
+
"""
|
| 7 |
+
|
| 8 |
+
from __future__ import annotations
|
| 9 |
+
|
| 10 |
+
import argparse
|
| 11 |
+
import time
|
| 12 |
+
|
| 13 |
+
import mlx.core as mx
|
| 14 |
+
|
| 15 |
+
from .convert import convert_model
|
| 16 |
+
|
| 17 |
+
|
| 18 |
+
def main():
|
| 19 |
+
ap = argparse.ArgumentParser()
|
| 20 |
+
ap.add_argument("--src", required=True, help="Inkling bf16 source dir (HF layout)")
|
| 21 |
+
ap.add_argument("--dst", required=True, help="output dir")
|
| 22 |
+
ap.add_argument("--bits", type=int, default=None, choices=[2, 3, 4, 5, 6, 8],
|
| 23 |
+
help="quantization bits; omit for bf16 passthrough")
|
| 24 |
+
ap.add_argument("--group-size", type=int, default=64)
|
| 25 |
+
ap.add_argument("--dtype", default="bfloat16", choices=["bfloat16", "float16"])
|
| 26 |
+
ap.add_argument("--device", default="gpu", choices=["gpu", "cpu"],
|
| 27 |
+
help="cpu avoids the Metal GPU-timeout watchdog on huge tensors (slower, robust)")
|
| 28 |
+
ap.add_argument("--recipe", default="uniform", choices=["uniform", "experts_only"],
|
| 29 |
+
help="experts_only keeps attention + embed/unembed at bf16 (coherent 4-bit-sized build)")
|
| 30 |
+
ap.add_argument("--prune", default=None,
|
| 31 |
+
help="REAP keep-indices npz (from prune_experts.py) -> prune experts during convert")
|
| 32 |
+
args = ap.parse_args()
|
| 33 |
+
|
| 34 |
+
if args.device == "cpu":
|
| 35 |
+
mx.set_default_device(mx.cpu)
|
| 36 |
+
print("[convert] using CPU device (avoids Metal command-buffer timeout)")
|
| 37 |
+
|
| 38 |
+
dtype = {"bfloat16": mx.bfloat16, "float16": mx.float16}[args.dtype]
|
| 39 |
+
t0 = time.time()
|
| 40 |
+
print(f"[convert] {args.src} -> {args.dst} bits={args.bits} group_size={args.group_size} dtype={args.dtype} recipe={args.recipe} prune={args.prune}")
|
| 41 |
+
convert_model(args.src, args.dst, bits=args.bits, group_size=args.group_size, out_dtype=dtype,
|
| 42 |
+
recipe=args.recipe, keep_path=args.prune)
|
| 43 |
+
print(f"[convert] done in {time.time()-t0:.0f}s -> {args.dst}")
|
| 44 |
+
|
| 45 |
+
|
| 46 |
+
if __name__ == "__main__":
|
| 47 |
+
main()
|
inkling_mlx/generate.py
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Greedy generation for an Inkling MLX model, using an incremental KV + conv-state
|
| 2 |
+
cache: the prompt is prefilled once, then each new token is a single-position step.
|
| 3 |
+
"""
|
| 4 |
+
|
| 5 |
+
from __future__ import annotations
|
| 6 |
+
|
| 7 |
+
import argparse
|
| 8 |
+
import time
|
| 9 |
+
|
| 10 |
+
import mlx.core as mx
|
| 11 |
+
|
| 12 |
+
from .cache import make_cache
|
| 13 |
+
from .load import load
|
| 14 |
+
|
| 15 |
+
|
| 16 |
+
def load_tokenizer(path: str):
|
| 17 |
+
try:
|
| 18 |
+
from transformers import AutoTokenizer
|
| 19 |
+
return AutoTokenizer.from_pretrained(path, trust_remote_code=True)
|
| 20 |
+
except Exception:
|
| 21 |
+
from transformers import PreTrainedTokenizerFast
|
| 22 |
+
import os
|
| 23 |
+
return PreTrainedTokenizerFast(tokenizer_file=os.path.join(path, "tokenizer.json"))
|
| 24 |
+
|
| 25 |
+
|
| 26 |
+
def greedy_generate(model, config, input_ids, max_new_tokens=32, eos_id=None,
|
| 27 |
+
pixel_values=None, audio_input_ids=None):
|
| 28 |
+
"""Greedy decode. For multimodal, pass ``pixel_values`` / ``audio_input_ids``
|
| 29 |
+
(from ``InklingProcessor``); they are consumed only by the prompt prefill."""
|
| 30 |
+
eos_id = eos_id if eos_id is not None else config.eos_token_id
|
| 31 |
+
caches = make_cache(model)
|
| 32 |
+
prompt = list(input_ids)
|
| 33 |
+
|
| 34 |
+
# prefill the whole prompt (with any media) in one pass
|
| 35 |
+
logits = model(mx.array([prompt]), caches=caches, start_pos=0, last_logit_only=True,
|
| 36 |
+
pixel_values=pixel_values, audio_input_ids=audio_input_ids)
|
| 37 |
+
next_id = int(mx.argmax(logits[0, -1]).item())
|
| 38 |
+
out = [next_id]
|
| 39 |
+
pos = len(prompt)
|
| 40 |
+
|
| 41 |
+
for _ in range(max_new_tokens - 1):
|
| 42 |
+
if next_id == eos_id:
|
| 43 |
+
break
|
| 44 |
+
logits = model(mx.array([[next_id]]), caches=caches, start_pos=pos, last_logit_only=True)
|
| 45 |
+
next_id = int(mx.argmax(logits[0, -1]).item())
|
| 46 |
+
out.append(next_id)
|
| 47 |
+
pos += 1
|
| 48 |
+
|
| 49 |
+
return prompt + out
|
| 50 |
+
|
| 51 |
+
|
| 52 |
+
def main():
|
| 53 |
+
ap = argparse.ArgumentParser()
|
| 54 |
+
ap.add_argument("--model", required=True, help="converted MLX model dir")
|
| 55 |
+
ap.add_argument("--prompt", default="The capital of France is")
|
| 56 |
+
ap.add_argument("--max-new-tokens", type=int, default=32)
|
| 57 |
+
ap.add_argument("--wired-limit-gb", type=float, default=500.0,
|
| 58 |
+
help="mx wired-memory limit; needs `sudo sysctl iogpu.wired_limit_mb` set too")
|
| 59 |
+
ap.add_argument("--lazy", action="store_true",
|
| 60 |
+
help="mmap weights instead of eager-loading (lower peak RAM, but forwards "
|
| 61 |
+
"re-read from disk and thrash near the memory ceiling)")
|
| 62 |
+
args = ap.parse_args()
|
| 63 |
+
|
| 64 |
+
# eager load pins the weights wired-resident so prefill/decode don't re-read the
|
| 65 |
+
# mmap (the big win for near-capacity models); pass --lazy to opt out.
|
| 66 |
+
try:
|
| 67 |
+
mx.set_wired_limit(int(args.wired_limit_gb * 1e9))
|
| 68 |
+
except Exception as e:
|
| 69 |
+
print(f"[warn] set_wired_limit: {e}")
|
| 70 |
+
|
| 71 |
+
print(f"[load] {args.model} ({'lazy mmap' if args.lazy else 'eager, wired-resident'})")
|
| 72 |
+
t0 = time.time()
|
| 73 |
+
model, config = load(args.model, lazy=args.lazy)
|
| 74 |
+
print(f"[load] ready in {time.time()-t0:.0f}s")
|
| 75 |
+
|
| 76 |
+
tok = load_tokenizer(args.model)
|
| 77 |
+
input_ids = tok(args.prompt)["input_ids"]
|
| 78 |
+
print(f"[prompt] {args.prompt!r} -> {len(input_ids)} tokens")
|
| 79 |
+
|
| 80 |
+
t0 = time.time()
|
| 81 |
+
out_ids = greedy_generate(model, config, input_ids, args.max_new_tokens)
|
| 82 |
+
dt = time.time() - t0
|
| 83 |
+
text = tok.decode(out_ids)
|
| 84 |
+
n_new = len(out_ids) - len(input_ids)
|
| 85 |
+
print(f"\n{text}\n")
|
| 86 |
+
print(f"[gen] {n_new} tokens in {dt:.1f}s ({n_new/dt:.2f} tok/s)")
|
| 87 |
+
|
| 88 |
+
|
| 89 |
+
if __name__ == "__main__":
|
| 90 |
+
main()
|
inkling_mlx/layers.py
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Inkling decoder layer: attention + MLP, each wrapped by a pre-norm and a
|
| 2 |
+
trailing short-convolution, with residual adds. Mirrors ``InklingDecoderLayer``.
|
| 3 |
+
"""
|
| 4 |
+
|
| 5 |
+
from __future__ import annotations
|
| 6 |
+
|
| 7 |
+
import mlx.core as mx
|
| 8 |
+
import mlx.nn as nn
|
| 9 |
+
|
| 10 |
+
from .attention import Attention
|
| 11 |
+
from .common import RMSNorm, ShortConvolution
|
| 12 |
+
from .config import TextConfig
|
| 13 |
+
from .moe import DenseMLP, MoE
|
| 14 |
+
|
| 15 |
+
|
| 16 |
+
class DecoderLayer(nn.Module):
|
| 17 |
+
def __init__(self, config: TextConfig, layer_idx: int):
|
| 18 |
+
super().__init__()
|
| 19 |
+
self.attn = Attention(config, layer_idx)
|
| 20 |
+
self.attn_norm = RMSNorm(config.hidden_size, eps=config.rms_norm_eps)
|
| 21 |
+
self.mlp_norm = RMSNorm(config.hidden_size, eps=config.rms_norm_eps)
|
| 22 |
+
if config.mlp_layer_types[layer_idx] == "sparse":
|
| 23 |
+
self.mlp = MoE(config)
|
| 24 |
+
else:
|
| 25 |
+
self.mlp = DenseMLP(config)
|
| 26 |
+
self.attn_sconv = ShortConvolution(config.hidden_size, config.sconv_kernel_size)
|
| 27 |
+
self.mlp_sconv = ShortConvolution(config.hidden_size, config.sconv_kernel_size)
|
| 28 |
+
|
| 29 |
+
def __call__(self, x, start_pos=0, cache=None, conv_mask=None):
|
| 30 |
+
kv = cache.kv if cache is not None else None
|
| 31 |
+
residual = x
|
| 32 |
+
h = self.attn_norm(x)
|
| 33 |
+
h = self.attn(
|
| 34 |
+
h, start_pos=start_pos, kv_cache=kv,
|
| 35 |
+
k_conv=cache.k_conv if cache is not None else None,
|
| 36 |
+
v_conv=cache.v_conv if cache is not None else None,
|
| 37 |
+
conv_mask=conv_mask,
|
| 38 |
+
)
|
| 39 |
+
h = self.attn_sconv(h, mask=conv_mask, cache=cache.attn_conv if cache is not None else None)
|
| 40 |
+
x = residual + h
|
| 41 |
+
|
| 42 |
+
residual = x
|
| 43 |
+
h = self.mlp_norm(x)
|
| 44 |
+
h = self.mlp(h)
|
| 45 |
+
h = self.mlp_sconv(h, mask=conv_mask, cache=cache.mlp_conv if cache is not None else None)
|
| 46 |
+
x = residual + h
|
| 47 |
+
return x
|
inkling_mlx/load.py
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Load a (possibly quantized) Inkling MLX model produced by ``convert_model``."""
|
| 2 |
+
|
| 3 |
+
from __future__ import annotations
|
| 4 |
+
|
| 5 |
+
import glob
|
| 6 |
+
import json
|
| 7 |
+
import os
|
| 8 |
+
|
| 9 |
+
import mlx.core as mx
|
| 10 |
+
import mlx.nn as nn
|
| 11 |
+
from mlx.utils import tree_flatten
|
| 12 |
+
|
| 13 |
+
from .config import InklingConfig
|
| 14 |
+
from .model import InklingForConditionalGeneration
|
| 15 |
+
|
| 16 |
+
|
| 17 |
+
def quant_predicate(group_size: int, recipe: str = "uniform"):
|
| 18 |
+
"""Quantize exactly the modules the converter did, by delegating to
|
| 19 |
+
``convert.is_quant_target`` with the same ``recipe``. Guarantees the loaded
|
| 20 |
+
module set matches the checkpoint (e.g. under ``experts_only``, attention and
|
| 21 |
+
embed/unembed stay bf16 and must NOT be re-quantized here)."""
|
| 22 |
+
from .convert import is_quant_target
|
| 23 |
+
|
| 24 |
+
def pred(path, module):
|
| 25 |
+
if not hasattr(module, "to_quantized"):
|
| 26 |
+
return False
|
| 27 |
+
w = getattr(module, "weight", None)
|
| 28 |
+
if w is None:
|
| 29 |
+
return False
|
| 30 |
+
return is_quant_target(path + ".weight", w.shape[-1], group_size, recipe)
|
| 31 |
+
|
| 32 |
+
return pred
|
| 33 |
+
|
| 34 |
+
|
| 35 |
+
def load(path: str, lazy: bool = False):
|
| 36 |
+
cfg_dict = json.load(open(os.path.join(path, "config.json")))
|
| 37 |
+
config = InklingConfig.from_dict(cfg_dict)
|
| 38 |
+
model = InklingForConditionalGeneration(config)
|
| 39 |
+
|
| 40 |
+
q = cfg_dict.get("quantization")
|
| 41 |
+
if q:
|
| 42 |
+
nn.quantize(model, group_size=q["group_size"], bits=q["bits"],
|
| 43 |
+
class_predicate=quant_predicate(q["group_size"], q.get("recipe", "uniform")))
|
| 44 |
+
|
| 45 |
+
# Stream shards: assign each, then release its handle. We do NOT eagerly
|
| 46 |
+
# mx.eval() the whole parameter tree — for a ~500 GB model that builds one
|
| 47 |
+
# enormous eval graph and trips a Metal resource limit. Weights stay lazy
|
| 48 |
+
# (mmap-backed) and materialize on demand during the forward pass, exactly
|
| 49 |
+
# like mlx-lm loads large models.
|
| 50 |
+
loaded = set()
|
| 51 |
+
shards = sorted(glob.glob(os.path.join(path, "*.safetensors")))
|
| 52 |
+
for shard in shards:
|
| 53 |
+
w = mx.load(shard)
|
| 54 |
+
model.load_weights(list(w.items()), strict=False)
|
| 55 |
+
if not lazy:
|
| 56 |
+
# materialize THIS shard's tensors now (bounded graph) and keep them
|
| 57 |
+
# resident. Avoids one enormous eval over all ~500 GB of params, which
|
| 58 |
+
# trips a Metal resource limit; also prevents per-token disk paging.
|
| 59 |
+
mx.eval(list(w.values()))
|
| 60 |
+
loaded.update(w.keys())
|
| 61 |
+
del w
|
| 62 |
+
|
| 63 |
+
expected = {k for k, _ in tree_flatten(model.parameters())}
|
| 64 |
+
missing = expected - loaded
|
| 65 |
+
if missing:
|
| 66 |
+
raise ValueError(f"{len(missing)} params not found in checkpoint, e.g. {sorted(missing)[:3]}")
|
| 67 |
+
|
| 68 |
+
model.eval()
|
| 69 |
+
return model, config
|
inkling_mlx/model.py
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Top-level Inkling multimodal model.
|
| 2 |
+
|
| 3 |
+
Checkpoint layout: ``model.llm.*`` (text backbone + untied unembed), ``model.visual.*``
|
| 4 |
+
(HMLP vision tower), ``model.audio.*`` (dMel audio tower). Image/audio features are
|
| 5 |
+
scattered into the token-embedding stream at their placeholder-token positions, then
|
| 6 |
+
the text backbone runs and the untied unembed head produces (muP-scaled) logits.
|
| 7 |
+
The MTP head (``model.mtp.*``) is intentionally not loaded (inference-irrelevant).
|
| 8 |
+
"""
|
| 9 |
+
|
| 10 |
+
from __future__ import annotations
|
| 11 |
+
|
| 12 |
+
import mlx.core as mx
|
| 13 |
+
import mlx.nn as nn
|
| 14 |
+
import numpy as np
|
| 15 |
+
|
| 16 |
+
from .audio import AudioModel
|
| 17 |
+
from .config import InklingConfig
|
| 18 |
+
from .text import TextModel
|
| 19 |
+
from .vision import VisionModel
|
| 20 |
+
|
| 21 |
+
|
| 22 |
+
def _scatter_features(embeds, input_ids, token_id, features):
|
| 23 |
+
"""Replace ``embeds`` rows where ``input_ids == token_id`` with ``features``
|
| 24 |
+
(in sequence order). ``input_ids`` is host-known so we resolve positions on CPU."""
|
| 25 |
+
B, L, H = embeds.shape
|
| 26 |
+
ids = np.array(input_ids).reshape(-1)
|
| 27 |
+
pos = np.nonzero(ids == token_id)[0]
|
| 28 |
+
if pos.size == 0:
|
| 29 |
+
return embeds
|
| 30 |
+
flat = embeds.reshape(B * L, H)
|
| 31 |
+
flat[mx.array(pos)] = features.astype(flat.dtype)
|
| 32 |
+
return flat.reshape(B, L, H)
|
| 33 |
+
|
| 34 |
+
|
| 35 |
+
class InnerModel(nn.Module):
|
| 36 |
+
"""The ``model.`` level holding the three towers."""
|
| 37 |
+
|
| 38 |
+
def __init__(self, config: InklingConfig):
|
| 39 |
+
super().__init__()
|
| 40 |
+
self.llm = TextModel(config.text)
|
| 41 |
+
self.visual = VisionModel(config.vision)
|
| 42 |
+
self.audio = AudioModel(config.audio)
|
| 43 |
+
|
| 44 |
+
|
| 45 |
+
class InklingForConditionalGeneration(nn.Module):
|
| 46 |
+
def __init__(self, config: InklingConfig):
|
| 47 |
+
super().__init__()
|
| 48 |
+
self.config = config
|
| 49 |
+
self.model = InnerModel(config)
|
| 50 |
+
|
| 51 |
+
# --- convenience accessors ---
|
| 52 |
+
@property
|
| 53 |
+
def llm(self) -> TextModel:
|
| 54 |
+
return self.model.llm
|
| 55 |
+
|
| 56 |
+
def __call__(
|
| 57 |
+
self,
|
| 58 |
+
input_ids: mx.array,
|
| 59 |
+
pixel_values: mx.array | None = None,
|
| 60 |
+
audio_input_ids: mx.array | None = None,
|
| 61 |
+
conv_mask=None,
|
| 62 |
+
caches=None,
|
| 63 |
+
start_pos: int = 0,
|
| 64 |
+
last_logit_only: bool = False,
|
| 65 |
+
) -> mx.array:
|
| 66 |
+
embeds = self.model.llm.embed_tokens(input_ids)
|
| 67 |
+
|
| 68 |
+
if pixel_values is not None:
|
| 69 |
+
img = self.model.visual(pixel_values)
|
| 70 |
+
embeds = _scatter_features(embeds, input_ids, self.config.image_token_id, img)
|
| 71 |
+
|
| 72 |
+
if audio_input_ids is not None:
|
| 73 |
+
aud = self.model.audio(audio_input_ids)
|
| 74 |
+
embeds = _scatter_features(embeds, input_ids, self.config.audio_token_id, aud)
|
| 75 |
+
|
| 76 |
+
hidden = self.model.llm.backbone(embeds, conv_mask=conv_mask, caches=caches, start_pos=start_pos)
|
| 77 |
+
if last_logit_only:
|
| 78 |
+
hidden = hidden[:, -1:, :]
|
| 79 |
+
return self.model.llm.logits(hidden)
|
inkling_mlx/moe.py
ADDED
|
@@ -0,0 +1,117 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Inkling MLP variants: dense SwiGLU (with a learned output scale) and the
|
| 2 |
+
sparse MoE (sigmoid router with correction bias, softmax-over-selected weights,
|
| 3 |
+
route/global scaling, and 2 always-on shared experts forming a routing "sink").
|
| 4 |
+
|
| 5 |
+
Mirrors ``InklingMLP`` / ``InklingTopkRouter`` / ``InklingExperts`` /
|
| 6 |
+
``InklingSharedExperts`` / ``InklingMoE`` from transformers PR #47347.
|
| 7 |
+
"""
|
| 8 |
+
|
| 9 |
+
from __future__ import annotations
|
| 10 |
+
|
| 11 |
+
import mlx.core as mx
|
| 12 |
+
import mlx.nn as nn
|
| 13 |
+
|
| 14 |
+
from mlx_lm.models.switch_layers import SwitchGLU
|
| 15 |
+
|
| 16 |
+
from .config import TextConfig
|
| 17 |
+
|
| 18 |
+
|
| 19 |
+
class DenseMLP(nn.Module):
|
| 20 |
+
"""SwiGLU MLP with a learned scalar output gain (``global_scale``).
|
| 21 |
+
|
| 22 |
+
The checkpoint fuses gate+up into ``w13_dn``; the converter splits it into
|
| 23 |
+
``gate_proj``/``up_proj`` so the standard MLX quantizer sees plain ``nn.Linear``s.
|
| 24 |
+
"""
|
| 25 |
+
|
| 26 |
+
def __init__(self, config: TextConfig):
|
| 27 |
+
super().__init__()
|
| 28 |
+
h = config.hidden_size
|
| 29 |
+
inter = config.dense_intermediate_size
|
| 30 |
+
self.gate_proj = nn.Linear(h, inter, bias=False)
|
| 31 |
+
self.up_proj = nn.Linear(h, inter, bias=False)
|
| 32 |
+
self.down_proj = nn.Linear(inter, h, bias=False)
|
| 33 |
+
self.global_scale = mx.ones((1,))
|
| 34 |
+
|
| 35 |
+
def __call__(self, x: mx.array) -> mx.array:
|
| 36 |
+
y = self.down_proj(nn.silu(self.gate_proj(x)) * self.up_proj(x))
|
| 37 |
+
return y * self.global_scale
|
| 38 |
+
|
| 39 |
+
|
| 40 |
+
class Router(nn.Module):
|
| 41 |
+
"""Sigmoid top-k router with a correction bias and a shared-expert sink.
|
| 42 |
+
|
| 43 |
+
Kept in full precision (tiny). Returns per-token routed weights/indices plus
|
| 44 |
+
the two shared-expert gammas produced by the same softmax (the "sink").
|
| 45 |
+
"""
|
| 46 |
+
|
| 47 |
+
def __init__(self, config: TextConfig):
|
| 48 |
+
super().__init__()
|
| 49 |
+
self.num_experts = config.n_routed_experts
|
| 50 |
+
self.n_shared = config.n_shared_experts
|
| 51 |
+
self.n_total = self.num_experts + self.n_shared
|
| 52 |
+
self.top_k = config.num_experts_per_tok
|
| 53 |
+
self.route_scale = config.route_scale
|
| 54 |
+
self.hidden = config.hidden_size
|
| 55 |
+
self.weight = mx.zeros((self.n_total, config.hidden_size))
|
| 56 |
+
self.bias = mx.zeros((self.num_experts,)) # e_score_correction_bias
|
| 57 |
+
self.global_scale = mx.ones((1,))
|
| 58 |
+
|
| 59 |
+
def __call__(self, x: mx.array):
|
| 60 |
+
# Routing (esp. the top-k selection) is precision-sensitive: in bf16 the
|
| 61 |
+
# rounding of near-tied expert scores flips which experts fire, and a wrong
|
| 62 |
+
# choice compounds over 64 MoE layers into incoherent output. Compute the
|
| 63 |
+
# whole router in fp32.
|
| 64 |
+
flat = x.reshape(-1, self.hidden).astype(mx.float32)
|
| 65 |
+
router_logits = flat @ self.weight.T.astype(mx.float32) # [T, n_total]
|
| 66 |
+
scores = mx.sigmoid(router_logits)
|
| 67 |
+
routed_scores = scores[:, : self.num_experts]
|
| 68 |
+
scores_for_choice = routed_scores + self.bias
|
| 69 |
+
|
| 70 |
+
# top-k experts (order within the top-k is irrelevant downstream)
|
| 71 |
+
topk_idx = mx.argpartition(-scores_for_choice, kth=self.top_k - 1, axis=-1)[:, : self.top_k]
|
| 72 |
+
|
| 73 |
+
routed_logits = router_logits[:, : self.num_experts]
|
| 74 |
+
shared_logits = router_logits[:, self.num_experts :] # [T, n_shared]
|
| 75 |
+
gathered = mx.take_along_axis(routed_logits, topk_idx, axis=-1) # [T, top_k]
|
| 76 |
+
topk_logits = mx.concatenate([gathered, shared_logits], axis=-1) # [T, top_k+n_shared]
|
| 77 |
+
|
| 78 |
+
# softmax over the selected (+shared) logits, computed in the log domain
|
| 79 |
+
log_probs = -mx.logaddexp(mx.zeros_like(topk_logits), -topk_logits) # logsigmoid
|
| 80 |
+
weights = mx.softmax(log_probs, axis=-1)
|
| 81 |
+
weights = weights * self.route_scale * self.global_scale
|
| 82 |
+
|
| 83 |
+
shared_gammas = weights[:, self.top_k :].astype(x.dtype) # [T, n_shared]
|
| 84 |
+
topk_weights = weights[:, : self.top_k].astype(x.dtype) # [T, top_k]
|
| 85 |
+
return topk_weights, topk_idx, shared_gammas
|
| 86 |
+
|
| 87 |
+
|
| 88 |
+
class MoE(nn.Module):
|
| 89 |
+
def __init__(self, config: TextConfig):
|
| 90 |
+
super().__init__()
|
| 91 |
+
self.config = config
|
| 92 |
+
self.n_shared = config.n_shared_experts
|
| 93 |
+
self.gate = Router(config)
|
| 94 |
+
self.experts = SwitchGLU(
|
| 95 |
+
config.hidden_size, config.moe_intermediate_size, config.n_routed_experts, bias=False
|
| 96 |
+
)
|
| 97 |
+
self.shared_experts = SwitchGLU(
|
| 98 |
+
config.hidden_size, config.moe_intermediate_size, config.n_shared_experts, bias=False
|
| 99 |
+
)
|
| 100 |
+
|
| 101 |
+
def __call__(self, x: mx.array) -> mx.array:
|
| 102 |
+
B, L, H = x.shape
|
| 103 |
+
topk_weights, topk_idx, shared_gammas = self.gate(x)
|
| 104 |
+
xf = x.reshape(-1, H) # [T, H]
|
| 105 |
+
T = xf.shape[0]
|
| 106 |
+
|
| 107 |
+
routed = self.experts(xf, topk_idx) # [T, top_k, H]
|
| 108 |
+
routed = (routed * topk_weights[..., None]).sum(axis=1)
|
| 109 |
+
|
| 110 |
+
shared_idx = mx.broadcast_to(
|
| 111 |
+
mx.arange(self.n_shared)[None], (T, self.n_shared)
|
| 112 |
+
)
|
| 113 |
+
shared = self.shared_experts(xf, shared_idx) # [T, n_shared, H]
|
| 114 |
+
shared = (shared.astype(mx.float32) * shared_gammas[..., None].astype(mx.float32)).sum(axis=1)
|
| 115 |
+
shared = shared.astype(routed.dtype)
|
| 116 |
+
|
| 117 |
+
return (routed + shared).reshape(B, L, H)
|
inkling_mlx/processing.py
ADDED
|
@@ -0,0 +1,188 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Image + audio preprocessing for Inkling (MLX), ported from the reference
|
| 2 |
+
`InklingImageProcessor` / `InklingFeatureExtractor` / `InklingProcessor`.
|
| 3 |
+
|
| 4 |
+
* image -> pixel_values [num_patches, T=2, 40, 40, 3] (feeds `VisionModel`)
|
| 5 |
+
* audio -> audio_input_ids [num_frames, 80] dMel bins (feeds `AudioModel`)
|
| 6 |
+
|
| 7 |
+
`InklingProcessor.apply` builds the full multimodal input (input_ids + features)
|
| 8 |
+
from a chat message list, inserting the right number of placeholder soft-tokens.
|
| 9 |
+
Uses numpy/PIL + transformers' mel filterbank; no torch needed at inference.
|
| 10 |
+
"""
|
| 11 |
+
|
| 12 |
+
from __future__ import annotations
|
| 13 |
+
|
| 14 |
+
import math
|
| 15 |
+
|
| 16 |
+
import numpy as np
|
| 17 |
+
|
| 18 |
+
# CLIP normalization (OPENAI_CLIP_MEAN / STD), per processor_config.json
|
| 19 |
+
CLIP_MEAN = np.array([0.48145466, 0.4578275, 0.40821073], dtype=np.float32)
|
| 20 |
+
CLIP_STD = np.array([0.26862954, 0.26130258, 0.27577711], dtype=np.float32)
|
| 21 |
+
|
| 22 |
+
PATCH = 40 # image patch size (== vision patch_size)
|
| 23 |
+
TEMPORAL = 2 # temporal_patch_size (images duplicated across 2 frames)
|
| 24 |
+
|
| 25 |
+
# audio (processor_config.json / feature_extraction_inkling.py)
|
| 26 |
+
SR = 16000
|
| 27 |
+
HOP = 800 # audio_token_duration_s (0.05) * SR
|
| 28 |
+
WIN = 1600 # * window_size_multiplier (2.0)
|
| 29 |
+
N_FFT = 1600
|
| 30 |
+
N_MEL = 80
|
| 31 |
+
DMEL_BINS = 16
|
| 32 |
+
DMEL_MIN, DMEL_MAX = -7.0, 2.0
|
| 33 |
+
|
| 34 |
+
# special tokens
|
| 35 |
+
IMAGE_TOKEN_ID = 200054 # <|unused_200054|> (soft-token slot)
|
| 36 |
+
AUDIO_TOKEN_ID = 200053 # <|unused_200053|>
|
| 37 |
+
IMAGE_BOS = "<|content_image|>"
|
| 38 |
+
AUDIO_BOS = "<|content_audio_input|>"
|
| 39 |
+
|
| 40 |
+
|
| 41 |
+
# ------------------------------- image -------------------------------
|
| 42 |
+
|
| 43 |
+
def preprocess_image(image, max_long_edge: int | None = None) -> tuple[np.ndarray, int]:
|
| 44 |
+
"""PIL.Image or HxWx3 uint8 array -> (pixel_values [N,2,40,40,3] float32, N).
|
| 45 |
+
|
| 46 |
+
``max_long_edge`` (optional) downscales the image (LANCZOS, aspect preserved) so its
|
| 47 |
+
long edge is at most this many pixels *before* patchify. Each 40px patch is one
|
| 48 |
+
vision soft-token, so this directly cuts the prompt length / prefill cost for big
|
| 49 |
+
images (e.g. cap 512 -> ~130 patches vs ~450 at full 960px). ``None`` keeps native
|
| 50 |
+
resolution (the reference default)."""
|
| 51 |
+
from PIL import Image
|
| 52 |
+
if not hasattr(image, "convert"):
|
| 53 |
+
image = Image.fromarray(np.asarray(image).astype(np.uint8))
|
| 54 |
+
image = image.convert("RGB")
|
| 55 |
+
if max_long_edge and max(image.size) > max_long_edge:
|
| 56 |
+
r = max_long_edge / max(image.size)
|
| 57 |
+
image = image.resize((max(1, round(image.width * r)), max(1, round(image.height * r))), Image.LANCZOS)
|
| 58 |
+
image = np.asarray(image)
|
| 59 |
+
if image.ndim == 2:
|
| 60 |
+
image = np.stack([image] * 3, axis=-1)
|
| 61 |
+
img = image[..., :3].astype(np.float32).transpose(2, 0, 1) # -> [C, H, W]
|
| 62 |
+
C, H, W = img.shape
|
| 63 |
+
|
| 64 |
+
num_rows = (H + PATCH - 1) // PATCH
|
| 65 |
+
num_cols = W // PATCH + 1 # reference: W//P + 1
|
| 66 |
+
patches = []
|
| 67 |
+
for i in range(num_rows):
|
| 68 |
+
for j in range(num_cols):
|
| 69 |
+
p = img[:, i * PATCH:(i + 1) * PATCH, j * PATCH:(j + 1) * PATCH] # may be < 40
|
| 70 |
+
padded = np.full((C, PATCH, PATCH), -1.0, dtype=np.float32) # pad value -1.0
|
| 71 |
+
padded[:, : p.shape[1], : p.shape[2]] = p
|
| 72 |
+
patches.append(padded)
|
| 73 |
+
patches = np.stack(patches, axis=0) # [N, C, 40, 40]
|
| 74 |
+
|
| 75 |
+
# rescale (1/255) + CLIP normalize per channel
|
| 76 |
+
patches = patches / 255.0
|
| 77 |
+
patches = (patches - CLIP_MEAN[None, :, None, None]) / CLIP_STD[None, :, None, None]
|
| 78 |
+
|
| 79 |
+
# add temporal dim, duplicate x2, then -> [N, T, H, W, C]
|
| 80 |
+
patches = np.repeat(patches[..., None], TEMPORAL, axis=-1) # [N, C, 40, 40, 2]
|
| 81 |
+
pixel_values = patches.transpose(0, 4, 2, 3, 1) # [N, 2, 40, 40, C]
|
| 82 |
+
return pixel_values.astype(np.float32), pixel_values.shape[0]
|
| 83 |
+
|
| 84 |
+
|
| 85 |
+
# ------------------------------- audio -------------------------------
|
| 86 |
+
|
| 87 |
+
_mel_fb = None
|
| 88 |
+
def _mel_filters() -> np.ndarray:
|
| 89 |
+
global _mel_fb
|
| 90 |
+
if _mel_fb is None:
|
| 91 |
+
from transformers.audio_utils import mel_filter_bank
|
| 92 |
+
fb = mel_filter_bank(num_frequency_bins=N_FFT // 2 + 1, num_mel_filters=N_MEL,
|
| 93 |
+
min_frequency=0.0, max_frequency=SR / 2.0, sampling_rate=SR,
|
| 94 |
+
norm="slaney", mel_scale="slaney") # [801, 80]
|
| 95 |
+
_mel_fb = np.ascontiguousarray(fb.T, dtype=np.float32) # [80, 801]
|
| 96 |
+
return _mel_fb
|
| 97 |
+
|
| 98 |
+
|
| 99 |
+
def _log_mel(waveform: np.ndarray) -> np.ndarray:
|
| 100 |
+
"""raw mono waveform -> log10-mel spectrogram [num_frames, 80]."""
|
| 101 |
+
wav = np.asarray(waveform, dtype=np.float32).reshape(-1)
|
| 102 |
+
right = math.ceil(wav.shape[0] / HOP) * HOP - wav.shape[0]
|
| 103 |
+
left = max(N_FFT - HOP, 0)
|
| 104 |
+
wav = np.pad(wav, (left, right))
|
| 105 |
+
window = np.hanning(WIN + 1)[:-1].astype(np.float32) # periodic Hann
|
| 106 |
+
n_frames = 1 + (wav.shape[0] - N_FFT) // HOP # center=False
|
| 107 |
+
frames = np.stack([wav[i * HOP: i * HOP + N_FFT] * window for i in range(n_frames)]) # [T, N_FFT]
|
| 108 |
+
mag = np.abs(np.fft.rfft(frames, n=N_FFT, axis=-1)) # [T, 801]
|
| 109 |
+
mag = np.maximum(mag, 1e-10)
|
| 110 |
+
mel = _mel_filters() @ mag.T # [80, T]
|
| 111 |
+
mel = np.log10(np.maximum(mel, 1e-10))
|
| 112 |
+
return mel.T # [T, 80]
|
| 113 |
+
|
| 114 |
+
|
| 115 |
+
def preprocess_audio(waveform: np.ndarray, sampling_rate: int = SR) -> np.ndarray:
|
| 116 |
+
"""raw 16 kHz mono waveform -> dMel bin ids [num_frames, 80] (int32, 0..15)."""
|
| 117 |
+
if sampling_rate != SR:
|
| 118 |
+
raise ValueError(f"Inkling audio expects {SR} Hz, got {sampling_rate}")
|
| 119 |
+
mel = _log_mel(waveform) # [T, 80] log10
|
| 120 |
+
n_valid = math.ceil(len(np.asarray(waveform).reshape(-1)) / HOP)
|
| 121 |
+
mel = mel[:n_valid] # drop trailing pad frames
|
| 122 |
+
centers = np.linspace(DMEL_MIN, DMEL_MAX, DMEL_BINS) # 16 bin centers
|
| 123 |
+
clamped = np.clip(mel.astype(np.float64), DMEL_MIN, DMEL_MAX)
|
| 124 |
+
bins = np.abs(clamped[..., None] - centers).argmin(-1) # nearest center
|
| 125 |
+
return bins.astype(np.int32) # [T, 80]
|
| 126 |
+
|
| 127 |
+
|
| 128 |
+
# --------------------------- prompt assembly ---------------------------
|
| 129 |
+
|
| 130 |
+
class InklingProcessor:
|
| 131 |
+
"""Assembles multimodal model inputs from chat messages with image/audio parts.
|
| 132 |
+
|
| 133 |
+
Content parts: {"type":"text","text":...}, {"type":"image","image":PIL/array},
|
| 134 |
+
{"type":"audio","audio":waveform, "sampling_rate":16000}.
|
| 135 |
+
"""
|
| 136 |
+
|
| 137 |
+
def __init__(self, tokenizer, chat_template: str):
|
| 138 |
+
self.tok = tokenizer
|
| 139 |
+
self.chat_template = chat_template
|
| 140 |
+
self.image_bos_id = tokenizer.encode(IMAGE_BOS, add_special_tokens=False)[0]
|
| 141 |
+
self.audio_bos_id = tokenizer.encode(AUDIO_BOS, add_special_tokens=False)[0]
|
| 142 |
+
|
| 143 |
+
def apply(self, messages, reasoning_effort: str = "none", max_long_edge: int | None = None):
|
| 144 |
+
import mlx.core as mx
|
| 145 |
+
pixel_values, audio_ids = [], []
|
| 146 |
+
# Render text via the chat template with placeholders stripped to a sentinel,
|
| 147 |
+
# then splice media spans in. We build ids directly for robustness.
|
| 148 |
+
ids: list[int] = []
|
| 149 |
+
|
| 150 |
+
def emit_text(s):
|
| 151 |
+
ids.extend(self.tok.encode(s, add_special_tokens=False))
|
| 152 |
+
|
| 153 |
+
# header: thinking-effort system message (matches chat_template)
|
| 154 |
+
eff = {"none": 0.0, "minimal": 0.1, "low": 0.2, "medium": 0.7, "high": 0.9, "max": 0.99}[reasoning_effort]
|
| 155 |
+
emit_text(f"<|message_system|><|content_text|>Thinking effort level: {0 if eff == 0 else eff}<|end_message|>")
|
| 156 |
+
|
| 157 |
+
for msg in messages:
|
| 158 |
+
role = {"user": "<|message_user|>", "assistant": "<|message_model|>",
|
| 159 |
+
"system": "<|message_system|>"}[msg["role"]]
|
| 160 |
+
content = msg["content"]
|
| 161 |
+
if isinstance(content, str):
|
| 162 |
+
content = [{"type": "text", "text": content}]
|
| 163 |
+
for part in content:
|
| 164 |
+
t = part.get("type", "text")
|
| 165 |
+
if t == "text":
|
| 166 |
+
emit_text(role + "<|content_text|>" + part["text"] + "<|end_message|>")
|
| 167 |
+
elif t == "image":
|
| 168 |
+
pv, n = preprocess_image(part["image"], max_long_edge=max_long_edge)
|
| 169 |
+
pixel_values.append(pv)
|
| 170 |
+
ids.append(self.tok.encode(role, add_special_tokens=False)[0])
|
| 171 |
+
ids.append(self.image_bos_id)
|
| 172 |
+
ids.extend([IMAGE_TOKEN_ID] * n)
|
| 173 |
+
ids.extend(self.tok.encode("<|end_message|>", add_special_tokens=False))
|
| 174 |
+
elif t == "audio":
|
| 175 |
+
aid = preprocess_audio(part["audio"], part.get("sampling_rate", SR))
|
| 176 |
+
audio_ids.append(aid)
|
| 177 |
+
ids.append(self.tok.encode(role, add_special_tokens=False)[0])
|
| 178 |
+
ids.append(self.audio_bos_id)
|
| 179 |
+
ids.extend([AUDIO_TOKEN_ID] * aid.shape[0])
|
| 180 |
+
ids.extend(self.tok.encode("<|end_message|>", add_special_tokens=False))
|
| 181 |
+
emit_text("<|message_model|>") # generation prompt
|
| 182 |
+
|
| 183 |
+
out = {"input_ids": ids}
|
| 184 |
+
if pixel_values:
|
| 185 |
+
out["pixel_values"] = mx.array(np.concatenate(pixel_values, axis=0))
|
| 186 |
+
if audio_ids:
|
| 187 |
+
out["audio_input_ids"] = mx.array(np.concatenate(audio_ids, axis=0))
|
| 188 |
+
return out
|
inkling_mlx/text.py
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Inkling text backbone (``model.llm.*``): token embedding + embed-norm,
|
| 2 |
+
66 decoder layers, final norm, and the (untied) unembed head.
|
| 3 |
+
|
| 4 |
+
Mirrors ``InklingTextModel`` + the unembed / muP-logit scaling from
|
| 5 |
+
``InklingForConditionalGeneration``.
|
| 6 |
+
"""
|
| 7 |
+
|
| 8 |
+
from __future__ import annotations
|
| 9 |
+
|
| 10 |
+
import mlx.core as mx
|
| 11 |
+
import mlx.nn as nn
|
| 12 |
+
|
| 13 |
+
from .common import RMSNorm
|
| 14 |
+
from .config import TextConfig
|
| 15 |
+
from .layers import DecoderLayer
|
| 16 |
+
|
| 17 |
+
|
| 18 |
+
class TextModel(nn.Module):
|
| 19 |
+
def __init__(self, config: TextConfig):
|
| 20 |
+
super().__init__()
|
| 21 |
+
self.config = config
|
| 22 |
+
self.embed = nn.Embedding(config.vocab_size, config.hidden_size)
|
| 23 |
+
self.embed_norm = RMSNorm(config.hidden_size, eps=config.rms_norm_eps)
|
| 24 |
+
self.layers = [DecoderLayer(config, i) for i in range(config.num_hidden_layers)]
|
| 25 |
+
self.norm = RMSNorm(config.hidden_size, eps=config.rms_norm_eps)
|
| 26 |
+
self.unembed = nn.Linear(config.hidden_size, config.vocab_size, bias=False)
|
| 27 |
+
|
| 28 |
+
def embed_tokens(self, input_ids: mx.array) -> mx.array:
|
| 29 |
+
return self.embed_norm(self.embed(input_ids))
|
| 30 |
+
|
| 31 |
+
def backbone(self, inputs_embeds: mx.array, conv_mask=None, caches=None, start_pos=0) -> mx.array:
|
| 32 |
+
h = inputs_embeds
|
| 33 |
+
for i, layer in enumerate(self.layers):
|
| 34 |
+
h = layer(h, start_pos=start_pos,
|
| 35 |
+
cache=caches[i] if caches is not None else None,
|
| 36 |
+
conv_mask=conv_mask)
|
| 37 |
+
return self.norm(h)
|
| 38 |
+
|
| 39 |
+
def logits(self, hidden: mx.array) -> mx.array:
|
| 40 |
+
hidden = hidden / self.config.logits_mup_width_multiplier
|
| 41 |
+
logits = self.unembed(hidden)
|
| 42 |
+
uv = self.config.unpadded_vocab_size
|
| 43 |
+
if uv is not None and uv < logits.shape[-1]:
|
| 44 |
+
logits = logits[..., :uv]
|
| 45 |
+
return logits
|
| 46 |
+
|
| 47 |
+
def __call__(self, input_ids: mx.array, conv_mask=None) -> mx.array:
|
| 48 |
+
h = self.embed_tokens(input_ids)
|
| 49 |
+
h = self.backbone(h, conv_mask=conv_mask)
|
| 50 |
+
return self.logits(h)
|
inkling_mlx/vision.py
ADDED
|
@@ -0,0 +1,126 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Inkling vision tower: an HMLP (hierarchical MLP) patch encoder.
|
| 2 |
+
|
| 3 |
+
No attention — each layer folds space/time into the channel dim then projects
|
| 4 |
+
(Linear -> RMSNorm -> GELU), progressively growing channels up to the text hidden
|
| 5 |
+
size. Mirrors ``InklingVisionModel`` / ``InklingVisionEncoderLayer`` /
|
| 6 |
+
``plan_out_scales``. Checkpoint keys are flat: ``visual.layers.linear_{i}`` and
|
| 7 |
+
``visual.layers.norm_{i}`` plus ``visual.final_norm``.
|
| 8 |
+
"""
|
| 9 |
+
|
| 10 |
+
from __future__ import annotations
|
| 11 |
+
|
| 12 |
+
import math
|
| 13 |
+
|
| 14 |
+
import mlx.core as mx
|
| 15 |
+
import mlx.nn as nn
|
| 16 |
+
|
| 17 |
+
from .common import RMSNorm
|
| 18 |
+
from .config import VisionConfig
|
| 19 |
+
|
| 20 |
+
|
| 21 |
+
def _prime_factors(n: int) -> list[int]:
|
| 22 |
+
factors = []
|
| 23 |
+
while n % 2 == 0:
|
| 24 |
+
factors.append(2)
|
| 25 |
+
n //= 2
|
| 26 |
+
p = 3
|
| 27 |
+
while p * p <= n:
|
| 28 |
+
while n % p == 0:
|
| 29 |
+
factors.append(p)
|
| 30 |
+
n //= p
|
| 31 |
+
p += 2
|
| 32 |
+
if n > 1:
|
| 33 |
+
factors.append(n)
|
| 34 |
+
return factors
|
| 35 |
+
|
| 36 |
+
|
| 37 |
+
def plan_out_scales(temporal_patch_size: int, patch_size: int, n_layers: int, n_channels: int):
|
| 38 |
+
"""Port of the reference ``plan_out_scales`` (returns an ``(n_layers+1, 4)``
|
| 39 |
+
array of (t, h, w, c) grid sizes). Uses numpy + scipy for the assignment."""
|
| 40 |
+
import numpy as np
|
| 41 |
+
from scipy.optimize import linear_sum_assignment
|
| 42 |
+
|
| 43 |
+
h = np.cumprod(np.array(_prime_factors(patch_size)[::-1]))
|
| 44 |
+
t = np.cumprod(np.array(_prime_factors(temporal_patch_size)[::-1]))
|
| 45 |
+
|
| 46 |
+
h_ch = np.ceil(h**2 * n_channels / 64).astype(np.int64) * 64
|
| 47 |
+
t_ch = (np.ceil(h[-1] ** 2 * n_channels * t)).astype(np.int64) * 64
|
| 48 |
+
|
| 49 |
+
base = np.array([[1, 1, 1, n_channels]], dtype=np.int64)
|
| 50 |
+
spatial = np.stack([np.ones_like(h), h, h, h_ch], axis=1)
|
| 51 |
+
temporal = np.stack([t, np.full_like(t, h[-1]), np.full_like(t, h[-1]), t_ch], axis=1)
|
| 52 |
+
scales = np.concatenate([base, spatial, temporal], axis=0).astype(np.int64)
|
| 53 |
+
|
| 54 |
+
size_reduction = np.prod(scales[:, :-1], axis=1).astype(np.float64)
|
| 55 |
+
total_elements = patch_size * patch_size * temporal_patch_size * n_channels
|
| 56 |
+
log_ideal = np.linspace(0.0, math.log(total_elements), n_layers + 1)
|
| 57 |
+
cost = np.abs(log_ideal[:, None] - np.log(size_reduction)[None, :])
|
| 58 |
+
|
| 59 |
+
if n_layers >= scales.shape[0]:
|
| 60 |
+
idxs = np.argmin(cost, axis=1)
|
| 61 |
+
else:
|
| 62 |
+
_, idxs = linear_sum_assignment(cost)
|
| 63 |
+
idxs = np.array(idxs)
|
| 64 |
+
idxs[0] = 0
|
| 65 |
+
idxs[-1] = scales.shape[0] - 1
|
| 66 |
+
return scales[idxs]
|
| 67 |
+
|
| 68 |
+
|
| 69 |
+
def _fold_timespace_to_depth(x, t_fold, hw_fold):
|
| 70 |
+
# x: [B, T, H, W, C] -> [B, T//t, H//hw, W//hw, C*t*hw*hw]
|
| 71 |
+
B, T, H, W, C = x.shape
|
| 72 |
+
t_new, h_new, w_new = T // t_fold, H // hw_fold, W // hw_fold
|
| 73 |
+
x = x.reshape(B, t_new, t_fold, h_new, hw_fold, w_new, hw_fold, C)
|
| 74 |
+
x = x.transpose(0, 1, 3, 5, 2, 4, 6, 7)
|
| 75 |
+
x = x.reshape(B, t_new, h_new, w_new, t_fold * hw_fold * hw_fold * C)
|
| 76 |
+
return x
|
| 77 |
+
|
| 78 |
+
|
| 79 |
+
class _VisionLayers(nn.Module):
|
| 80 |
+
"""Holds ``linear_{i}`` / ``norm_{i}`` to match checkpoint keys."""
|
| 81 |
+
|
| 82 |
+
def __init__(self, config: VisionConfig):
|
| 83 |
+
super().__init__()
|
| 84 |
+
scales = plan_out_scales(
|
| 85 |
+
config.temporal_patch_size, config.patch_size, config.n_layers, config.num_channels
|
| 86 |
+
)
|
| 87 |
+
self.n_layers = config.n_layers
|
| 88 |
+
self.folds = [] # (t_fold, hw_fold, add_norm)
|
| 89 |
+
for i in range(config.n_layers):
|
| 90 |
+
start, end = scales[i], scales[i + 1]
|
| 91 |
+
shuffle = (
|
| 92 |
+
(end[0] // start[0]) * (end[1] // start[1]) * (end[2] // start[2])
|
| 93 |
+
)
|
| 94 |
+
hw_fold = int(end[1] // start[1])
|
| 95 |
+
t_fold = int(end[0] // start[0])
|
| 96 |
+
in_dim = int(start[3]) * int(shuffle)
|
| 97 |
+
add_norm = i != config.n_layers - 1
|
| 98 |
+
out_dim = config.text_hidden_size if i == config.n_layers - 1 else int(end[3])
|
| 99 |
+
setattr(self, f"linear_{i}", nn.Linear(in_dim, out_dim, bias=False))
|
| 100 |
+
if add_norm:
|
| 101 |
+
setattr(self, f"norm_{i}", RMSNorm(out_dim, eps=config.rms_norm_eps))
|
| 102 |
+
self.folds.append((t_fold, hw_fold, add_norm))
|
| 103 |
+
|
| 104 |
+
def __call__(self, x):
|
| 105 |
+
for i, (t_fold, hw_fold, add_norm) in enumerate(self.folds):
|
| 106 |
+
if hw_fold > 1 or t_fold > 1:
|
| 107 |
+
x = _fold_timespace_to_depth(x, t_fold, hw_fold)
|
| 108 |
+
x = getattr(self, f"linear_{i}")(x)
|
| 109 |
+
if add_norm:
|
| 110 |
+
x = getattr(self, f"norm_{i}")(x)
|
| 111 |
+
x = nn.gelu(x)
|
| 112 |
+
return x
|
| 113 |
+
|
| 114 |
+
|
| 115 |
+
class VisionModel(nn.Module):
|
| 116 |
+
def __init__(self, config: VisionConfig):
|
| 117 |
+
super().__init__()
|
| 118 |
+
self.config = config
|
| 119 |
+
self.layers = _VisionLayers(config)
|
| 120 |
+
self.final_norm = RMSNorm(config.text_hidden_size, eps=config.rms_norm_eps)
|
| 121 |
+
|
| 122 |
+
def __call__(self, pixel_values: mx.array) -> mx.array:
|
| 123 |
+
num_patches = pixel_values.shape[0]
|
| 124 |
+
h = self.layers(pixel_values)
|
| 125 |
+
h = self.final_norm(h)
|
| 126 |
+
return h.reshape(num_patches, -1)
|
model-00001-of-00023.safetensors
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:9e42734850bdcdbaeedb40b04ec4d5602368fddd11099b5e42398995d986bf21
|
| 3 |
+
size 5594332943
|
model-00002-of-00023.safetensors
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:ddfaeba71d13110153129d30e626b31a68c73f39360761d9653670969a3f663f
|
| 3 |
+
size 5000887318
|
model-00003-of-00023.safetensors
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:9f5c6478694d5c2dea4165f0c618257d28881261a309264e2263a50eb8a16d2c
|
| 3 |
+
size 5580625118
|
model-00004-of-00023.safetensors
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:bc55b82655c224201a5b54faf3a85d4b9ad36d5ebf9bf5634724b89d332d71c5
|
| 3 |
+
size 5035610204
|
model-00005-of-00023.safetensors
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:b521d9ee213d4e60cb7bfd213fc626cd0b0ea442b7fabe8fe80aa3c36fd80e65
|
| 3 |
+
size 5057678969
|
model-00006-of-00023.safetensors
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:4759bad1e4af29df75916ca01bea2368204cbe48227edb7486e21663dedc4164
|
| 3 |
+
size 5033513658
|
model-00007-of-00023.safetensors
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:a4c8e4016a34b788ff9f72367c299dd5a7c06ce269199685bce9532b49b0b52d
|
| 3 |
+
size 5011868077
|
model-00008-of-00023.safetensors
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:a21f18fad8207cf2c5f7d9acd2888a15b9247ab12d8c29df1f812fc1e048bea9
|
| 3 |
+
size 5019857728
|
model-00009-of-00023.safetensors
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:900765f2db646433da17ffbe77e1153ae7b699a178d9ef41f2d6855cb4f7d4cf
|
| 3 |
+
size 5001781520
|
model-00010-of-00023.safetensors
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:ee46ca2cdda187a133021be9ada09039acb309328708d608575610634cc4ee46
|
| 3 |
+
size 5661531108
|
model-00011-of-00023.safetensors
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:d5fb7c4b974ecb52ced7cbffb816f50c892012e04b53584393ee73cce913497b
|
| 3 |
+
size 5795660286
|
model-00012-of-00023.safetensors
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:c843327ff1a2abcf69d38324ae46b370f806780560981f2f68e84a717e9d3aa0
|
| 3 |
+
size 5039732129
|
model-00013-of-00023.safetensors
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:1c1f97a0adf24967d92988f60a4c2cf110a6d22340380c7fce056f5ed7dd70d7
|
| 3 |
+
size 5617638322
|
model-00014-of-00023.safetensors
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:81fa417091e2a5472c93d94d51c4dfab6b46d50542a769cab034946cae19181d
|
| 3 |
+
size 5628885415
|
model-00015-of-00023.safetensors
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:69bc0de3d94e5bd99b18154c4eb1c1228bb3dd51f55270e141cdf17bc2c670e9
|
| 3 |
+
size 5267393483
|
model-00016-of-00023.safetensors
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:52712f0e018f906c2a2e0406a39f8d4d3e0e380145739551731370385698019c
|
| 3 |
+
size 5009388976
|
model-00017-of-00023.safetensors
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:fe1c46b6d45fa427ebe4c8852e511f961a42aa1e5c320127d685d9f051bd7373
|
| 3 |
+
size 5650057900
|
model-00018-of-00023.safetensors
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:e2f96c8ac308f6027252bfe0b3369841948c8048473f3dac6bf0398e60562b99
|
| 3 |
+
size 5060808586
|
model-00019-of-00023.safetensors
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:4040ecd3725b5ce786fbd708e1686295d27cce7921868c134a613843f38b8f0a
|
| 3 |
+
size 5797784251
|
model-00020-of-00023.safetensors
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:52639817b466372de4e030cfd76c17ff574d643611e0e4dbf97b871da37148f7
|
| 3 |
+
size 5031172621
|
model-00021-of-00023.safetensors
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:2b6a1d3b7894320c5c07388c9bccbf106f21d452e055c5bd0194d0f2e891935e
|
| 3 |
+
size 5281288361
|
model-00022-of-00023.safetensors
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:4c03d985bd8761e409f8f40ad5cc76c048a4a7f7c767484bd7ae8d24a9319d61
|
| 3 |
+
size 5683582825
|
model-00023-of-00023.safetensors
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:45dbe282191d5afeec100f4e6826fc556d8738e8b077555a089cb99ac0021633
|
| 3 |
+
size 4051827493
|
model.safetensors.index.json
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
processor_config.json
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"audio_token": "<|unused_200053|>",
|
| 3 |
+
"audio_bos_token": "<|content_audio_input|>",
|
| 4 |
+
"dmel_max_value": 2.0,
|
| 5 |
+
"dmel_min_value": -7.0,
|
| 6 |
+
"feature_extractor": {
|
| 7 |
+
"audio_token_duration_s": 0.05,
|
| 8 |
+
"feature_extractor_type": "InklingFeatureExtractor",
|
| 9 |
+
"feature_size": 80,
|
| 10 |
+
"hop_length": 800,
|
| 11 |
+
"n_fft": 1600,
|
| 12 |
+
"padding_side": "right",
|
| 13 |
+
"padding_value": 0.0,
|
| 14 |
+
"return_attention_mask": true,
|
| 15 |
+
"sampling_rate": 16000,
|
| 16 |
+
"window_size": 1600,
|
| 17 |
+
"window_size_multiplier": 2.0
|
| 18 |
+
},
|
| 19 |
+
"image_processor": {
|
| 20 |
+
"do_convert_rgb": true,
|
| 21 |
+
"do_normalize": true,
|
| 22 |
+
"do_rescale": true,
|
| 23 |
+
"do_resize": true,
|
| 24 |
+
"image_mean": [
|
| 25 |
+
0.48145466,
|
| 26 |
+
0.4578275,
|
| 27 |
+
0.40821073
|
| 28 |
+
],
|
| 29 |
+
"image_processor_type": "InklingImageProcessor",
|
| 30 |
+
"image_std": [
|
| 31 |
+
0.26862954,
|
| 32 |
+
0.26130258,
|
| 33 |
+
0.27577711
|
| 34 |
+
],
|
| 35 |
+
"resample": 3,
|
| 36 |
+
"rescale_factor": 0.00392156862745098,
|
| 37 |
+
"size": {
|
| 38 |
+
"height": 40,
|
| 39 |
+
"width": 40
|
| 40 |
+
}
|
| 41 |
+
},
|
| 42 |
+
"image_token": "<|unused_200054|>",
|
| 43 |
+
"image_bos_token": "<|content_image|>",
|
| 44 |
+
"num_dmel_bins": 16,
|
| 45 |
+
"processor_class": "InklingProcessor"
|
| 46 |
+
}
|
special_tokens_map.json
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"additional_special_tokens": [
|
| 3 |
+
"<|endoftext|>",
|
| 4 |
+
"<|message_user|>",
|
| 5 |
+
"<|message_model|>",
|
| 6 |
+
"<|message_system|>",
|
| 7 |
+
"<|message_tool|>",
|
| 8 |
+
"<|content_text|>",
|
| 9 |
+
"<|content_image|>",
|
| 10 |
+
"<|content_model_end_sampling|>",
|
| 11 |
+
"<|content_thinking|>",
|
| 12 |
+
"<|end_message|>",
|
| 13 |
+
"<|content_audio_input|>",
|
| 14 |
+
"<|content_tool_error|>",
|
| 15 |
+
"<|audio|>",
|
| 16 |
+
"<|content_xml|>",
|
| 17 |
+
"<|begin_of_text|>",
|
| 18 |
+
"<|audio_end|>",
|
| 19 |
+
"<|content_invoke_tool_json|>",
|
| 20 |
+
"<|content_invoke_tool_text|>"
|
| 21 |
+
]
|
| 22 |
+
}
|
tiktoken/tokenizer.model
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:bc253fd2b702f7a6da7105eaa8f3463b2f1247e83614f23e5323b921088bed2a
|
| 3 |
+
size 3615874
|
tokenizer.json
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:9fb6333a7db8fe5da90728e741e4a3ee4ac2ae12c5dd4958cc6f31688787d3c2
|
| 3 |
+
size 27875797
|