d3 Core ML: lite (vllm-sr Decision 3.0 v3.1.0)
Browse files- .gitattributes +4 -0
- lite/README.md +50 -0
- lite/config.json +116 -0
- lite/coreml_config.json +312 -0
- lite/d3_E1536.mlpackage/Data/com.apple.CoreML/model.mlmodel +3 -0
- lite/d3_E1536.mlpackage/Data/com.apple.CoreML/weights/weight.bin +3 -0
- lite/d3_E1536.mlpackage/Manifest.json +18 -0
- lite/d3_E256.mlpackage/Data/com.apple.CoreML/model.mlmodel +3 -0
- lite/d3_E256.mlpackage/Data/com.apple.CoreML/weights/weight.bin +3 -0
- lite/d3_E256.mlpackage/Manifest.json +18 -0
- lite/d3_E640.mlpackage/Data/com.apple.CoreML/model.mlmodel +3 -0
- lite/d3_E640.mlpackage/Data/com.apple.CoreML/weights/weight.bin +3 -0
- lite/d3_E640.mlpackage/Manifest.json +18 -0
- lite/embeddings.f16 +3 -0
- lite/pos_embed.f32 +3 -0
- lite/python/d3_coreml_image.py +128 -0
- lite/python/d3_coreml_text.py +80 -0
- lite/python/d3_coreml_video.py +122 -0
- lite/python/d3_graph.py +120 -0
- lite/python/qwen35_export.py +333 -0
- lite/python/vision_export.py +230 -0
- lite/readout.f32 +3 -0
- lite/tokenizer.json +3 -0
- lite/vision_P1536.mlpackage/Data/com.apple.CoreML/model.mlmodel +3 -0
- lite/vision_P1536.mlpackage/Data/com.apple.CoreML/weights/weight.bin +3 -0
- lite/vision_P1536.mlpackage/Manifest.json +18 -0
- lite/vision_P3072.mlpackage/Data/com.apple.CoreML/model.mlmodel +3 -0
- lite/vision_P3072.mlpackage/Data/com.apple.CoreML/weights/weight.bin +3 -0
- lite/vision_P3072.mlpackage/Manifest.json +18 -0
- lite/vision_P320.mlpackage/Data/com.apple.CoreML/model.mlmodel +3 -0
- lite/vision_P320.mlpackage/Data/com.apple.CoreML/weights/weight.bin +3 -0
- lite/vision_P320.mlpackage/Manifest.json +18 -0
.gitattributes
CHANGED
|
@@ -33,3 +33,7 @@ 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 |
+
lite/embeddings.f16 filter=lfs diff=lfs merge=lfs -text
|
| 37 |
+
lite/pos_embed.f32 filter=lfs diff=lfs merge=lfs -text
|
| 38 |
+
lite/readout.f32 filter=lfs diff=lfs merge=lfs -text
|
| 39 |
+
lite/tokenizer.json filter=lfs diff=lfs merge=lfs -text
|
lite/README.md
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
license: apache-2.0
|
| 3 |
+
base_model: vllm-sr/d3-lite
|
| 4 |
+
library_name: coreml
|
| 5 |
+
pipeline_tag: zero-shot-classification
|
| 6 |
+
tags:
|
| 7 |
+
- coreml
|
| 8 |
+
- apple-silicon
|
| 9 |
+
- decision-model
|
| 10 |
+
- vision
|
| 11 |
+
- video
|
| 12 |
+
---
|
| 13 |
+
|
| 14 |
+
# d3-lite — Core ML
|
| 15 |
+
|
| 16 |
+
[vllm-sr/d3-lite](https://huggingface.co/vllm-sr/d3-lite) (0.85B, part of [Decision 3.0](https://huggingface.co/collections/vllm-sr/decision-30) by the vLLM Semantic Router team) converted to Core ML for Apple silicon: text, image and video requests on device. Same contract as upstream: one forward pass per question, the 255-way FP32 readout at the last prompt token, a probability for every option, no text generated.
|
| 17 |
+
|
| 18 |
+
## Files
|
| 19 |
+
|
| 20 |
+
| File | What |
|
| 21 |
+
|---|---|
|
| 22 |
+
| `d3_E256` / `d3_E640` / `d3_E1536.mlpackage` | text graph (fp16 compute, int8 weights) per prompt-length bucket; input = token embeddings with image/video tokens spliced in |
|
| 23 |
+
| `vision_P320` / `vision_P1536` / `vision_P3072.mlpackage` | vision tower (fp32) per patch bucket; video frames go through it one frame pair at a time (block mask) |
|
| 24 |
+
| `embeddings.f16`, `pos_embed.f32`, `readout.f32`, `tokenizer.json`, `config.json`, `coreml_config.json` | host-side tables and config |
|
| 25 |
+
| `python/` | Python host (coremltools): `d3_coreml_text.py`, `d3_coreml_image.py`, `d3_coreml_video.py` |
|
| 26 |
+
|
| 27 |
+
Swift: `D3VisionManager` in [FluidUse](https://github.com/FluidInference/FluidUse) loads this folder.
|
| 28 |
+
|
| 29 |
+
## Checked against upstream
|
| 30 |
+
|
| 31 |
+
Same weights as upstream v3.1.0. Answers that differ from the upstream PyTorch runtime (bf16, MPS), same requests:
|
| 32 |
+
|
| 33 |
+
| | flips |
|
| 34 |
+
|---|---|
|
| 35 |
+
| Text (typed-decisions TEST, 20 requests) | 2 / 100 |
|
| 36 |
+
| Images (receipt checks) | 1 / 24 |
|
| 37 |
+
| Video (4-second street clips, 2 fps) | 0 / 28 |
|
| 38 |
+
|
| 39 |
+
Speed on an M5 Pro (24 GB), `CPU_AND_GPU`: ~46 ms per text question, 0.32 s per 4-second video look.
|
| 40 |
+
|
| 41 |
+
## Python
|
| 42 |
+
|
| 43 |
+
```python
|
| 44 |
+
from d3_coreml_text import D3CoreMLText # d3_coreml_image.D3CoreMLImage, d3_coreml_video.D3CoreMLVideo
|
| 45 |
+
m = D3CoreMLText("<vllm-sr/d3-lite snapshot>", "<this repo>")
|
| 46 |
+
m.system_one(state="...", questions={"route": {"type": "choice", "criteria": {...}}})
|
| 47 |
+
```
|
| 48 |
+
The Python host builds prompts with upstream's own runtime (`d3_runtime.py` in the upstream snapshot, run without weights), so prompts match upstream exactly.
|
| 49 |
+
|
| 50 |
+
License: Apache-2.0, as upstream.
|
lite/config.json
ADDED
|
@@ -0,0 +1,116 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"architectures": [
|
| 3 |
+
"Qwen3_5Model"
|
| 4 |
+
],
|
| 5 |
+
"dtype": "bfloat16",
|
| 6 |
+
"image_token_id": 248056,
|
| 7 |
+
"model_type": "qwen3_5",
|
| 8 |
+
"text_config": {
|
| 9 |
+
"attention_bias": false,
|
| 10 |
+
"attention_dropout": 0.0,
|
| 11 |
+
"attn_output_gate": true,
|
| 12 |
+
"bos_token_id": null,
|
| 13 |
+
"dtype": "bfloat16",
|
| 14 |
+
"eos_token_id": 248044,
|
| 15 |
+
"full_attention_interval": 4,
|
| 16 |
+
"head_dim": 256,
|
| 17 |
+
"hidden_act": "silu",
|
| 18 |
+
"hidden_size": 1024,
|
| 19 |
+
"initializer_range": 0.02,
|
| 20 |
+
"intermediate_size": 3584,
|
| 21 |
+
"layer_types": [
|
| 22 |
+
"linear_attention",
|
| 23 |
+
"linear_attention",
|
| 24 |
+
"linear_attention",
|
| 25 |
+
"full_attention",
|
| 26 |
+
"linear_attention",
|
| 27 |
+
"linear_attention",
|
| 28 |
+
"linear_attention",
|
| 29 |
+
"full_attention",
|
| 30 |
+
"linear_attention",
|
| 31 |
+
"linear_attention",
|
| 32 |
+
"linear_attention",
|
| 33 |
+
"full_attention",
|
| 34 |
+
"linear_attention",
|
| 35 |
+
"linear_attention",
|
| 36 |
+
"linear_attention",
|
| 37 |
+
"full_attention",
|
| 38 |
+
"linear_attention",
|
| 39 |
+
"linear_attention",
|
| 40 |
+
"linear_attention",
|
| 41 |
+
"full_attention",
|
| 42 |
+
"linear_attention",
|
| 43 |
+
"linear_attention",
|
| 44 |
+
"linear_attention",
|
| 45 |
+
"full_attention"
|
| 46 |
+
],
|
| 47 |
+
"linear_conv_kernel_dim": 4,
|
| 48 |
+
"linear_key_head_dim": 128,
|
| 49 |
+
"linear_num_key_heads": 16,
|
| 50 |
+
"linear_num_value_heads": 16,
|
| 51 |
+
"linear_value_head_dim": 128,
|
| 52 |
+
"mamba_ssm_dtype": "float32",
|
| 53 |
+
"max_position_embeddings": 262144,
|
| 54 |
+
"mlp_only_layers": [],
|
| 55 |
+
"model_type": "qwen3_5_text",
|
| 56 |
+
"mtp_num_hidden_layers": 1,
|
| 57 |
+
"mtp_use_dedicated_embeddings": false,
|
| 58 |
+
"num_attention_heads": 8,
|
| 59 |
+
"num_hidden_layers": 24,
|
| 60 |
+
"num_key_value_heads": 2,
|
| 61 |
+
"pad_token_id": null,
|
| 62 |
+
"partial_rotary_factor": 0.25,
|
| 63 |
+
"rms_norm_eps": 1e-06,
|
| 64 |
+
"rope_parameters": {
|
| 65 |
+
"mrope_interleaved": true,
|
| 66 |
+
"mrope_section": [
|
| 67 |
+
11,
|
| 68 |
+
11,
|
| 69 |
+
10
|
| 70 |
+
],
|
| 71 |
+
"partial_rotary_factor": 0.25,
|
| 72 |
+
"rope_theta": 10000000,
|
| 73 |
+
"rope_type": "default"
|
| 74 |
+
},
|
| 75 |
+
"tie_word_embeddings": true,
|
| 76 |
+
"use_cache": true,
|
| 77 |
+
"vocab_size": 248320
|
| 78 |
+
},
|
| 79 |
+
"tie_word_embeddings": true,
|
| 80 |
+
"transformers_version": "5.17.0",
|
| 81 |
+
"video_token_id": 248057,
|
| 82 |
+
"vision_config": {
|
| 83 |
+
"deepstack_visual_indexes": [],
|
| 84 |
+
"depth": 12,
|
| 85 |
+
"hidden_act": "gelu_pytorch_tanh",
|
| 86 |
+
"hidden_size": 768,
|
| 87 |
+
"in_channels": 3,
|
| 88 |
+
"initializer_range": 0.02,
|
| 89 |
+
"intermediate_size": 3072,
|
| 90 |
+
"model_type": "qwen3_5_vision",
|
| 91 |
+
"num_heads": 12,
|
| 92 |
+
"num_position_embeddings": 2304,
|
| 93 |
+
"out_hidden_size": 1024,
|
| 94 |
+
"patch_size": 16,
|
| 95 |
+
"rope_parameters": {
|
| 96 |
+
"rope_theta": 10000.0,
|
| 97 |
+
"rope_type": "axial"
|
| 98 |
+
},
|
| 99 |
+
"spatial_merge_size": 2,
|
| 100 |
+
"temporal_patch_size": 2
|
| 101 |
+
},
|
| 102 |
+
"vision_end_token_id": 248054,
|
| 103 |
+
"vision_start_token_id": 248053,
|
| 104 |
+
"auto_map": {
|
| 105 |
+
"AutoModel": "modeling_d3.D3Model"
|
| 106 |
+
},
|
| 107 |
+
"custom_pipelines": {
|
| 108 |
+
"decision": {
|
| 109 |
+
"impl": "pipeline_d3.D3Pipeline",
|
| 110 |
+
"pt": [
|
| 111 |
+
"AutoModel"
|
| 112 |
+
],
|
| 113 |
+
"type": "text"
|
| 114 |
+
}
|
| 115 |
+
}
|
| 116 |
+
}
|
lite/coreml_config.json
ADDED
|
@@ -0,0 +1,312 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"model_name": "lite",
|
| 3 |
+
"source": "vllm-sr/lite",
|
| 4 |
+
"codes": [
|
| 5 |
+
"A",
|
| 6 |
+
"B",
|
| 7 |
+
"C",
|
| 8 |
+
"D",
|
| 9 |
+
"E",
|
| 10 |
+
"F",
|
| 11 |
+
"G",
|
| 12 |
+
"H",
|
| 13 |
+
"I",
|
| 14 |
+
"J",
|
| 15 |
+
"K",
|
| 16 |
+
"L",
|
| 17 |
+
"M",
|
| 18 |
+
"N",
|
| 19 |
+
"O",
|
| 20 |
+
"P",
|
| 21 |
+
"Q",
|
| 22 |
+
"R",
|
| 23 |
+
"S",
|
| 24 |
+
"T",
|
| 25 |
+
"U",
|
| 26 |
+
"V",
|
| 27 |
+
"W",
|
| 28 |
+
"X",
|
| 29 |
+
"Y",
|
| 30 |
+
"Z",
|
| 31 |
+
"AA",
|
| 32 |
+
"AB",
|
| 33 |
+
"AC",
|
| 34 |
+
"AD",
|
| 35 |
+
"AE",
|
| 36 |
+
"AF",
|
| 37 |
+
"AG",
|
| 38 |
+
"AH",
|
| 39 |
+
"AI",
|
| 40 |
+
"AJ",
|
| 41 |
+
"AK",
|
| 42 |
+
"AL",
|
| 43 |
+
"AM",
|
| 44 |
+
"AN",
|
| 45 |
+
"AO",
|
| 46 |
+
"AP",
|
| 47 |
+
"AQ",
|
| 48 |
+
"AR",
|
| 49 |
+
"AS",
|
| 50 |
+
"AT",
|
| 51 |
+
"AU",
|
| 52 |
+
"AV",
|
| 53 |
+
"AW",
|
| 54 |
+
"AX",
|
| 55 |
+
"AY",
|
| 56 |
+
"AZ",
|
| 57 |
+
"BA",
|
| 58 |
+
"BB",
|
| 59 |
+
"BC",
|
| 60 |
+
"BD",
|
| 61 |
+
"BE",
|
| 62 |
+
"BF",
|
| 63 |
+
"BG",
|
| 64 |
+
"BH",
|
| 65 |
+
"BI",
|
| 66 |
+
"BJ",
|
| 67 |
+
"BK",
|
| 68 |
+
"BL",
|
| 69 |
+
"BM",
|
| 70 |
+
"BN",
|
| 71 |
+
"BO",
|
| 72 |
+
"BP",
|
| 73 |
+
"BR",
|
| 74 |
+
"BS",
|
| 75 |
+
"BT",
|
| 76 |
+
"BU",
|
| 77 |
+
"BV",
|
| 78 |
+
"BW",
|
| 79 |
+
"BX",
|
| 80 |
+
"BY",
|
| 81 |
+
"CA",
|
| 82 |
+
"CB",
|
| 83 |
+
"CC",
|
| 84 |
+
"CD",
|
| 85 |
+
"CE",
|
| 86 |
+
"CF",
|
| 87 |
+
"CG",
|
| 88 |
+
"CH",
|
| 89 |
+
"CI",
|
| 90 |
+
"CK",
|
| 91 |
+
"CL",
|
| 92 |
+
"CM",
|
| 93 |
+
"CN",
|
| 94 |
+
"CO",
|
| 95 |
+
"CP",
|
| 96 |
+
"CR",
|
| 97 |
+
"CS",
|
| 98 |
+
"CT",
|
| 99 |
+
"CU",
|
| 100 |
+
"CV",
|
| 101 |
+
"CW",
|
| 102 |
+
"CX",
|
| 103 |
+
"CY",
|
| 104 |
+
"DA",
|
| 105 |
+
"DB",
|
| 106 |
+
"DC",
|
| 107 |
+
"DD",
|
| 108 |
+
"DE",
|
| 109 |
+
"DF",
|
| 110 |
+
"DG",
|
| 111 |
+
"DH",
|
| 112 |
+
"DI",
|
| 113 |
+
"DJ",
|
| 114 |
+
"DK",
|
| 115 |
+
"DL",
|
| 116 |
+
"DM",
|
| 117 |
+
"DN",
|
| 118 |
+
"DO",
|
| 119 |
+
"DP",
|
| 120 |
+
"DR",
|
| 121 |
+
"DS",
|
| 122 |
+
"DT",
|
| 123 |
+
"DU",
|
| 124 |
+
"DV",
|
| 125 |
+
"DW",
|
| 126 |
+
"DX",
|
| 127 |
+
"DY",
|
| 128 |
+
"EA",
|
| 129 |
+
"EB",
|
| 130 |
+
"EC",
|
| 131 |
+
"ED",
|
| 132 |
+
"EE",
|
| 133 |
+
"EF",
|
| 134 |
+
"EG",
|
| 135 |
+
"EH",
|
| 136 |
+
"EI",
|
| 137 |
+
"EK",
|
| 138 |
+
"EL",
|
| 139 |
+
"EM",
|
| 140 |
+
"EN",
|
| 141 |
+
"EO",
|
| 142 |
+
"EP",
|
| 143 |
+
"EQ",
|
| 144 |
+
"ER",
|
| 145 |
+
"ES",
|
| 146 |
+
"ET",
|
| 147 |
+
"EU",
|
| 148 |
+
"EV",
|
| 149 |
+
"EW",
|
| 150 |
+
"EX",
|
| 151 |
+
"EZ",
|
| 152 |
+
"FA",
|
| 153 |
+
"FB",
|
| 154 |
+
"FC",
|
| 155 |
+
"FD",
|
| 156 |
+
"FE",
|
| 157 |
+
"FF",
|
| 158 |
+
"FG",
|
| 159 |
+
"FH",
|
| 160 |
+
"FI",
|
| 161 |
+
"FK",
|
| 162 |
+
"FL",
|
| 163 |
+
"FM",
|
| 164 |
+
"FN",
|
| 165 |
+
"FO",
|
| 166 |
+
"FP",
|
| 167 |
+
"FR",
|
| 168 |
+
"FS",
|
| 169 |
+
"FT",
|
| 170 |
+
"FU",
|
| 171 |
+
"FW",
|
| 172 |
+
"FX",
|
| 173 |
+
"FY",
|
| 174 |
+
"GA",
|
| 175 |
+
"GB",
|
| 176 |
+
"GC",
|
| 177 |
+
"GD",
|
| 178 |
+
"GE",
|
| 179 |
+
"GF",
|
| 180 |
+
"GG",
|
| 181 |
+
"GH",
|
| 182 |
+
"GI",
|
| 183 |
+
"GL",
|
| 184 |
+
"GM",
|
| 185 |
+
"GN",
|
| 186 |
+
"GO",
|
| 187 |
+
"GP",
|
| 188 |
+
"GR",
|
| 189 |
+
"GS",
|
| 190 |
+
"GT",
|
| 191 |
+
"GU",
|
| 192 |
+
"GV",
|
| 193 |
+
"GW",
|
| 194 |
+
"GX",
|
| 195 |
+
"GY",
|
| 196 |
+
"HA",
|
| 197 |
+
"HB",
|
| 198 |
+
"HC",
|
| 199 |
+
"HD",
|
| 200 |
+
"HE",
|
| 201 |
+
"HF",
|
| 202 |
+
"HG",
|
| 203 |
+
"HH",
|
| 204 |
+
"HI",
|
| 205 |
+
"HK",
|
| 206 |
+
"HL",
|
| 207 |
+
"HM",
|
| 208 |
+
"HN",
|
| 209 |
+
"HO",
|
| 210 |
+
"HP",
|
| 211 |
+
"HQ",
|
| 212 |
+
"HR",
|
| 213 |
+
"HS",
|
| 214 |
+
"HT",
|
| 215 |
+
"HU",
|
| 216 |
+
"HV",
|
| 217 |
+
"HW",
|
| 218 |
+
"HX",
|
| 219 |
+
"HY",
|
| 220 |
+
"HZ",
|
| 221 |
+
"IA",
|
| 222 |
+
"IB",
|
| 223 |
+
"IC",
|
| 224 |
+
"ID",
|
| 225 |
+
"IE",
|
| 226 |
+
"IF",
|
| 227 |
+
"IG",
|
| 228 |
+
"IH",
|
| 229 |
+
"II",
|
| 230 |
+
"IJ",
|
| 231 |
+
"IK",
|
| 232 |
+
"IL",
|
| 233 |
+
"IM",
|
| 234 |
+
"IN",
|
| 235 |
+
"IO",
|
| 236 |
+
"IP",
|
| 237 |
+
"IQ",
|
| 238 |
+
"IR",
|
| 239 |
+
"IS",
|
| 240 |
+
"IT",
|
| 241 |
+
"IU",
|
| 242 |
+
"IV",
|
| 243 |
+
"IW",
|
| 244 |
+
"IX",
|
| 245 |
+
"IZ",
|
| 246 |
+
"JA",
|
| 247 |
+
"JB",
|
| 248 |
+
"JC",
|
| 249 |
+
"JD",
|
| 250 |
+
"JE",
|
| 251 |
+
"JI",
|
| 252 |
+
"JJ",
|
| 253 |
+
"JK",
|
| 254 |
+
"JM",
|
| 255 |
+
"JO",
|
| 256 |
+
"JP",
|
| 257 |
+
"JR",
|
| 258 |
+
"JS",
|
| 259 |
+
"JT"
|
| 260 |
+
],
|
| 261 |
+
"temperature": 1.0,
|
| 262 |
+
"pad_token_id": 248044,
|
| 263 |
+
"image_token_id": 248056,
|
| 264 |
+
"vision_start_token_id": 248053,
|
| 265 |
+
"vision_end_token_id": 248054,
|
| 266 |
+
"hidden_size": 1024,
|
| 267 |
+
"vocab_size": 248320,
|
| 268 |
+
"rope": {
|
| 269 |
+
"theta": 10000000,
|
| 270 |
+
"rotary_dim": 64,
|
| 271 |
+
"mrope_section": [
|
| 272 |
+
11,
|
| 273 |
+
11,
|
| 274 |
+
10
|
| 275 |
+
]
|
| 276 |
+
},
|
| 277 |
+
"vision": {
|
| 278 |
+
"patch_size": 16,
|
| 279 |
+
"merge_size": 2,
|
| 280 |
+
"temporal_patch_size": 2,
|
| 281 |
+
"hidden_size": 768,
|
| 282 |
+
"num_heads": 12,
|
| 283 |
+
"out_hidden_size": 1024,
|
| 284 |
+
"num_position_embeddings": 2304,
|
| 285 |
+
"rope_theta": 10000.0
|
| 286 |
+
},
|
| 287 |
+
"image": {
|
| 288 |
+
"min_pixels": 65536,
|
| 289 |
+
"max_pixels": 1638400,
|
| 290 |
+
"mean": [
|
| 291 |
+
0.5,
|
| 292 |
+
0.5,
|
| 293 |
+
0.5
|
| 294 |
+
],
|
| 295 |
+
"std": [
|
| 296 |
+
0.5,
|
| 297 |
+
0.5,
|
| 298 |
+
0.5
|
| 299 |
+
],
|
| 300 |
+
"rescale": 0.00392156862745098
|
| 301 |
+
},
|
| 302 |
+
"vision_buckets": [
|
| 303 |
+
320,
|
| 304 |
+
1536,
|
| 305 |
+
3072
|
| 306 |
+
],
|
| 307 |
+
"text_buckets": [
|
| 308 |
+
256,
|
| 309 |
+
640,
|
| 310 |
+
1536
|
| 311 |
+
]
|
| 312 |
+
}
|
lite/d3_E1536.mlpackage/Data/com.apple.CoreML/model.mlmodel
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:06625e4407aba711b4d13c0caaa85ea5d11d573d6f7e4245b62b52c49a960534
|
| 3 |
+
size 4539378
|
lite/d3_E1536.mlpackage/Data/com.apple.CoreML/weights/weight.bin
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:1570efcc188e884763f440b4f4780383b2b27b7286ff113a0c7b2f8dcdb83ddf
|
| 3 |
+
size 500897664
|
lite/d3_E1536.mlpackage/Manifest.json
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"fileFormatVersion": "1.0.0",
|
| 3 |
+
"itemInfoEntries": {
|
| 4 |
+
"0F786AA6-669F-4BB1-9F0F-9BE9E09F20EF": {
|
| 5 |
+
"author": "com.apple.CoreML",
|
| 6 |
+
"description": "CoreML Model Specification",
|
| 7 |
+
"name": "model.mlmodel",
|
| 8 |
+
"path": "com.apple.CoreML/model.mlmodel"
|
| 9 |
+
},
|
| 10 |
+
"78F6A973-8C96-4F9B-B548-288DFCBD0406": {
|
| 11 |
+
"author": "com.apple.CoreML",
|
| 12 |
+
"description": "CoreML Model Weights",
|
| 13 |
+
"name": "weights",
|
| 14 |
+
"path": "com.apple.CoreML/weights"
|
| 15 |
+
}
|
| 16 |
+
},
|
| 17 |
+
"rootModelIdentifier": "0F786AA6-669F-4BB1-9F0F-9BE9E09F20EF"
|
| 18 |
+
}
|
lite/d3_E256.mlpackage/Data/com.apple.CoreML/model.mlmodel
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:9886980c329516c7732b7c07f21cd480bc6b953a2091876743b5b39799848489
|
| 3 |
+
size 1944915
|
lite/d3_E256.mlpackage/Data/com.apple.CoreML/weights/weight.bin
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:a5565253ea689f841b849230ed43061d12f993c0d6c293627cef497cf7d64787
|
| 3 |
+
size 500221312
|
lite/d3_E256.mlpackage/Manifest.json
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"fileFormatVersion": "1.0.0",
|
| 3 |
+
"itemInfoEntries": {
|
| 4 |
+
"5A657C48-D6B0-4EC9-A96E-CE6DDEFEC813": {
|
| 5 |
+
"author": "com.apple.CoreML",
|
| 6 |
+
"description": "CoreML Model Specification",
|
| 7 |
+
"name": "model.mlmodel",
|
| 8 |
+
"path": "com.apple.CoreML/model.mlmodel"
|
| 9 |
+
},
|
| 10 |
+
"AFC1813F-EB9F-47B9-A90B-AAF702E6BD57": {
|
| 11 |
+
"author": "com.apple.CoreML",
|
| 12 |
+
"description": "CoreML Model Weights",
|
| 13 |
+
"name": "weights",
|
| 14 |
+
"path": "com.apple.CoreML/weights"
|
| 15 |
+
}
|
| 16 |
+
},
|
| 17 |
+
"rootModelIdentifier": "5A657C48-D6B0-4EC9-A96E-CE6DDEFEC813"
|
| 18 |
+
}
|
lite/d3_E640.mlpackage/Data/com.apple.CoreML/model.mlmodel
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:4c1ef6632dc8144243c270a18f1a4a27c519dd21a98d0f25f443300a325484c8
|
| 3 |
+
size 2722606
|
lite/d3_E640.mlpackage/Data/com.apple.CoreML/weights/weight.bin
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:7c18d2ca33683162515a27f22c2ce7cec5832eef740a7eb165a6ffcdb020a7e7
|
| 3 |
+
size 500423680
|
lite/d3_E640.mlpackage/Manifest.json
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"fileFormatVersion": "1.0.0",
|
| 3 |
+
"itemInfoEntries": {
|
| 4 |
+
"176FF80A-94E3-4B6C-82DB-0642EF7BE052": {
|
| 5 |
+
"author": "com.apple.CoreML",
|
| 6 |
+
"description": "CoreML Model Specification",
|
| 7 |
+
"name": "model.mlmodel",
|
| 8 |
+
"path": "com.apple.CoreML/model.mlmodel"
|
| 9 |
+
},
|
| 10 |
+
"F528A27F-912C-4218-ACEE-AE0622862F5D": {
|
| 11 |
+
"author": "com.apple.CoreML",
|
| 12 |
+
"description": "CoreML Model Weights",
|
| 13 |
+
"name": "weights",
|
| 14 |
+
"path": "com.apple.CoreML/weights"
|
| 15 |
+
}
|
| 16 |
+
},
|
| 17 |
+
"rootModelIdentifier": "176FF80A-94E3-4B6C-82DB-0642EF7BE052"
|
| 18 |
+
}
|
lite/embeddings.f16
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:dd7ed88d1e676ff4a3dc46d25d4751b1e465540a31522aba620a4948878e14e9
|
| 3 |
+
size 508559360
|
lite/pos_embed.f32
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:9a883241c3d1f55bbb10b1aca1202f463dc0ebe0efba8d11c4d00aaf1e95a4db
|
| 3 |
+
size 7077888
|
lite/python/d3_coreml_image.py
ADDED
|
@@ -0,0 +1,128 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""d3 image requests on Core ML (Python host): upstream prompt, processor and answer code; Core ML vision tower
|
| 2 |
+
(fp32) + text graph (fp16, host-spliced embeddings, host M-RoPE); FP32 readout on the host.
|
| 3 |
+
|
| 4 |
+
m = D3CoreMLImage("<d3 checkpoint dir>", "<coreml dir: Vision_P*, d3_E*.mlpackage, embeddings.f16>")
|
| 5 |
+
m.system_one(state=..., images=[...], questions={...}) # same answers dict as upstream
|
| 6 |
+
|
| 7 |
+
Positions come from transformers' Qwen3_5Model.compute_3d_position_ids on a weightless (meta) model."""
|
| 8 |
+
import json
|
| 9 |
+
import sys
|
| 10 |
+
from pathlib import Path
|
| 11 |
+
|
| 12 |
+
import coremltools as ct
|
| 13 |
+
import numpy as np
|
| 14 |
+
import torch
|
| 15 |
+
|
| 16 |
+
HERE = Path(__file__).parent
|
| 17 |
+
|
| 18 |
+
|
| 19 |
+
class D3CoreMLImage:
|
| 20 |
+
def __init__(self, root, coreml_dir, compute_units=ct.ComputeUnit.CPU_AND_GPU):
|
| 21 |
+
root, cdir = Path(root), Path(coreml_dir)
|
| 22 |
+
sys.path.insert(0, str(root))
|
| 23 |
+
sys.path.insert(0, str(HERE.parent / "vision"))
|
| 24 |
+
sys.path.insert(0, str(HERE.parent))
|
| 25 |
+
import d3_runtime as up
|
| 26 |
+
from qwen35_export import rope_cos_sin
|
| 27 |
+
from transformers import AutoConfig
|
| 28 |
+
from transformers.models.qwen3_5.modeling_qwen3_5 import Qwen3_5Model
|
| 29 |
+
from vision_export import VisionConfig
|
| 30 |
+
from d3_graph import load_text_config
|
| 31 |
+
|
| 32 |
+
self.up, self.rope_cos_sin = up, rope_cos_sin
|
| 33 |
+
config = json.loads((root / "decision_config.json").read_text())
|
| 34 |
+
self.codes, self.temperature = config["codes"], float(config.get("temperature", 1.0))
|
| 35 |
+
self.processor = up.load_processor(root)
|
| 36 |
+
self.vcfg = VisionConfig(json.loads((root / "config.json").read_text())["vision_config"])
|
| 37 |
+
self.tcfg = load_text_config(str(root))
|
| 38 |
+
with torch.device("meta"):
|
| 39 |
+
self.positions = Qwen3_5Model(AutoConfig.from_pretrained(str(root)))
|
| 40 |
+
from safetensors import safe_open
|
| 41 |
+
self.pos_table = None
|
| 42 |
+
for path in sorted(root.glob("model*.safetensors")):
|
| 43 |
+
with safe_open(str(path), "pt") as f:
|
| 44 |
+
if "visual.pos_embed.weight" in f.keys():
|
| 45 |
+
self.pos_table = f.get_tensor("visual.pos_embed.weight").float()
|
| 46 |
+
from safetensors.torch import load_file
|
| 47 |
+
self.readout = load_file(str(root / "readout.safetensors"))["weight"].float().numpy()
|
| 48 |
+
H = self.tcfg.hidden_size
|
| 49 |
+
self.embed = np.memmap(cdir / "embeddings.f16", dtype=np.float16, mode="r").reshape(-1, H)
|
| 50 |
+
self.image_token = AutoConfig.from_pretrained(str(root)).image_token_id
|
| 51 |
+
self.vision = {int(p.name[8:]): ct.models.MLModel(str(p / "VisionTower_fp32.mlpackage"), compute_units=compute_units)
|
| 52 |
+
for p in sorted(cdir.glob("Vision_P*"))}
|
| 53 |
+
multi = cdir / "d3_text.mlpackage" # one package, functions E{L} sharing the weights
|
| 54 |
+
if multi.exists():
|
| 55 |
+
names = json.loads((cdir / "d3_text.functions.json").read_text())
|
| 56 |
+
self.text = {int(n[1:]): ct.models.MLModel(str(multi), compute_units=compute_units, function_name=n)
|
| 57 |
+
for n in names}
|
| 58 |
+
else:
|
| 59 |
+
self.text = {int(p.stem[4:]): ct.models.MLModel(str(p), compute_units=compute_units)
|
| 60 |
+
for p in sorted(cdir.glob("d3_E*.mlpackage"))}
|
| 61 |
+
self.model_name = root.name + "-coreml"
|
| 62 |
+
|
| 63 |
+
def warm(self):
|
| 64 |
+
"""Run every vision and text bucket once: the first call per bucket pays the GPU setup (seconds)."""
|
| 65 |
+
for N, model in self.vision.items():
|
| 66 |
+
model.predict({"patches": np.zeros((N, self.vcfg.patch_dim), np.float32),
|
| 67 |
+
"pos_embeds": np.zeros((N, self.vcfg.hidden), np.float32),
|
| 68 |
+
"cos": np.zeros((N, self.vcfg.head_dim), np.float32),
|
| 69 |
+
"sin": np.zeros((N, self.vcfg.head_dim), np.float32),
|
| 70 |
+
"mask": np.zeros((1, 1, N, N), np.float32)})
|
| 71 |
+
H, R = self.tcfg.hidden_size, self.tcfg.rotary_dim
|
| 72 |
+
for L, model in self.text.items():
|
| 73 |
+
model.predict({"hidden": np.zeros((1, L, H), np.float16), "cos": np.ones((L, R), np.float16),
|
| 74 |
+
"sin": np.zeros((L, R), np.float16), "key_valid": np.ones(L, np.float16),
|
| 75 |
+
"last_onehot": np.eye(1, L, L - 1, dtype=np.float16)})
|
| 76 |
+
|
| 77 |
+
def _tokens(self, pv, grid):
|
| 78 |
+
from vision_export import prepare_inputs
|
| 79 |
+
N = next(b for b in sorted(self.vision) if b >= pv.shape[0])
|
| 80 |
+
patches, pe, cs, sn, mask, n = prepare_inputs(self.vcfg, self.pos_table, pv, grid, N)
|
| 81 |
+
out = self.vision[N].predict({"patches": patches.numpy(), "pos_embeds": pe.numpy(), "cos": cs.numpy(),
|
| 82 |
+
"sin": sn.numpy(), "mask": mask.numpy()})["tokens"]
|
| 83 |
+
return out[: n // 4]
|
| 84 |
+
|
| 85 |
+
def _probs(self, text, images, count, image_tokens):
|
| 86 |
+
enc = self.processor(text=[text], images=images, return_tensors="pt")
|
| 87 |
+
ids = enc["input_ids"][0].numpy()
|
| 88 |
+
hidden = self.embed[ids].astype(np.float32)
|
| 89 |
+
rows = np.where(ids == self.image_token)[0]
|
| 90 |
+
hidden[rows] = np.concatenate(image_tokens)
|
| 91 |
+
with torch.no_grad():
|
| 92 |
+
pos = self.positions.compute_3d_position_ids(
|
| 93 |
+
input_ids=enc["input_ids"], inputs_embeds=None, image_grid_thw=enc["image_grid_thw"],
|
| 94 |
+
attention_mask=enc["attention_mask"], mm_token_type_ids=enc["mm_token_type_ids"])[:, 0]
|
| 95 |
+
n = len(ids)
|
| 96 |
+
L = next((b for b in sorted(self.text) if b >= n), None)
|
| 97 |
+
if L is None:
|
| 98 |
+
raise ValueError(f"{n} tokens exceed the largest bucket {max(self.text)}")
|
| 99 |
+
cos, sin = self.rope_cos_sin(self.tcfg, torch.cat([pos, pos[:, -1:].expand(3, L - n)], dim=1))
|
| 100 |
+
x = np.zeros((1, L, hidden.shape[1]), dtype=np.float16); x[0, :n] = hidden
|
| 101 |
+
valid = np.zeros(L, dtype=np.float16); valid[:n] = 1
|
| 102 |
+
last = np.zeros((1, L), dtype=np.float16); last[0, n - 1] = 1
|
| 103 |
+
h = self.text[L].predict({"hidden": x, "cos": cos.numpy().astype(np.float16), "sin": sin.numpy().astype(np.float16),
|
| 104 |
+
"key_valid": valid, "last_onehot": last})["final"].reshape(-1)
|
| 105 |
+
logits = self.readout[:count].astype(np.float64) @ h.astype(np.float64) / self.temperature
|
| 106 |
+
p = np.exp(logits - logits.max())
|
| 107 |
+
return (p / p.sum()).tolist(), n
|
| 108 |
+
|
| 109 |
+
def system_one(self, *, state, questions, images):
|
| 110 |
+
up = self.up
|
| 111 |
+
decoded = [up.load_image(v) for v in images]
|
| 112 |
+
enc = self.processor.image_processor(images=decoded, return_tensors="pt")
|
| 113 |
+
# one vision call per image; every question reuses the image tokens
|
| 114 |
+
tokens, start = [], 0
|
| 115 |
+
for k, grid in enumerate(enc["image_grid_thw"]):
|
| 116 |
+
n = int(grid.prod())
|
| 117 |
+
tokens.append(self._tokens(enc["pixel_values"][start:start + n], grid[None]))
|
| 118 |
+
start += n
|
| 119 |
+
answers, used = {}, 0
|
| 120 |
+
for key, q in questions.items():
|
| 121 |
+
question = up.normalize_question(q)
|
| 122 |
+
text = self.processor.apply_chat_template(
|
| 123 |
+
up.image_messages("d3", state, question.rendered, self.codes, len(decoded)),
|
| 124 |
+
tokenize=False, add_generation_prompt=True, enable_thinking=False)
|
| 125 |
+
probs, n = self._probs(text, decoded, len(question.keys), tokens)
|
| 126 |
+
used += n
|
| 127 |
+
answers[key] = up.product_answer(question, probs)
|
| 128 |
+
return {"model": self.model_name, "answers": answers, "usage": {"input_tokens": used, "output_tokens": 0}}
|
lite/python/d3_coreml_text.py
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""d3 text requests on Core ML (Python host), for the checkpoints whose text graph takes embeddings (d3-mini).
|
| 2 |
+
|
| 3 |
+
Upstream's own request preparation runs on a weightless (meta-device) d3, so prompts and token ids are exactly
|
| 4 |
+
upstream's; Core ML runs the text graph per question (host embedding gather, text positions on all three M-RoPE axes);
|
| 5 |
+
FP32 readout on the host.
|
| 6 |
+
|
| 7 |
+
m = D3CoreMLText("<d3 checkpoint>", "<coreml dir>", text_buckets=[256, 640])
|
| 8 |
+
m.system_one(state=..., questions={...})"""
|
| 9 |
+
import sys
|
| 10 |
+
import time
|
| 11 |
+
from pathlib import Path
|
| 12 |
+
|
| 13 |
+
import coremltools as ct
|
| 14 |
+
import numpy as np
|
| 15 |
+
import torch
|
| 16 |
+
|
| 17 |
+
HERE = Path(__file__).parent
|
| 18 |
+
|
| 19 |
+
|
| 20 |
+
class D3CoreMLText:
|
| 21 |
+
def __init__(self, root, coreml_dir, text_buckets=None, compute_units=ct.ComputeUnit.CPU_AND_GPU):
|
| 22 |
+
root, cdir = Path(root), Path(coreml_dir)
|
| 23 |
+
sys.path.insert(0, str(root))
|
| 24 |
+
sys.path.insert(0, str(HERE.parent))
|
| 25 |
+
import d3_runtime as up
|
| 26 |
+
from d3_graph import load_text_config
|
| 27 |
+
from qwen35_export import rope_cos_sin
|
| 28 |
+
from safetensors.torch import load_file
|
| 29 |
+
|
| 30 |
+
self.up, self.rope_cos_sin = up, rope_cos_sin
|
| 31 |
+
self.prep = up.D3.from_pretrained(str(root), device="meta", verify="none") # no weights: prompts only
|
| 32 |
+
self.temperature = self.prep.temperature
|
| 33 |
+
self.tcfg = load_text_config(str(root))
|
| 34 |
+
self.readout = load_file(str(root / "readout.safetensors"))["weight"].float().numpy()
|
| 35 |
+
self.embed = np.memmap(cdir / "embeddings.f16", dtype=np.float16, mode="r").reshape(-1, self.tcfg.hidden_size)
|
| 36 |
+
self.text = {int(p.stem[4:]): ct.models.MLModel(str(p), compute_units=compute_units)
|
| 37 |
+
for p in sorted(cdir.glob("d3_E*.mlpackage")) if not text_buckets or int(p.stem[4:]) in text_buckets}
|
| 38 |
+
self.pad = self.prep.pad_id
|
| 39 |
+
self.model_name = root.name + "-coreml"
|
| 40 |
+
self.timing = []
|
| 41 |
+
|
| 42 |
+
def warm(self):
|
| 43 |
+
for L, model in self.text.items():
|
| 44 |
+
for _ in range(2):
|
| 45 |
+
self._final(list(range(10)), L)
|
| 46 |
+
|
| 47 |
+
def _final(self, ids, L):
|
| 48 |
+
n = len(ids)
|
| 49 |
+
x = np.zeros((1, L, self.tcfg.hidden_size), dtype=np.float16)
|
| 50 |
+
x[0, :n] = self.embed[np.asarray(ids)]
|
| 51 |
+
x[0, n:] = self.embed[self.pad]
|
| 52 |
+
pos = torch.arange(L).clamp(max=n - 1)[None].expand(3, -1)
|
| 53 |
+
cos, sin = self.rope_cos_sin(self.tcfg, pos)
|
| 54 |
+
valid = np.zeros(L, dtype=np.float16); valid[:n] = 1
|
| 55 |
+
last = np.zeros((1, L), dtype=np.float16); last[0, n - 1] = 1
|
| 56 |
+
return self.text[L].predict({"hidden": x, "cos": cos.numpy().astype(np.float16),
|
| 57 |
+
"sin": sin.numpy().astype(np.float16), "key_valid": valid,
|
| 58 |
+
"last_onehot": last})["final"].reshape(-1)
|
| 59 |
+
|
| 60 |
+
def system_one(self, *, state, questions):
|
| 61 |
+
prepared = self.prep.prepare(state, questions)
|
| 62 |
+
answers, used, self.timing = {}, 0, []
|
| 63 |
+
for key in prepared.keys:
|
| 64 |
+
if key in prepared.errors:
|
| 65 |
+
answers[key] = prepared.errors[key]
|
| 66 |
+
continue
|
| 67 |
+
ids, question = prepared.sequences[key], prepared.questions[key]
|
| 68 |
+
L = next((b for b in sorted(self.text) if b >= len(ids)), None)
|
| 69 |
+
if L is None:
|
| 70 |
+
answers[key] = {"type": question.kind, "error": "max_length_exceeded",
|
| 71 |
+
"message": f"{len(ids)} tokens exceed the largest Core ML bucket {max(self.text)}"}
|
| 72 |
+
continue
|
| 73 |
+
t = time.perf_counter()
|
| 74 |
+
h = self._final(ids, L)
|
| 75 |
+
self.timing.append((len(ids), L, round(time.perf_counter() - t, 3)))
|
| 76 |
+
logits = self.readout[:len(question.keys)].astype(np.float64) @ h.astype(np.float64) / self.temperature
|
| 77 |
+
p = np.exp(logits - logits.max())
|
| 78 |
+
answers[key] = self.up.product_answer(question, (p / p.sum()).tolist())
|
| 79 |
+
used += len(ids)
|
| 80 |
+
return {"model": self.model_name, "answers": answers, "usage": {"input_tokens": used, "output_tokens": 0}}
|
lite/python/d3_coreml_video.py
ADDED
|
@@ -0,0 +1,122 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""d3 video requests on Core ML (Python host).
|
| 2 |
+
|
| 3 |
+
Upstream's own request preparation runs on a weightless (meta-device) d3, so frame sampling, the video processor,
|
| 4 |
+
the timestamped placeholders and the 3-D positions are exactly upstream's. Core ML runs the compute: the fp32 vision
|
| 5 |
+
tower once per video (attention within each frame pair, as upstream: one block per temporal group), then the text
|
| 6 |
+
graph per question with the video tokens spliced in; FP32 readout on the host.
|
| 7 |
+
|
| 8 |
+
m = D3CoreMLVideo("<d3 checkpoint>", "<coreml dir>", max_pixels=100_352)
|
| 9 |
+
m.system_one(state=..., questions={...}, videos=[frames]) # frames: uint8 [n, h, w, 3] at 2 fps"""
|
| 10 |
+
import json
|
| 11 |
+
import sys
|
| 12 |
+
import time
|
| 13 |
+
from pathlib import Path
|
| 14 |
+
|
| 15 |
+
import coremltools as ct
|
| 16 |
+
import numpy as np
|
| 17 |
+
import torch
|
| 18 |
+
|
| 19 |
+
HERE = Path(__file__).parent
|
| 20 |
+
|
| 21 |
+
|
| 22 |
+
class D3CoreMLVideo:
|
| 23 |
+
def __init__(self, root, coreml_dir, max_pixels=None, vision_buckets=None, text_buckets=None,
|
| 24 |
+
compute_units=ct.ComputeUnit.CPU_AND_GPU):
|
| 25 |
+
"""`vision_buckets` / `text_buckets`: load only these sizes (each loaded model costs memory)."""
|
| 26 |
+
root, cdir = Path(root), Path(coreml_dir)
|
| 27 |
+
sys.path.insert(0, str(root))
|
| 28 |
+
sys.path.insert(0, str(HERE.parent / "vision"))
|
| 29 |
+
sys.path.insert(0, str(HERE.parent))
|
| 30 |
+
import d3_runtime as up
|
| 31 |
+
if max_pixels:
|
| 32 |
+
up.VIDEO_MAX_PIXELS = max_pixels
|
| 33 |
+
from d3_graph import load_text_config
|
| 34 |
+
from qwen35_export import rope_cos_sin
|
| 35 |
+
from safetensors import safe_open
|
| 36 |
+
from safetensors.torch import load_file
|
| 37 |
+
from vision_export import VisionConfig
|
| 38 |
+
|
| 39 |
+
self.up, self.rope_cos_sin = up, rope_cos_sin
|
| 40 |
+
self.prep = up.D3.from_pretrained(str(root), device="meta", verify="none") # no weights: prompts only
|
| 41 |
+
self.processor, self.temperature = self.prep.processor, self.prep.temperature
|
| 42 |
+
self.backbone = self.prep.backbone
|
| 43 |
+
self.vcfg = VisionConfig(json.loads((root / "config.json").read_text())["vision_config"])
|
| 44 |
+
self.tcfg = load_text_config(str(root))
|
| 45 |
+
for path in sorted(root.glob("model*.safetensors")):
|
| 46 |
+
with safe_open(str(path), "pt") as f:
|
| 47 |
+
if "visual.pos_embed.weight" in f.keys():
|
| 48 |
+
self.pos_table = f.get_tensor("visual.pos_embed.weight").float()
|
| 49 |
+
self.readout = load_file(str(root / "readout.safetensors"))["weight"].float().numpy()
|
| 50 |
+
H = self.tcfg.hidden_size
|
| 51 |
+
self.embed = np.memmap(cdir / "embeddings.f16", dtype=np.float16, mode="r").reshape(-1, H)
|
| 52 |
+
self.video_token = self.backbone.config.video_token_id
|
| 53 |
+
self.vision = {int(p.name[8:]): ct.models.MLModel(str(p / "VisionTower_fp32.mlpackage"), compute_units=compute_units)
|
| 54 |
+
for p in sorted(cdir.glob("Vision_P*")) if not vision_buckets or int(p.name[8:]) in vision_buckets}
|
| 55 |
+
self.text = {int(p.stem[4:]): ct.models.MLModel(str(p), compute_units=compute_units)
|
| 56 |
+
for p in sorted(cdir.glob("d3_E*.mlpackage")) if not text_buckets or int(p.stem[4:]) in text_buckets}
|
| 57 |
+
self.model_name = root.name + "-coreml"
|
| 58 |
+
self.timing = {}
|
| 59 |
+
|
| 60 |
+
def warm(self, frames, state, questions):
|
| 61 |
+
for _ in range(2):
|
| 62 |
+
self.system_one(state=state, questions=questions, videos=[frames])
|
| 63 |
+
|
| 64 |
+
def _video_tokens(self, pixel_values, grid):
|
| 65 |
+
"""Vision tower for one video: attention within each temporal group (frame pair), padding masked."""
|
| 66 |
+
from vision_export import prepare_inputs
|
| 67 |
+
n = pixel_values.shape[0]
|
| 68 |
+
N = next(b for b in sorted(self.vision) if b >= n)
|
| 69 |
+
patches, pe, cs, sn, mask, n = prepare_inputs(self.vcfg, self.pos_table, pixel_values, grid, N)
|
| 70 |
+
t, h, w = (int(x) for x in grid.reshape(-1))
|
| 71 |
+
block = np.full((N, N), -1e4, dtype=np.float32)
|
| 72 |
+
for g in range(t): # rows are t-major: each frame pair is h*w consecutive patches
|
| 73 |
+
s = g * h * w
|
| 74 |
+
block[s:s + h * w, s:s + h * w] = 0
|
| 75 |
+
out = self.vision[N].predict({"patches": patches.numpy(), "pos_embeds": pe.numpy(), "cos": cs.numpy(),
|
| 76 |
+
"sin": sn.numpy(), "mask": block[None, None]})["tokens"]
|
| 77 |
+
return out[: n // 4]
|
| 78 |
+
|
| 79 |
+
def system_one(self, *, state, questions, videos):
|
| 80 |
+
up, proc = self.up, self.processor
|
| 81 |
+
t0 = time.perf_counter()
|
| 82 |
+
prepared = self.prep.prepare(state, questions, videos=videos)
|
| 83 |
+
media = prepared.media
|
| 84 |
+
grids = media["video_grid_thw"]
|
| 85 |
+
tokens, start = [], 0
|
| 86 |
+
for grid in grids:
|
| 87 |
+
n = int(grid.prod())
|
| 88 |
+
tokens.append(self._video_tokens(media["pixel_values_videos"][start:start + n], grid[None]))
|
| 89 |
+
start += n
|
| 90 |
+
tokens = np.concatenate(tokens)
|
| 91 |
+
t1 = time.perf_counter()
|
| 92 |
+
answers, used = {}, 0
|
| 93 |
+
for key in prepared.runnable:
|
| 94 |
+
question = prepared.questions[key]
|
| 95 |
+
expanded = proc.get_text_with_replacements([prepared.texts[key]], [], prepared.video_texts)[0][0]
|
| 96 |
+
enc = proc.tokenizer([expanded], return_token_type_ids=False, return_tensors="pt")
|
| 97 |
+
ids = enc["input_ids"]
|
| 98 |
+
types = torch.as_tensor(proc.create_mm_token_type_ids(ids))
|
| 99 |
+
row = ids[0].numpy()
|
| 100 |
+
hidden = self.embed[row].astype(np.float32)
|
| 101 |
+
hidden[np.where(row == self.video_token)[0]] = tokens
|
| 102 |
+
with torch.no_grad():
|
| 103 |
+
pos = self.backbone.compute_3d_position_ids(
|
| 104 |
+
input_ids=ids, inputs_embeds=None, image_grid_thw=None, video_grid_thw=grids,
|
| 105 |
+
attention_mask=enc["attention_mask"], past_key_values=None, mm_token_type_ids=types)[:, 0]
|
| 106 |
+
n = len(row)
|
| 107 |
+
L = next((b for b in sorted(self.text) if b >= n), None)
|
| 108 |
+
if L is None:
|
| 109 |
+
raise ValueError(f"{n} tokens exceed the largest text bucket {max(self.text)}")
|
| 110 |
+
cos, sin = self.rope_cos_sin(self.tcfg, torch.cat([pos, pos[:, -1:].expand(3, L - n)], dim=1))
|
| 111 |
+
x = np.zeros((1, L, hidden.shape[1]), dtype=np.float16); x[0, :n] = hidden
|
| 112 |
+
valid = np.zeros(L, dtype=np.float16); valid[:n] = 1
|
| 113 |
+
last = np.zeros((1, L), dtype=np.float16); last[0, n - 1] = 1
|
| 114 |
+
h = self.text[L].predict({"hidden": x, "cos": cos.numpy().astype(np.float16),
|
| 115 |
+
"sin": sin.numpy().astype(np.float16), "key_valid": valid,
|
| 116 |
+
"last_onehot": last})["final"].reshape(-1)
|
| 117 |
+
logits = self.readout[:len(question.keys)].astype(np.float64) @ h.astype(np.float64) / self.temperature
|
| 118 |
+
p = np.exp(logits - logits.max())
|
| 119 |
+
answers[key] = up.product_answer(question, (p / p.sum()).tolist())
|
| 120 |
+
used += n
|
| 121 |
+
self.timing = {"prepare+vision": round(t1 - t0, 3), "text": round(time.perf_counter() - t1, 3)}
|
| 122 |
+
return {"model": self.model_name, "answers": answers, "usage": {"input_tokens": used, "output_tokens": 0}}
|
lite/python/d3_graph.py
ADDED
|
@@ -0,0 +1,120 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""d3-lite (Qwen3.5-0.8B, code readout v1) as one Core ML row graph.
|
| 2 |
+
|
| 3 |
+
One question per call, as upstream: the row is right-padded to the bucket length L. The 6 full-attention layers are
|
| 4 |
+
noncausal (decision_config `noncausal_full_attention`): they see every real token and mask only the padding keys. The
|
| 5 |
+
Gated DeltaNet layers are causal scans, so padding after the last real token cannot reach it. Output is the final-normed
|
| 6 |
+
hidden state at the last real token; the 255 x 1024 FP32 readout runs on the host, as upstream.
|
| 7 |
+
"""
|
| 8 |
+
|
| 9 |
+
import json
|
| 10 |
+
|
| 11 |
+
import numpy as np
|
| 12 |
+
import torch
|
| 13 |
+
from safetensors.torch import load_file
|
| 14 |
+
from torch import nn
|
| 15 |
+
|
| 16 |
+
from qwen35_export import DecoderLayer, RMSNorm, TextConfig, rope_cos_sin
|
| 17 |
+
|
| 18 |
+
MASK_VALUE = -1e4
|
| 19 |
+
|
| 20 |
+
|
| 21 |
+
class D3Row(nn.Module):
|
| 22 |
+
def __init__(self, cfg: TextConfig, seq_len: int, chunk: int = 64):
|
| 23 |
+
super().__init__()
|
| 24 |
+
self.cfg, self.L = cfg, seq_len
|
| 25 |
+
self.embed_tokens = nn.Embedding(1, cfg.hidden_size) # replaced on load
|
| 26 |
+
self.layers = nn.ModuleList([DecoderLayer(cfg, i, seq_len, chunk) for i in range(cfg.num_layers)])
|
| 27 |
+
self.norm = RMSNorm(cfg.hidden_size, cfg.eps)
|
| 28 |
+
|
| 29 |
+
def forward(self, input_ids, cos, sin, key_valid, last_onehot):
|
| 30 |
+
"""input_ids [1, L] int32; cos/sin [L, R]; key_valid [L] (1 real, 0 pad); last_onehot [1, L] -> hidden [1, H]."""
|
| 31 |
+
mask = ((1.0 - key_valid) * MASK_VALUE)[None, None, None, :]
|
| 32 |
+
x = self.embed_tokens(input_ids)
|
| 33 |
+
for layer in self.layers:
|
| 34 |
+
x = layer(x, cos, sin, mask)
|
| 35 |
+
return self.norm(torch.matmul(last_onehot, x[0]))
|
| 36 |
+
|
| 37 |
+
|
| 38 |
+
class D3RowEmbeds(nn.Module):
|
| 39 |
+
"""D3Row for image requests: the host gathers token embeddings and splices the vision tower's tokens in at the
|
| 40 |
+
image-pad positions, and builds cos/sin from the 3-D M-RoPE positions (text and image tokens differ)."""
|
| 41 |
+
|
| 42 |
+
def __init__(self, cfg: TextConfig, seq_len: int, chunk: int = 64):
|
| 43 |
+
super().__init__()
|
| 44 |
+
self.cfg, self.L = cfg, seq_len
|
| 45 |
+
self.layers = nn.ModuleList([DecoderLayer(cfg, i, seq_len, chunk) for i in range(cfg.num_layers)])
|
| 46 |
+
self.norm = RMSNorm(cfg.hidden_size, cfg.eps)
|
| 47 |
+
|
| 48 |
+
def forward(self, hidden, cos, sin, key_valid, last_onehot):
|
| 49 |
+
"""hidden [1, L, H]; cos/sin [L, R]; key_valid [L]; last_onehot [1, L] -> final hidden [1, H]."""
|
| 50 |
+
mask = ((1.0 - key_valid) * MASK_VALUE)[None, None, None, :]
|
| 51 |
+
x = hidden
|
| 52 |
+
for layer in self.layers:
|
| 53 |
+
x = layer(x, cos, sin, mask)
|
| 54 |
+
return self.norm(torch.matmul(last_onehot, x[0]))
|
| 55 |
+
|
| 56 |
+
|
| 57 |
+
def load_row_embeds(root: str, seq_len: int, chunk: int = 64, dtype=torch.float32) -> tuple[D3RowEmbeds, torch.Tensor]:
|
| 58 |
+
"""(graph, embedding table [V, H]). dtype=float16 halves peak memory for tracing the larger checkpoints (d3-mini:
|
| 59 |
+
fp32 weights plus their copy do not fit in 24 GB); weights are assigned in place, not copied."""
|
| 60 |
+
cfg = load_text_config(root)
|
| 61 |
+
prefix = "language_model."
|
| 62 |
+
state = {}
|
| 63 |
+
from safetensors import safe_open
|
| 64 |
+
files = sorted(__import__("glob").glob(f"{root}/model*.safetensors"))
|
| 65 |
+
for path in files:
|
| 66 |
+
with safe_open(path, "pt") as f:
|
| 67 |
+
for k in f.keys():
|
| 68 |
+
if k.startswith(prefix):
|
| 69 |
+
state[k[len(prefix):]] = f.get_tensor(k).to(dtype)
|
| 70 |
+
embed = state.pop("embed_tokens.weight")
|
| 71 |
+
model = D3RowEmbeds(cfg, seq_len, chunk).to(dtype)
|
| 72 |
+
missing, unexpected = model.load_state_dict(state, strict=False, assign=True)
|
| 73 |
+
if missing or unexpected:
|
| 74 |
+
raise RuntimeError(f"weight mismatch: missing={missing[:8]} unexpected={unexpected[:8]}")
|
| 75 |
+
return model.eval(), embed
|
| 76 |
+
|
| 77 |
+
|
| 78 |
+
def load_text_config(root: str) -> TextConfig:
|
| 79 |
+
return TextConfig(json.load(open(f"{root}/config.json"))["text_config"])
|
| 80 |
+
|
| 81 |
+
|
| 82 |
+
def load_row(root: str, seq_len: int, chunk: int = 64) -> D3Row:
|
| 83 |
+
cfg = load_text_config(root)
|
| 84 |
+
weights = load_file(f"{root}/model.safetensors")
|
| 85 |
+
prefix = "language_model."
|
| 86 |
+
state = {k[len(prefix):]: v.float() for k, v in weights.items() if k.startswith(prefix)}
|
| 87 |
+
model = D3Row(cfg, seq_len, chunk)
|
| 88 |
+
model.embed_tokens = nn.Embedding.from_pretrained(state.pop("embed_tokens.weight"), freeze=True)
|
| 89 |
+
missing, unexpected = model.load_state_dict(state, strict=False)
|
| 90 |
+
missing = [m for m in missing if not m.startswith("embed_tokens")]
|
| 91 |
+
if missing or unexpected:
|
| 92 |
+
raise RuntimeError(f"weight mismatch: missing={missing[:8]} unexpected={unexpected[:8]}")
|
| 93 |
+
return model.eval()
|
| 94 |
+
|
| 95 |
+
|
| 96 |
+
def load_readout(root: str) -> np.ndarray:
|
| 97 |
+
return load_file(f"{root}/readout.safetensors")["weight"].float().numpy()
|
| 98 |
+
|
| 99 |
+
|
| 100 |
+
def row_inputs(cfg: TextConfig, ids, L: int, pad_id: int) -> dict[str, np.ndarray]:
|
| 101 |
+
n = len(ids)
|
| 102 |
+
if not 1 <= n <= L:
|
| 103 |
+
raise ValueError(f"{n} tokens do not fit L{L}")
|
| 104 |
+
input_ids = np.full((1, L), pad_id, dtype=np.int32)
|
| 105 |
+
input_ids[0, :n] = ids
|
| 106 |
+
pos = torch.arange(L)
|
| 107 |
+
pos[n:] = n - 1
|
| 108 |
+
cos, sin = rope_cos_sin(cfg, pos[None].expand(3, -1))
|
| 109 |
+
valid = np.zeros(L, dtype=np.float32)
|
| 110 |
+
valid[:n] = 1
|
| 111 |
+
last = np.zeros((1, L), dtype=np.float32)
|
| 112 |
+
last[0, n - 1] = 1
|
| 113 |
+
return {"input_ids": input_ids, "cos": cos.numpy(), "sin": sin.numpy(), "key_valid": valid, "last_onehot": last}
|
| 114 |
+
|
| 115 |
+
|
| 116 |
+
def code_probabilities(hidden: np.ndarray, readout: np.ndarray, count: int, temperature: float = 1.0) -> np.ndarray:
|
| 117 |
+
logits = hidden.astype(np.float64).reshape(-1) @ readout[:count].astype(np.float64).T / temperature
|
| 118 |
+
logits -= logits.max()
|
| 119 |
+
p = np.exp(logits)
|
| 120 |
+
return p / p.sum()
|
lite/python/qwen35_export.py
ADDED
|
@@ -0,0 +1,333 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Trace-friendly, prefill-only Qwen3.5 text decoder (copied from models/computer-use/cua-s1-4b/coreml, mobius PR #104).
|
| 2 |
+
|
| 3 |
+
Cua-S1-4B reads one forward pass: the logits of the answer letters A..Z at the
|
| 4 |
+
last prompt position. Nothing is generated, so this graph has no KV cache, no
|
| 5 |
+
recurrent-state I/O and no full vocabulary head:
|
| 6 |
+
|
| 7 |
+
- inputs are `inputs_embeds` (host-side embedding gather, so a multimodal
|
| 8 |
+
caller can splice vision features in) plus host-computed M-RoPE `cos`/`sin`;
|
| 9 |
+
- prompts are right-padded to a fixed bucket; the model is causal and the gated
|
| 10 |
+
delta rule is a forward scan, so padding after the last real token cannot
|
| 11 |
+
change anything before it;
|
| 12 |
+
- the last chunk gathers the hidden state at `last_index` (one-hot matmul),
|
| 13 |
+
applies the final norm and multiplies by the 26 tied-embedding rows of the
|
| 14 |
+
letter tokens.
|
| 15 |
+
|
| 16 |
+
The gated delta rule uses the chunked (WY/UT) form. The unit-lower-triangular
|
| 17 |
+
solve is a recursive 2x2 block inverse (log2(chunk) batched matmul levels). The
|
| 18 |
+
nilpotent power series (I + N)(I + N^2)... is exact algebraically but its terms
|
| 19 |
+
grow combinatorially before cancelling and overflow on real Qwen3.5 layers.
|
| 20 |
+
Within-chunk decay differences are computed as masked sums of the per-step
|
| 21 |
+
decays instead of differences of cumulative sums, which keeps them accurate in
|
| 22 |
+
fp16 when the cumulative decay is large.
|
| 23 |
+
"""
|
| 24 |
+
|
| 25 |
+
from __future__ import annotations
|
| 26 |
+
|
| 27 |
+
import math
|
| 28 |
+
|
| 29 |
+
import torch
|
| 30 |
+
import torch.nn.functional as F
|
| 31 |
+
from torch import nn
|
| 32 |
+
|
| 33 |
+
|
| 34 |
+
class TextConfig:
|
| 35 |
+
def __init__(self, cfg: dict):
|
| 36 |
+
self.hidden_size = cfg["hidden_size"]
|
| 37 |
+
self.intermediate_size = cfg["intermediate_size"]
|
| 38 |
+
self.num_layers = cfg["num_hidden_layers"]
|
| 39 |
+
self.layer_types = cfg["layer_types"]
|
| 40 |
+
self.eps = cfg["rms_norm_eps"]
|
| 41 |
+
self.num_heads = cfg["num_attention_heads"]
|
| 42 |
+
self.num_kv_heads = cfg["num_key_value_heads"]
|
| 43 |
+
self.head_dim = cfg["head_dim"]
|
| 44 |
+
self.lin_k_heads = cfg["linear_num_key_heads"]
|
| 45 |
+
self.lin_v_heads = cfg["linear_num_value_heads"]
|
| 46 |
+
self.lin_k_dim = cfg["linear_key_head_dim"]
|
| 47 |
+
self.lin_v_dim = cfg["linear_value_head_dim"]
|
| 48 |
+
self.conv_kernel = cfg["linear_conv_kernel_dim"]
|
| 49 |
+
rope = cfg["rope_parameters"]
|
| 50 |
+
self.rope_theta = rope["rope_theta"]
|
| 51 |
+
self.rotary_dim = int(self.head_dim * rope.get("partial_rotary_factor", 1.0))
|
| 52 |
+
self.mrope_section = rope.get("mrope_section", [11, 11, 10])
|
| 53 |
+
|
| 54 |
+
|
| 55 |
+
def rope_cos_sin(cfg: TextConfig, position_ids: torch.Tensor) -> tuple[torch.Tensor, torch.Tensor]:
|
| 56 |
+
"""Interleaved M-RoPE tables. `position_ids` is [3, L] (t, h, w); returns [L, rotary_dim] fp32 each.
|
| 57 |
+
|
| 58 |
+
Mirrors `Qwen3_5TextRotaryEmbedding` (computed in float64 then cast, host side)."""
|
| 59 |
+
dim = cfg.rotary_dim
|
| 60 |
+
inv_freq = 1.0 / (cfg.rope_theta ** (torch.arange(0, dim, 2, dtype=torch.float64) / dim))
|
| 61 |
+
freqs = position_ids.to(torch.float64)[:, :, None] * inv_freq[None, None, :] # [3, L, dim/2]
|
| 62 |
+
thw = freqs[0].clone()
|
| 63 |
+
for axis, offset in ((1, 1), (2, 2)):
|
| 64 |
+
length = cfg.mrope_section[axis] * 3
|
| 65 |
+
thw[:, offset:length:3] = freqs[axis][:, offset:length:3]
|
| 66 |
+
emb = torch.cat([thw, thw], dim=-1)
|
| 67 |
+
return emb.cos().float(), emb.sin().float()
|
| 68 |
+
|
| 69 |
+
|
| 70 |
+
class RMSNorm(nn.Module):
|
| 71 |
+
"""Qwen3.5 zero-centred RMSNorm: x * rsqrt(mean(x^2)+eps) * (1 + w)."""
|
| 72 |
+
|
| 73 |
+
def __init__(self, dim: int, eps: float):
|
| 74 |
+
super().__init__()
|
| 75 |
+
self.eps = eps
|
| 76 |
+
self.weight = nn.Parameter(torch.zeros(dim))
|
| 77 |
+
|
| 78 |
+
def forward(self, x):
|
| 79 |
+
x = x * torch.rsqrt(x.pow(2).mean(-1, keepdim=True) + self.eps)
|
| 80 |
+
return x * (1.0 + self.weight)
|
| 81 |
+
|
| 82 |
+
|
| 83 |
+
class MLP(nn.Module):
|
| 84 |
+
def __init__(self, cfg: TextConfig):
|
| 85 |
+
super().__init__()
|
| 86 |
+
self.gate_proj = nn.Linear(cfg.hidden_size, cfg.intermediate_size, bias=False)
|
| 87 |
+
self.up_proj = nn.Linear(cfg.hidden_size, cfg.intermediate_size, bias=False)
|
| 88 |
+
self.down_proj = nn.Linear(cfg.intermediate_size, cfg.hidden_size, bias=False)
|
| 89 |
+
|
| 90 |
+
def forward(self, x):
|
| 91 |
+
return self.down_proj(F.silu(self.gate_proj(x)) * self.up_proj(x))
|
| 92 |
+
|
| 93 |
+
|
| 94 |
+
def rotate_half(x):
|
| 95 |
+
half = x.shape[-1] // 2
|
| 96 |
+
return torch.cat([-x[..., half:], x[..., :half]], dim=-1)
|
| 97 |
+
|
| 98 |
+
|
| 99 |
+
class FullAttention(nn.Module):
|
| 100 |
+
def __init__(self, cfg: TextConfig):
|
| 101 |
+
super().__init__()
|
| 102 |
+
self.cfg = cfg
|
| 103 |
+
h, kv, d = cfg.num_heads, cfg.num_kv_heads, cfg.head_dim
|
| 104 |
+
self.q_proj = nn.Linear(cfg.hidden_size, h * d * 2, bias=False)
|
| 105 |
+
self.k_proj = nn.Linear(cfg.hidden_size, kv * d, bias=False)
|
| 106 |
+
self.v_proj = nn.Linear(cfg.hidden_size, kv * d, bias=False)
|
| 107 |
+
self.o_proj = nn.Linear(h * d, cfg.hidden_size, bias=False)
|
| 108 |
+
self.q_norm = RMSNorm(d, cfg.eps)
|
| 109 |
+
self.k_norm = RMSNorm(d, cfg.eps)
|
| 110 |
+
|
| 111 |
+
def forward(self, x, cos, sin, mask):
|
| 112 |
+
cfg = self.cfg
|
| 113 |
+
L = x.shape[1]
|
| 114 |
+
h, kv, d, r = cfg.num_heads, cfg.num_kv_heads, cfg.head_dim, cfg.rotary_dim
|
| 115 |
+
qg = self.q_proj(x).view(1, L, h, 2 * d)
|
| 116 |
+
q, gate = qg[..., :d], qg[..., d:]
|
| 117 |
+
gate = gate.reshape(1, L, h * d)
|
| 118 |
+
q = self.q_norm(q).transpose(1, 2) # [1, h, L, d]
|
| 119 |
+
k = self.k_norm(self.k_proj(x).view(1, L, kv, d)).transpose(1, 2)
|
| 120 |
+
v = self.v_proj(x).view(1, L, kv, d).transpose(1, 2)
|
| 121 |
+
|
| 122 |
+
c, s = cos[None, None], sin[None, None] # [1, 1, L, r]
|
| 123 |
+
q = torch.cat([q[..., :r] * c + rotate_half(q[..., :r]) * s, q[..., r:]], dim=-1)
|
| 124 |
+
k = torch.cat([k[..., :r] * c + rotate_half(k[..., :r]) * s, k[..., r:]], dim=-1)
|
| 125 |
+
|
| 126 |
+
rep = h // kv
|
| 127 |
+
k = k[:, :, None].expand(1, kv, rep, L, d).reshape(1, h, L, d)
|
| 128 |
+
v = v[:, :, None].expand(1, kv, rep, L, d).reshape(1, h, L, d)
|
| 129 |
+
scores = torch.matmul(q, k.transpose(-1, -2)) * (d**-0.5) + mask
|
| 130 |
+
out = torch.matmul(torch.softmax(scores, dim=-1), v) # [1, h, L, d]
|
| 131 |
+
out = out.transpose(1, 2).reshape(1, L, h * d)
|
| 132 |
+
return self.o_proj(out * torch.sigmoid(gate))
|
| 133 |
+
|
| 134 |
+
|
| 135 |
+
class GatedRMSNorm(nn.Module):
|
| 136 |
+
def __init__(self, dim: int, eps: float):
|
| 137 |
+
super().__init__()
|
| 138 |
+
self.eps = eps
|
| 139 |
+
self.weight = nn.Parameter(torch.ones(dim))
|
| 140 |
+
|
| 141 |
+
def forward(self, x, gate):
|
| 142 |
+
x = x * torch.rsqrt(x.pow(2).mean(-1, keepdim=True) + self.eps)
|
| 143 |
+
return self.weight * x * F.silu(gate)
|
| 144 |
+
|
| 145 |
+
|
| 146 |
+
class GatedDeltaNet(nn.Module):
|
| 147 |
+
def __init__(self, cfg: TextConfig, seq_len: int, chunk_size: int):
|
| 148 |
+
super().__init__()
|
| 149 |
+
self.cfg = cfg
|
| 150 |
+
self.chunk = chunk_size
|
| 151 |
+
assert seq_len % chunk_size == 0, "bucket length must be a multiple of the delta-rule chunk size"
|
| 152 |
+
self.num_chunks = seq_len // chunk_size
|
| 153 |
+
kd = cfg.lin_k_heads * cfg.lin_k_dim
|
| 154 |
+
vd = cfg.lin_v_heads * cfg.lin_v_dim
|
| 155 |
+
self.key_dim, self.value_dim = kd, vd
|
| 156 |
+
self.conv_dim = 2 * kd + vd
|
| 157 |
+
self.in_proj_qkv = nn.Linear(cfg.hidden_size, self.conv_dim, bias=False)
|
| 158 |
+
self.in_proj_z = nn.Linear(cfg.hidden_size, vd, bias=False)
|
| 159 |
+
self.in_proj_b = nn.Linear(cfg.hidden_size, cfg.lin_v_heads, bias=False)
|
| 160 |
+
self.in_proj_a = nn.Linear(cfg.hidden_size, cfg.lin_v_heads, bias=False)
|
| 161 |
+
self.conv1d = nn.Conv1d(self.conv_dim, self.conv_dim, cfg.conv_kernel, groups=self.conv_dim, bias=False)
|
| 162 |
+
self.dt_bias = nn.Parameter(torch.ones(cfg.lin_v_heads))
|
| 163 |
+
self.A_log = nn.Parameter(torch.zeros(cfg.lin_v_heads))
|
| 164 |
+
self.norm = GatedRMSNorm(cfg.lin_v_dim, cfg.eps)
|
| 165 |
+
self.out_proj = nn.Linear(vd, cfg.hidden_size, bias=False)
|
| 166 |
+
|
| 167 |
+
C = chunk_size
|
| 168 |
+
idx = torch.arange(C)
|
| 169 |
+
# incl[i, j, k] = 1 if j < k <= i : sum_k g_k = cum_i - cum_j (lower triangle incl. diagonal is 0 on i == j)
|
| 170 |
+
incl = ((idx[None, None, :] > idx[None, :, None]) & (idx[None, None, :] <= idx[:, None, None])).float()
|
| 171 |
+
self.register_buffer("pair_sum_t", incl.reshape(C * C, C), persistent=False) # [C*C, C]
|
| 172 |
+
self.register_buffer("prefix_sum_t", (idx[None, :] <= idx[:, None]).float(), persistent=False) # [C, C]
|
| 173 |
+
self.register_buffer("suffix_sum_t", (idx[None, :] > idx[:, None]).float(), persistent=False)
|
| 174 |
+
self.register_buffer("lower_incl", (idx[None, :] <= idx[:, None]).float(), persistent=False)
|
| 175 |
+
self.register_buffer("strict_lower", (idx[None, :] < idx[:, None]).float(), persistent=False)
|
| 176 |
+
self.register_buffer("eye", torch.eye(C), persistent=False)
|
| 177 |
+
assert C & (C - 1) == 0, "delta-rule chunk size must be a power of two"
|
| 178 |
+
for n in (C // (2 * s) for s in (2**i for i in range(int(math.log2(C))))):
|
| 179 |
+
self.register_buffer(f"block_eye_{n}", torch.eye(n), persistent=False)
|
| 180 |
+
|
| 181 |
+
def unit_lower_inverse(self, t: torch.Tensor) -> torch.Tensor:
|
| 182 |
+
"""Inverse of unit-lower-triangular [..., C, C] by recursive 2x2 blocking:
|
| 183 |
+
[[A, 0], [X, B]]^-1 = [[A^-1, 0], [-B^-1 X A^-1, B^-1]].
|
| 184 |
+
|
| 185 |
+
Stable like forward substitution (it only forms the true inverse's sub-blocks),
|
| 186 |
+
but log2(C) batched levels instead of C sequential row updates."""
|
| 187 |
+
C = self.chunk
|
| 188 |
+
lead = t.shape[:-2]
|
| 189 |
+
t = t.reshape(-1, C, C) # Core ML tensors are rank <= 5
|
| 190 |
+
b = t.shape[0]
|
| 191 |
+
inv = torch.ones(b, C, 1, 1, dtype=t.dtype, device=t.device) # 1x1 diagonal blocks of a unit triangle
|
| 192 |
+
s = 1
|
| 193 |
+
while s < C:
|
| 194 |
+
n2 = C // (2 * s)
|
| 195 |
+
# diagonal 2s x 2s blocks of t, then their lower-left s x s part
|
| 196 |
+
blocks = (t.reshape(b, n2, 2 * s, n2, 2 * s) * getattr(self, f"block_eye_{n2}")[:, None, :, None]).sum(-2)
|
| 197 |
+
x = blocks[..., s:, :s] # [b, n2, s, s]
|
| 198 |
+
pair = inv.reshape(b, n2, 2, s, s)
|
| 199 |
+
a_inv, b_inv = pair[..., 0, :, :], pair[..., 1, :, :]
|
| 200 |
+
lower = -torch.matmul(b_inv, torch.matmul(x, a_inv))
|
| 201 |
+
top = torch.cat([a_inv, torch.zeros_like(a_inv)], dim=-1)
|
| 202 |
+
bottom = torch.cat([lower, b_inv], dim=-1)
|
| 203 |
+
inv = torch.cat([top, bottom], dim=-2) # [b, n2, 2s, 2s]
|
| 204 |
+
s *= 2
|
| 205 |
+
return inv.reshape(*lead, C, C)
|
| 206 |
+
|
| 207 |
+
def forward(self, x):
|
| 208 |
+
cfg = self.cfg
|
| 209 |
+
L = x.shape[1]
|
| 210 |
+
C, N = self.chunk, self.num_chunks
|
| 211 |
+
H, Dk, Dv = cfg.lin_v_heads, cfg.lin_k_dim, cfg.lin_v_dim
|
| 212 |
+
rep = cfg.lin_v_heads // cfg.lin_k_heads
|
| 213 |
+
|
| 214 |
+
qkv = self.in_proj_qkv(x).transpose(1, 2) # [1, conv_dim, L]
|
| 215 |
+
qkv = F.silu(self.conv1d(F.pad(qkv, (cfg.conv_kernel - 1, 0)))).transpose(1, 2)
|
| 216 |
+
q = qkv[..., : self.key_dim].reshape(1, L, cfg.lin_k_heads, Dk)
|
| 217 |
+
k = qkv[..., self.key_dim : 2 * self.key_dim].reshape(1, L, cfg.lin_k_heads, Dk)
|
| 218 |
+
v = qkv[..., 2 * self.key_dim :].reshape(1, L, H, Dv)
|
| 219 |
+
z = self.in_proj_z(x).reshape(1, L, H, Dv)
|
| 220 |
+
beta = torch.sigmoid(self.in_proj_b(x)) # [1, L, H]
|
| 221 |
+
g = -torch.exp(self.A_log) * F.softplus(self.in_proj_a(x) + self.dt_bias) # [1, L, H], <= 0
|
| 222 |
+
|
| 223 |
+
# l2norm (FLA convention) then repeat k heads up to v heads
|
| 224 |
+
q = q * torch.rsqrt((q * q).sum(-1, keepdim=True) + 1e-6)
|
| 225 |
+
k = k * torch.rsqrt((k * k).sum(-1, keepdim=True) + 1e-6)
|
| 226 |
+
q = q[:, :, :, None].expand(1, L, cfg.lin_k_heads, rep, Dk).reshape(1, L, H, Dk)
|
| 227 |
+
k = k[:, :, :, None].expand(1, L, cfg.lin_k_heads, rep, Dk).reshape(1, L, H, Dk)
|
| 228 |
+
q = q * (Dk**-0.5)
|
| 229 |
+
|
| 230 |
+
# -> [H, N, C, D]
|
| 231 |
+
q = q[0].permute(1, 0, 2).reshape(H, N, C, Dk)
|
| 232 |
+
k = k[0].permute(1, 0, 2).reshape(H, N, C, Dk)
|
| 233 |
+
v = v[0].permute(1, 0, 2).reshape(H, N, C, Dv)
|
| 234 |
+
beta = beta[0].T.reshape(H, N, C, 1)
|
| 235 |
+
g = g[0].T.reshape(H, N, C)
|
| 236 |
+
|
| 237 |
+
# constant-first matmuls: `x @ const` would lower to Core ML `linear` and be picked up by weight compression
|
| 238 |
+
g_col = g.unsqueeze(-1) # [H, N, C, 1]
|
| 239 |
+
cum = torch.matmul(self.prefix_sum_t, g_col).squeeze(-1) # cum_i = sum_{k<=i} g_k
|
| 240 |
+
to_end = torch.matmul(self.suffix_sum_t, g_col).squeeze(-1) # sum_{k>i} g_k = cum_last - cum_i
|
| 241 |
+
pair = torch.matmul(self.pair_sum_t, g_col).reshape(H, N, C, C) # cum_i - cum_j (0 above diagonal)
|
| 242 |
+
pair_decay = torch.exp(pair) * self.lower_incl
|
| 243 |
+
|
| 244 |
+
k_beta = k * beta
|
| 245 |
+
v_beta = v * beta
|
| 246 |
+
kkt = torch.matmul(k_beta, k.transpose(-1, -2)) * pair_decay
|
| 247 |
+
attn_intra = torch.matmul(q, k.transpose(-1, -2)) * pair_decay
|
| 248 |
+
|
| 249 |
+
inv = self.unit_lower_inverse(self.eye + kkt * self.strict_lower)
|
| 250 |
+
new_v = torch.matmul(inv, v_beta) # [H, N, C, Dv]
|
| 251 |
+
k_cumdecay = torch.matmul(inv, k_beta * torch.exp(cum)[..., None]) # [H, N, C, Dk]
|
| 252 |
+
|
| 253 |
+
q_dec = q * torch.exp(cum)[..., None]
|
| 254 |
+
k_dec = k * torch.exp(to_end)[..., None]
|
| 255 |
+
chunk_decay = torch.exp(cum[..., -1:])[..., None] # [H, N, 1, 1]
|
| 256 |
+
|
| 257 |
+
state = torch.zeros(H, Dk, Dv, dtype=x.dtype, device=x.device)
|
| 258 |
+
outs = []
|
| 259 |
+
for i in range(N):
|
| 260 |
+
v_new = new_v[:, i] - torch.matmul(k_cumdecay[:, i], state)
|
| 261 |
+
outs.append(torch.matmul(q_dec[:, i], state) + torch.matmul(attn_intra[:, i], v_new))
|
| 262 |
+
if i + 1 < N:
|
| 263 |
+
state = state * chunk_decay[:, i] + torch.matmul(k_dec[:, i].transpose(-1, -2), v_new)
|
| 264 |
+
core = torch.stack(outs, dim=1).reshape(H, L, Dv).permute(1, 0, 2) # [L, H, Dv]
|
| 265 |
+
|
| 266 |
+
core = self.norm(core, z[0])
|
| 267 |
+
return self.out_proj(core.reshape(1, L, H * Dv))
|
| 268 |
+
|
| 269 |
+
|
| 270 |
+
class DecoderLayer(nn.Module):
|
| 271 |
+
def __init__(self, cfg: TextConfig, layer_idx: int, seq_len: int, chunk_size: int):
|
| 272 |
+
super().__init__()
|
| 273 |
+
self.is_linear = cfg.layer_types[layer_idx] == "linear_attention"
|
| 274 |
+
if self.is_linear:
|
| 275 |
+
self.linear_attn = GatedDeltaNet(cfg, seq_len, chunk_size)
|
| 276 |
+
else:
|
| 277 |
+
self.self_attn = FullAttention(cfg)
|
| 278 |
+
self.mlp = MLP(cfg)
|
| 279 |
+
self.input_layernorm = RMSNorm(cfg.hidden_size, cfg.eps)
|
| 280 |
+
self.post_attention_layernorm = RMSNorm(cfg.hidden_size, cfg.eps)
|
| 281 |
+
|
| 282 |
+
def forward(self, x, cos, sin, mask):
|
| 283 |
+
h = self.input_layernorm(x)
|
| 284 |
+
h = self.linear_attn(h) if self.is_linear else self.self_attn(h, cos, sin, mask)
|
| 285 |
+
x = x + h
|
| 286 |
+
return x + self.mlp(self.post_attention_layernorm(x))
|
| 287 |
+
|
| 288 |
+
|
| 289 |
+
class DecoderChunk(nn.Module):
|
| 290 |
+
"""Layers [start, end). The last chunk (with_head) returns letter logits [1, n_letters]."""
|
| 291 |
+
|
| 292 |
+
def __init__(
|
| 293 |
+
self,
|
| 294 |
+
cfg: TextConfig,
|
| 295 |
+
start: int,
|
| 296 |
+
end: int,
|
| 297 |
+
seq_len: int,
|
| 298 |
+
chunk_size: int = 64,
|
| 299 |
+
with_head: bool = False,
|
| 300 |
+
n_letters: int = 26,
|
| 301 |
+
):
|
| 302 |
+
super().__init__()
|
| 303 |
+
self.start, self.end, self.with_head = start, end, with_head
|
| 304 |
+
self.layers = nn.ModuleList([DecoderLayer(cfg, i, seq_len, chunk_size) for i in range(start, end)])
|
| 305 |
+
mask = torch.full((seq_len, seq_len), -1e4).triu(1)
|
| 306 |
+
self.register_buffer("mask", mask[None, None], persistent=False)
|
| 307 |
+
if with_head:
|
| 308 |
+
self.norm = RMSNorm(cfg.hidden_size, cfg.eps)
|
| 309 |
+
self.letter_head = nn.Linear(cfg.hidden_size, n_letters, bias=False)
|
| 310 |
+
|
| 311 |
+
def forward(self, hidden, cos, sin, last_onehot=None):
|
| 312 |
+
for layer in self.layers:
|
| 313 |
+
hidden = layer(hidden, cos, sin, self.mask)
|
| 314 |
+
if not self.with_head:
|
| 315 |
+
return hidden
|
| 316 |
+
last = torch.matmul(last_onehot, hidden[0]) # [1, hidden]
|
| 317 |
+
return self.letter_head(self.norm(last))
|
| 318 |
+
|
| 319 |
+
def load_merged(self, state: dict[str, torch.Tensor], letter_rows: torch.Tensor | None = None):
|
| 320 |
+
"""`state` uses Qwen3_5TextModel names relative to the language model (`layers.N...`, `norm.weight`)."""
|
| 321 |
+
own = {}
|
| 322 |
+
for local, global_idx in enumerate(range(self.start, self.end)):
|
| 323 |
+
prefix = f"layers.{global_idx}."
|
| 324 |
+
for key, value in state.items():
|
| 325 |
+
if key.startswith(prefix):
|
| 326 |
+
own[f"layers.{local}.{key[len(prefix):]}"] = value
|
| 327 |
+
if self.with_head:
|
| 328 |
+
own["norm.weight"] = state["norm.weight"]
|
| 329 |
+
own["letter_head.weight"] = letter_rows
|
| 330 |
+
missing, unexpected = self.load_state_dict({k: v.float() for k, v in own.items()}, strict=False)
|
| 331 |
+
missing = [m for m in missing if not m.endswith(("mask",))]
|
| 332 |
+
if missing or unexpected:
|
| 333 |
+
raise RuntimeError(f"weight mismatch: missing={missing[:8]} unexpected={unexpected[:8]}")
|
lite/python/vision_export.py
ADDED
|
@@ -0,0 +1,230 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Trace-friendly Qwen3.5 vision tower for Core ML (from clef-vision-0.8b; d3 weight names): one image per call, patches right-padded to a fixed count.
|
| 2 |
+
|
| 3 |
+
Inputs (all fp32, host-prepared from the HF processor's ``pixel_values`` / ``image_grid_thw``):
|
| 4 |
+
patches [N, 1536] flattened (C, T, P, P) patches, zero padded after the image's n patches
|
| 5 |
+
pos_embeds [N, 768] bilinear-resampled learned position grid (host: HF interpolation indices/weights)
|
| 6 |
+
cos, sin [N, 64] axial 2D rope tables (host)
|
| 7 |
+
mask [1, 1, N, N] additive key mask: 0 for real patches, -1e4 for padding
|
| 8 |
+
Output: merged tokens [N/4, 1024]; rows [0, n/4) are the image's tokens in the order the language model expects.
|
| 9 |
+
|
| 10 |
+
Padding is appended after the image, so with the key mask real patches never attend to it; the merger groups four
|
| 11 |
+
consecutive patches, and n is always a multiple of 4 (grid h, w are multiples of the merge size), so no group mixes
|
| 12 |
+
real and padded patches. Parity vs ``Qwen3_5VisionModel`` is checked in ``vision_parity``.
|
| 13 |
+
"""
|
| 14 |
+
from __future__ import annotations
|
| 15 |
+
|
| 16 |
+
import argparse
|
| 17 |
+
import json
|
| 18 |
+
import math
|
| 19 |
+
import time
|
| 20 |
+
from pathlib import Path
|
| 21 |
+
|
| 22 |
+
import numpy as np
|
| 23 |
+
import torch
|
| 24 |
+
import torch.nn.functional as F
|
| 25 |
+
from torch import nn
|
| 26 |
+
|
| 27 |
+
VISION_PREFIX = "visual."
|
| 28 |
+
|
| 29 |
+
|
| 30 |
+
class VisionConfig:
|
| 31 |
+
def __init__(self, cfg: dict):
|
| 32 |
+
self.depth = cfg["depth"]
|
| 33 |
+
self.hidden = cfg["hidden_size"]
|
| 34 |
+
self.heads = cfg["num_heads"]
|
| 35 |
+
self.head_dim = self.hidden // self.heads
|
| 36 |
+
self.intermediate = cfg["intermediate_size"]
|
| 37 |
+
self.out_hidden = cfg["out_hidden_size"]
|
| 38 |
+
self.patch = cfg["patch_size"]
|
| 39 |
+
self.temporal = cfg["temporal_patch_size"]
|
| 40 |
+
self.channels = cfg["in_channels"]
|
| 41 |
+
self.merge = cfg["spatial_merge_size"]
|
| 42 |
+
self.num_pos = cfg["num_position_embeddings"]
|
| 43 |
+
self.act = cfg["hidden_act"] # gelu_pytorch_tanh for the blocks; the merger uses exact GELU
|
| 44 |
+
self.rope_theta = cfg.get("rope_parameters", {}).get("rope_theta", 10000.0)
|
| 45 |
+
self.patch_dim = self.channels * self.temporal * self.patch * self.patch
|
| 46 |
+
|
| 47 |
+
|
| 48 |
+
def rotate_half(x: torch.Tensor) -> torch.Tensor:
|
| 49 |
+
x1, x2 = x.chunk(2, dim=-1)
|
| 50 |
+
return torch.cat([-x2, x1], dim=-1)
|
| 51 |
+
|
| 52 |
+
|
| 53 |
+
class Block(nn.Module):
|
| 54 |
+
def __init__(self, cfg: VisionConfig):
|
| 55 |
+
super().__init__()
|
| 56 |
+
self.cfg = cfg
|
| 57 |
+
self.norm1 = nn.LayerNorm(cfg.hidden, eps=1e-6)
|
| 58 |
+
self.norm2 = nn.LayerNorm(cfg.hidden, eps=1e-6)
|
| 59 |
+
self.qkv = nn.Linear(cfg.hidden, cfg.hidden * 3)
|
| 60 |
+
self.proj = nn.Linear(cfg.hidden, cfg.hidden)
|
| 61 |
+
self.fc1 = nn.Linear(cfg.hidden, cfg.intermediate)
|
| 62 |
+
self.fc2 = nn.Linear(cfg.intermediate, cfg.hidden)
|
| 63 |
+
self.scale = cfg.head_dim**-0.5
|
| 64 |
+
|
| 65 |
+
def forward(self, x, cos, sin, mask):
|
| 66 |
+
n = x.shape[0]
|
| 67 |
+
h = self.norm1(x)
|
| 68 |
+
qkv = self.qkv(h).reshape(n, 3, self.cfg.heads, self.cfg.head_dim)
|
| 69 |
+
q, k, v = qkv[:, 0], qkv[:, 1], qkv[:, 2] # [N, H, d]
|
| 70 |
+
c, s = cos[:, None, :], sin[:, None, :]
|
| 71 |
+
q = q * c + rotate_half(q) * s
|
| 72 |
+
k = k * c + rotate_half(k) * s
|
| 73 |
+
q = q.permute(1, 0, 2)[None] # [1, H, N, d]
|
| 74 |
+
k = k.permute(1, 0, 2)[None]
|
| 75 |
+
v = v.permute(1, 0, 2)[None]
|
| 76 |
+
scores = torch.matmul(q, k.transpose(-1, -2)) * self.scale + mask
|
| 77 |
+
attn = torch.matmul(torch.softmax(scores, dim=-1), v) # [1, H, N, d]
|
| 78 |
+
attn = attn[0].permute(1, 0, 2).reshape(n, self.cfg.hidden)
|
| 79 |
+
x = x + self.proj(attn)
|
| 80 |
+
h = self.norm2(x)
|
| 81 |
+
act = F.gelu(self.fc1(h), approximate="tanh" if self.cfg.act == "gelu_pytorch_tanh" else "none")
|
| 82 |
+
return x + self.fc2(act)
|
| 83 |
+
|
| 84 |
+
|
| 85 |
+
class VisionTower(nn.Module):
|
| 86 |
+
def __init__(self, cfg: VisionConfig, n_patches: int):
|
| 87 |
+
super().__init__()
|
| 88 |
+
assert n_patches % (cfg.merge**2) == 0
|
| 89 |
+
self.cfg = cfg
|
| 90 |
+
self.n_patches = n_patches
|
| 91 |
+
self.patch_embed = nn.Linear(cfg.patch_dim, cfg.hidden) # Conv3d with kernel == stride == patch
|
| 92 |
+
self.blocks = nn.ModuleList([Block(cfg) for _ in range(cfg.depth)])
|
| 93 |
+
merged = cfg.hidden * cfg.merge**2
|
| 94 |
+
self.merger_norm = nn.LayerNorm(cfg.hidden, eps=1e-6)
|
| 95 |
+
self.merger_fc1 = nn.Linear(merged, merged)
|
| 96 |
+
self.merger_fc2 = nn.Linear(merged, cfg.out_hidden)
|
| 97 |
+
|
| 98 |
+
def forward(self, patches, pos_embeds, cos, sin, mask):
|
| 99 |
+
x = self.patch_embed(patches) + pos_embeds
|
| 100 |
+
for block in self.blocks:
|
| 101 |
+
x = block(x, cos, sin, mask)
|
| 102 |
+
x = self.merger_norm(x).reshape(self.n_patches // self.cfg.merge**2, -1)
|
| 103 |
+
return self.merger_fc2(F.gelu(self.merger_fc1(x)))
|
| 104 |
+
|
| 105 |
+
def load_hf(self, state: dict[str, torch.Tensor]) -> None:
|
| 106 |
+
"""``state`` holds the HF vision weights with the ``visual.`` prefix stripped."""
|
| 107 |
+
own = {
|
| 108 |
+
"patch_embed.weight": state["patch_embed.proj.weight"].reshape(self.cfg.hidden, -1),
|
| 109 |
+
"patch_embed.bias": state["patch_embed.proj.bias"],
|
| 110 |
+
"merger_norm.weight": state["merger.norm.weight"],
|
| 111 |
+
"merger_norm.bias": state["merger.norm.bias"],
|
| 112 |
+
"merger_fc1.weight": state["merger.linear_fc1.weight"],
|
| 113 |
+
"merger_fc1.bias": state["merger.linear_fc1.bias"],
|
| 114 |
+
"merger_fc2.weight": state["merger.linear_fc2.weight"],
|
| 115 |
+
"merger_fc2.bias": state["merger.linear_fc2.bias"],
|
| 116 |
+
}
|
| 117 |
+
for i in range(self.cfg.depth):
|
| 118 |
+
src, dst = f"blocks.{i}.", f"blocks.{i}."
|
| 119 |
+
for a, b in (("norm1", "norm1"), ("norm2", "norm2"), ("attn.qkv", "qkv"), ("attn.proj", "proj"),
|
| 120 |
+
("mlp.linear_fc1", "fc1"), ("mlp.linear_fc2", "fc2")):
|
| 121 |
+
own[f"{dst}{b}.weight"] = state[f"{src}{a}.weight"]
|
| 122 |
+
own[f"{dst}{b}.bias"] = state[f"{src}{a}.bias"]
|
| 123 |
+
missing, unexpected = self.load_state_dict({k: v.float() for k, v in own.items()}, strict=False)
|
| 124 |
+
if missing or unexpected:
|
| 125 |
+
raise RuntimeError(f"vision weight mismatch: missing={missing[:6]} unexpected={unexpected[:6]}")
|
| 126 |
+
|
| 127 |
+
|
| 128 |
+
# ----------------------------------------------------------------------------------------------------------------------
|
| 129 |
+
# Host-side preparation (numpy/torch; mirrors transformers.vision_utils + Qwen3_5VisionRotaryEmbedding)
|
| 130 |
+
|
| 131 |
+
def axial_rope(cfg: VisionConfig, position_ids: torch.Tensor) -> tuple[torch.Tensor, torch.Tensor]:
|
| 132 |
+
"""``position_ids`` [n, 2] (h, w) -> cos/sin [n, head_dim]: freqs over head_dim//4 per axis, concat (h, w), x2."""
|
| 133 |
+
spatial = cfg.head_dim // 2
|
| 134 |
+
inv_freq = 1.0 / (cfg.rope_theta ** (torch.arange(0, spatial, 2, dtype=torch.float32) / spatial))
|
| 135 |
+
freqs = position_ids[..., None].float() * inv_freq # [n, 2, spatial/2]
|
| 136 |
+
hw = torch.cat([freqs[:, 0], freqs[:, 1]], dim=-1)
|
| 137 |
+
emb = torch.cat([hw, hw], dim=-1)
|
| 138 |
+
return emb.cos(), emb.sin()
|
| 139 |
+
|
| 140 |
+
|
| 141 |
+
def prepare_inputs(cfg: VisionConfig, pos_table: torch.Tensor, pixel_values: torch.Tensor, grid_thw: torch.Tensor,
|
| 142 |
+
n_patches: int):
|
| 143 |
+
"""One image. Uses the HF helpers for position ids and pos-embed interpolation so the host matches the reference."""
|
| 144 |
+
from transformers.vision_utils import get_vision_interpolation_indices_and_weights, get_vision_position_ids
|
| 145 |
+
|
| 146 |
+
n = int(pixel_values.shape[0])
|
| 147 |
+
if n > n_patches:
|
| 148 |
+
raise ValueError(f"image has {n} patches; bucket holds {n_patches}")
|
| 149 |
+
grid = grid_thw.reshape(1, 3)
|
| 150 |
+
idx, w = get_vision_interpolation_indices_and_weights(
|
| 151 |
+
grid, num_grid_per_side=int(math.isqrt(cfg.num_pos)), mode="bilinear", align_corners=True,
|
| 152 |
+
spatial_merge_size=cfg.merge, kwargs={})
|
| 153 |
+
pos_embeds = (pos_table[idx] * w[:, :, None]).sum(1) # [n, hidden]
|
| 154 |
+
position_ids = get_vision_position_ids(grid, cfg.merge, kwargs={}) # [n, 2]
|
| 155 |
+
cos, sin = axial_rope(cfg, position_ids)
|
| 156 |
+
patches = torch.zeros(n_patches, cfg.patch_dim)
|
| 157 |
+
patches[:n] = pixel_values.float()
|
| 158 |
+
pe = torch.zeros(n_patches, cfg.hidden)
|
| 159 |
+
pe[:n] = pos_embeds.float()
|
| 160 |
+
cs = torch.zeros(n_patches, cfg.head_dim)
|
| 161 |
+
sn = torch.zeros(n_patches, cfg.head_dim)
|
| 162 |
+
cs[:n], sn[:n] = cos, sin
|
| 163 |
+
mask = torch.zeros(1, 1, n_patches, n_patches)
|
| 164 |
+
mask[..., n:] = -1e4
|
| 165 |
+
return patches, pe, cs, sn, mask, n
|
| 166 |
+
|
| 167 |
+
|
| 168 |
+
def load_vision(merged_backbone: Path, n_patches: int):
|
| 169 |
+
from safetensors.torch import load_file
|
| 170 |
+
|
| 171 |
+
config = json.loads((merged_backbone / "config.json").read_text())
|
| 172 |
+
cfg = VisionConfig(config["vision_config"])
|
| 173 |
+
state = {}
|
| 174 |
+
for path in sorted(merged_backbone.glob("model*.safetensors")): # one file or shards
|
| 175 |
+
state.update({k[len(VISION_PREFIX):]: v for k, v in load_file(str(path)).items() if k.startswith(VISION_PREFIX)})
|
| 176 |
+
tower = VisionTower(cfg, n_patches)
|
| 177 |
+
tower.load_hf(state)
|
| 178 |
+
return tower.eval(), cfg, state["pos_embed.weight"].float()
|
| 179 |
+
|
| 180 |
+
|
| 181 |
+
def main() -> None:
|
| 182 |
+
ap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
|
| 183 |
+
ap.add_argument("--merged", type=Path, required=True, help="d3 checkpoint dir")
|
| 184 |
+
ap.add_argument("--patches", type=int, default=784, help="patch bucket (784 = 448x448 cap -> 196 tokens)")
|
| 185 |
+
ap.add_argument("--precision", choices=("fp16", "fp32", "mixed"), default="fp32",
|
| 186 |
+
help="fp32 is the only exact option: fp16 (0.27 max err) and mixed with fp32 norms/softmax "
|
| 187 |
+
"(0.33) both break on this ViT's activation range; fp32 runs 35 ms on the M5 Pro GPU")
|
| 188 |
+
ap.add_argument("--out", type=Path, default=Path("build/coreml"))
|
| 189 |
+
args = ap.parse_args()
|
| 190 |
+
import coremltools as ct
|
| 191 |
+
|
| 192 |
+
tower, cfg, pos_table = load_vision(args.merged, args.patches)
|
| 193 |
+
N = args.patches
|
| 194 |
+
example = (torch.zeros(N, cfg.patch_dim), torch.zeros(N, cfg.hidden), torch.zeros(N, cfg.head_dim),
|
| 195 |
+
torch.zeros(N, cfg.head_dim), torch.zeros(1, 1, N, N))
|
| 196 |
+
started = time.time()
|
| 197 |
+
with torch.no_grad():
|
| 198 |
+
traced = torch.jit.trace(tower, example, check_trace=False)
|
| 199 |
+
names = ["patches", "pos_embeds", "cos", "sin", "mask"]
|
| 200 |
+
if args.precision == "fp32":
|
| 201 |
+
precision = ct.precision.FLOAT32
|
| 202 |
+
elif args.precision == "fp16":
|
| 203 |
+
precision = ct.precision.FLOAT16
|
| 204 |
+
else:
|
| 205 |
+
keep_fp32 = {"layer_norm", "softmax", "reduce_mean", "reduce_sum", "pow", "sqrt", "rsqrt", "real_div"}
|
| 206 |
+
precision = ct.transform.FP16ComputePrecision(op_selector=lambda op: op.op_type not in keep_fp32)
|
| 207 |
+
model = ct.convert(
|
| 208 |
+
traced, convert_to="mlprogram", minimum_deployment_target=ct.target.iOS17,
|
| 209 |
+
compute_precision=precision,
|
| 210 |
+
compute_units=ct.ComputeUnit.CPU_ONLY,
|
| 211 |
+
inputs=[ct.TensorType(name=n, shape=tuple(t.shape), dtype=np.float32) for n, t in zip(names, example)],
|
| 212 |
+
outputs=[ct.TensorType(name="tokens", dtype=np.float32)],
|
| 213 |
+
)
|
| 214 |
+
model.short_description = "d3 vision tower (vLLM-SR Decision 3.0): one image, fixed patch bucket"
|
| 215 |
+
model.license = "Apache-2.0"
|
| 216 |
+
model.user_defined_metadata.update({"patches": str(N), "tokens": str(N // cfg.merge**2)})
|
| 217 |
+
out = args.out / f"Vision_P{N}"
|
| 218 |
+
out.mkdir(parents=True, exist_ok=True)
|
| 219 |
+
package = out / f"VisionTower_{args.precision}.mlpackage"
|
| 220 |
+
model.save(str(package))
|
| 221 |
+
pos_table.numpy().astype(np.float32).tofile(out / "pos_embed.f32") # the host resamples this per image
|
| 222 |
+
(out / "config.json").write_text(json.dumps({"patches": N, "tokens": N // cfg.merge**2, "patch_dim": cfg.patch_dim,
|
| 223 |
+
"hidden": cfg.hidden, "head_dim": cfg.head_dim,
|
| 224 |
+
"out_hidden": cfg.out_hidden, "precision": args.precision},
|
| 225 |
+
indent=2) + "\n")
|
| 226 |
+
print(f"saved {package} in {time.time() - started:.0f} s")
|
| 227 |
+
|
| 228 |
+
|
| 229 |
+
if __name__ == "__main__":
|
| 230 |
+
main()
|
lite/readout.f32
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:7772300f2e211f3c2028a16b9f493e941c2343bd14a9131145eb34da39f36448
|
| 3 |
+
size 1044480
|
lite/tokenizer.json
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:5f9e4d4901a92b997e463c1f46055088b6cca5ca61a6522d1b9f64c4bb81cb42
|
| 3 |
+
size 12807982
|
lite/vision_P1536.mlpackage/Data/com.apple.CoreML/model.mlmodel
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:eec6afaa226859a3d717d2ced945947b3663d1b641076d0abc5e96af19b61d57
|
| 3 |
+
size 153343
|
lite/vision_P1536.mlpackage/Data/com.apple.CoreML/weights/weight.bin
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:3d571710dd88a5f2e34d0b51b815a839bc0b53425eca17a45f965d474218ec6e
|
| 3 |
+
size 395303488
|
lite/vision_P1536.mlpackage/Manifest.json
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"fileFormatVersion": "1.0.0",
|
| 3 |
+
"itemInfoEntries": {
|
| 4 |
+
"ABBDA214-38F9-44F0-A089-B636CD88D5C9": {
|
| 5 |
+
"author": "com.apple.CoreML",
|
| 6 |
+
"description": "CoreML Model Specification",
|
| 7 |
+
"name": "model.mlmodel",
|
| 8 |
+
"path": "com.apple.CoreML/model.mlmodel"
|
| 9 |
+
},
|
| 10 |
+
"E4544D1C-B3E4-4D3A-A03E-147542E6B2CD": {
|
| 11 |
+
"author": "com.apple.CoreML",
|
| 12 |
+
"description": "CoreML Model Weights",
|
| 13 |
+
"name": "weights",
|
| 14 |
+
"path": "com.apple.CoreML/weights"
|
| 15 |
+
}
|
| 16 |
+
},
|
| 17 |
+
"rootModelIdentifier": "ABBDA214-38F9-44F0-A089-B636CD88D5C9"
|
| 18 |
+
}
|
lite/vision_P3072.mlpackage/Data/com.apple.CoreML/model.mlmodel
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:4d6a1ec684714a1aeb552cb699b9e5303a342db6c0bec6ab481abe1581df1810
|
| 3 |
+
size 153343
|
lite/vision_P3072.mlpackage/Data/com.apple.CoreML/weights/weight.bin
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:3d571710dd88a5f2e34d0b51b815a839bc0b53425eca17a45f965d474218ec6e
|
| 3 |
+
size 395303488
|
lite/vision_P3072.mlpackage/Manifest.json
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"fileFormatVersion": "1.0.0",
|
| 3 |
+
"itemInfoEntries": {
|
| 4 |
+
"77FBF011-9CB1-4E8C-8C83-A7CA7E3C7916": {
|
| 5 |
+
"author": "com.apple.CoreML",
|
| 6 |
+
"description": "CoreML Model Specification",
|
| 7 |
+
"name": "model.mlmodel",
|
| 8 |
+
"path": "com.apple.CoreML/model.mlmodel"
|
| 9 |
+
},
|
| 10 |
+
"A8B8926F-B70F-4353-AE83-938EC417390B": {
|
| 11 |
+
"author": "com.apple.CoreML",
|
| 12 |
+
"description": "CoreML Model Weights",
|
| 13 |
+
"name": "weights",
|
| 14 |
+
"path": "com.apple.CoreML/weights"
|
| 15 |
+
}
|
| 16 |
+
},
|
| 17 |
+
"rootModelIdentifier": "77FBF011-9CB1-4E8C-8C83-A7CA7E3C7916"
|
| 18 |
+
}
|
lite/vision_P320.mlpackage/Data/com.apple.CoreML/model.mlmodel
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:9c6e604d1d2e1c2b63d5cd01a0263e0245c10b7a41c16bb177953c9208166b8b
|
| 3 |
+
size 153335
|
lite/vision_P320.mlpackage/Data/com.apple.CoreML/weights/weight.bin
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:3d571710dd88a5f2e34d0b51b815a839bc0b53425eca17a45f965d474218ec6e
|
| 3 |
+
size 395303488
|
lite/vision_P320.mlpackage/Manifest.json
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"fileFormatVersion": "1.0.0",
|
| 3 |
+
"itemInfoEntries": {
|
| 4 |
+
"02E1986B-12A5-4C92-8F54-8B6A3CADE0DF": {
|
| 5 |
+
"author": "com.apple.CoreML",
|
| 6 |
+
"description": "CoreML Model Weights",
|
| 7 |
+
"name": "weights",
|
| 8 |
+
"path": "com.apple.CoreML/weights"
|
| 9 |
+
},
|
| 10 |
+
"AB4963EB-278E-43AA-B7F3-BAC7CACD9427": {
|
| 11 |
+
"author": "com.apple.CoreML",
|
| 12 |
+
"description": "CoreML Model Specification",
|
| 13 |
+
"name": "model.mlmodel",
|
| 14 |
+
"path": "com.apple.CoreML/model.mlmodel"
|
| 15 |
+
}
|
| 16 |
+
},
|
| 17 |
+
"rootModelIdentifier": "AB4963EB-278E-43AA-B7F3-BAC7CACD9427"
|
| 18 |
+
}
|