Add files using upload-large-folder tool
Browse files- .gitattributes +1 -35
- .gitignore +7 -0
- .python-version +1 -0
- DeepSeek-V4-Flash-0731-DSpark-Drafter-MXFP4-Q8_0.gguf +3 -0
- LICENSE +21 -0
- LICENSE.code +21 -0
- README.md +138 -0
- SHA256SUMS +1 -0
- THIRD_PARTY_NOTICES.md +52 -0
- manifest/build.json +38 -0
- manifest/source.json +19 -0
- manifest/validation.json +46 -0
- requirements-linux-aarch64-py314.lock +4 -0
- scripts/convert.py +653 -0
- scripts/download_sources.py +70 -0
- scripts/formats.py +456 -0
- scripts/recipe.py +519 -0
- scripts/reproduce.py +227 -0
- scripts/verify.py +613 -0
- tests/test_recipe.py +306 -0
.gitattributes
CHANGED
|
@@ -1,35 +1 @@
|
|
| 1 |
-
*.
|
| 2 |
-
*.arrow filter=lfs diff=lfs merge=lfs -text
|
| 3 |
-
*.bin filter=lfs diff=lfs merge=lfs -text
|
| 4 |
-
*.bz2 filter=lfs diff=lfs merge=lfs -text
|
| 5 |
-
*.ckpt filter=lfs diff=lfs merge=lfs -text
|
| 6 |
-
*.ftz filter=lfs diff=lfs merge=lfs -text
|
| 7 |
-
*.gz filter=lfs diff=lfs merge=lfs -text
|
| 8 |
-
*.h5 filter=lfs diff=lfs merge=lfs -text
|
| 9 |
-
*.joblib filter=lfs diff=lfs merge=lfs -text
|
| 10 |
-
*.lfs.* filter=lfs diff=lfs merge=lfs -text
|
| 11 |
-
*.mlmodel filter=lfs diff=lfs merge=lfs -text
|
| 12 |
-
*.model filter=lfs diff=lfs merge=lfs -text
|
| 13 |
-
*.msgpack filter=lfs diff=lfs merge=lfs -text
|
| 14 |
-
*.npy filter=lfs diff=lfs merge=lfs -text
|
| 15 |
-
*.npz filter=lfs diff=lfs merge=lfs -text
|
| 16 |
-
*.onnx filter=lfs diff=lfs merge=lfs -text
|
| 17 |
-
*.ot filter=lfs diff=lfs merge=lfs -text
|
| 18 |
-
*.parquet filter=lfs diff=lfs merge=lfs -text
|
| 19 |
-
*.pb filter=lfs diff=lfs merge=lfs -text
|
| 20 |
-
*.pickle filter=lfs diff=lfs merge=lfs -text
|
| 21 |
-
*.pkl filter=lfs diff=lfs merge=lfs -text
|
| 22 |
-
*.pt filter=lfs diff=lfs merge=lfs -text
|
| 23 |
-
*.pth filter=lfs diff=lfs merge=lfs -text
|
| 24 |
-
*.rar filter=lfs diff=lfs merge=lfs -text
|
| 25 |
-
*.safetensors filter=lfs diff=lfs merge=lfs -text
|
| 26 |
-
saved_model/**/* filter=lfs diff=lfs merge=lfs -text
|
| 27 |
-
*.tar.* filter=lfs diff=lfs merge=lfs -text
|
| 28 |
-
*.tar filter=lfs diff=lfs merge=lfs -text
|
| 29 |
-
*.tflite filter=lfs diff=lfs merge=lfs -text
|
| 30 |
-
*.tgz filter=lfs diff=lfs merge=lfs -text
|
| 31 |
-
*.wasm filter=lfs diff=lfs merge=lfs -text
|
| 32 |
-
*.xz 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
|
|
|
|
| 1 |
+
*.gguf filter=lfs diff=lfs merge=lfs -text
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
.gitignore
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
.venv/
|
| 2 |
+
sources/
|
| 3 |
+
upstream-metadata/
|
| 4 |
+
*.partial-*
|
| 5 |
+
.*.repeat-check
|
| 6 |
+
__pycache__/
|
| 7 |
+
*.pyc
|
.python-version
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
3.14.6
|
DeepSeek-V4-Flash-0731-DSpark-Drafter-MXFP4-Q8_0.gguf
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:5c8a1ce75e4797c170e22ac65080281a1b694c77f2e4fa7a703fbf09c5de0e33
|
| 3 |
+
size 10897110272
|
LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
MIT License
|
| 2 |
+
|
| 3 |
+
Copyright (c) 2023 DeepSeek
|
| 4 |
+
|
| 5 |
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
| 6 |
+
of this software and associated documentation files (the "Software"), to deal
|
| 7 |
+
in the Software without restriction, including without limitation the rights
|
| 8 |
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
| 9 |
+
copies of the Software, and to permit persons to whom the Software is
|
| 10 |
+
furnished to do so, subject to the following conditions:
|
| 11 |
+
|
| 12 |
+
The above copyright notice and this permission notice shall be included in all
|
| 13 |
+
copies or substantial portions of the Software.
|
| 14 |
+
|
| 15 |
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
| 16 |
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
| 17 |
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
| 18 |
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
| 19 |
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
| 20 |
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
| 21 |
+
SOFTWARE.
|
LICENSE.code
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
MIT License
|
| 2 |
+
|
| 3 |
+
Copyright (c) 2026 Alessandro Bologna
|
| 4 |
+
|
| 5 |
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
| 6 |
+
of this software and associated documentation files (the "Software"), to deal
|
| 7 |
+
in the Software without restriction, including without limitation the rights
|
| 8 |
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
| 9 |
+
copies of the Software, and to permit persons to whom the Software is
|
| 10 |
+
furnished to do so, subject to the following conditions:
|
| 11 |
+
|
| 12 |
+
The above copyright notice and this permission notice shall be included in all
|
| 13 |
+
copies or substantial portions of the Software.
|
| 14 |
+
|
| 15 |
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
| 16 |
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
| 17 |
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
| 18 |
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
| 19 |
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
| 20 |
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
| 21 |
+
SOFTWARE.
|
README.md
ADDED
|
@@ -0,0 +1,138 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
license: mit
|
| 3 |
+
library_name: gguf
|
| 4 |
+
base_model: deepseek-ai/DeepSeek-V4-Flash-0731
|
| 5 |
+
base_model_relation: quantized
|
| 6 |
+
tags:
|
| 7 |
+
- gguf
|
| 8 |
+
- deepseek-v4
|
| 9 |
+
- dspark
|
| 10 |
+
- speculative-decoding
|
| 11 |
+
- draft-model
|
| 12 |
+
- auxiliary-model
|
| 13 |
+
---
|
| 14 |
+
|
| 15 |
+
# DeepSeek V4 Flash 0731 DSpark Drafter GGUF
|
| 16 |
+
|
| 17 |
+
This repository contains only the **DSpark auxiliary drafter** extracted from
|
| 18 |
+
DeepSeek V4 Flash 0731. It is not a standalone language model and does not
|
| 19 |
+
contain the target model weights.
|
| 20 |
+
|
| 21 |
+
The GGUF is built from the official
|
| 22 |
+
[`deepseek-ai/DeepSeek-V4-Flash-0731`](https://huggingface.co/deepseek-ai/DeepSeek-V4-Flash-0731)
|
| 23 |
+
checkpoint using the standalone Python scripts included here. The complete
|
| 24 |
+
source manifest, build recipe, structural parser, numeric verifier, dependency
|
| 25 |
+
pin, and build results are published alongside the artifact.
|
| 26 |
+
|
| 27 |
+
## Artifact
|
| 28 |
+
|
| 29 |
+
| File | Bytes | SHA-256 |
|
| 30 |
+
|---|---:|---|
|
| 31 |
+
| `DeepSeek-V4-Flash-0731-DSpark-Drafter-MXFP4-Q8_0.gguf` | 10,897,110,272 | `5c8a1ce75e4797c170e22ac65080281a1b694c77f2e4fa7a703fbf09c5de0e33` |
|
| 32 |
+
|
| 33 |
+
The artifact uses GGUF v3 with the custom architecture
|
| 34 |
+
`deepseek_v4_flash_dspark_draft` and contains 81 tensors:
|
| 35 |
+
|
| 36 |
+
| Output type | Tensors | Transformation |
|
| 37 |
+
|---|---:|---|
|
| 38 |
+
| F32 | 45 | BF16 expanded exactly to F32, or source F32 bytes preserved |
|
| 39 |
+
| F16 | 2 | BF16 Markov heads rounded to F16 |
|
| 40 |
+
| Q8_0 | 25 | FP8 E4M3 weights with E8M0 block scales dequantized, then quantized to Q8_0 |
|
| 41 |
+
| MXFP4 | 9 | Routed-expert FP4 nibbles and E8M0 scales repacked without requantization |
|
| 42 |
+
|
| 43 |
+
The MXFP4 transformation is bit-preserving. Q8_0 and F16 transformations are
|
| 44 |
+
lossy and are checked against explicit numeric error bounds. This repository
|
| 45 |
+
does not claim that the complete conversion is lossless.
|
| 46 |
+
|
| 47 |
+
## Pinned source
|
| 48 |
+
|
| 49 |
+
- Repository: `deepseek-ai/DeepSeek-V4-Flash-0731`
|
| 50 |
+
- Revision: `9e165c30e2704aec5d9d593cce3eebd58bbef1cb`
|
| 51 |
+
- Input: only safetensors shards 46, 47, and 48
|
| 52 |
+
- Total source size: 10,863,342,388 bytes
|
| 53 |
+
|
| 54 |
+
Exact file sizes and SHA-256 values are in
|
| 55 |
+
[`manifest/source.json`](manifest/source.json). The downloader and converter
|
| 56 |
+
refuse any source that does not match them.
|
| 57 |
+
|
| 58 |
+
## Reproduce
|
| 59 |
+
|
| 60 |
+
The reference build used Linux aarch64, Python 3.14.6, NumPy 2.5.1, uv 0.11.29,
|
| 61 |
+
and Hugging Face CLI 1.24.0. The NumPy wheel is pinned by hash in
|
| 62 |
+
`requirements-linux-aarch64-py314.lock`. The upstream checkpoint is public, so
|
| 63 |
+
a Hugging Face token is not required for source download.
|
| 64 |
+
|
| 65 |
+
```bash
|
| 66 |
+
uv venv --python 3.14.6 .venv
|
| 67 |
+
uv pip install --python .venv/bin/python \
|
| 68 |
+
--require-hashes --only-binary=:all: \
|
| 69 |
+
-r requirements-linux-aarch64-py314.lock
|
| 70 |
+
|
| 71 |
+
.venv/bin/python scripts/download_sources.py --destination sources
|
| 72 |
+
.venv/bin/python -m unittest discover -s tests -v
|
| 73 |
+
.venv/bin/python scripts/reproduce.py \
|
| 74 |
+
--sources sources \
|
| 75 |
+
--recipe mxfp4-q8_0 \
|
| 76 |
+
--manifest-dir manifest \
|
| 77 |
+
--repeat-check
|
| 78 |
+
sha256sum -c SHA256SUMS
|
| 79 |
+
```
|
| 80 |
+
|
| 81 |
+
`--repeat-check` performs a second clean conversion and requires its complete
|
| 82 |
+
GGUF SHA-256 to match the verified first build byte for byte. The converter
|
| 83 |
+
memory-maps the source shards and processes FP8 tensors in bounded bands. It
|
| 84 |
+
does not require PyTorch, CUDA, safetensors, a GGUF package, or a GPU.
|
| 85 |
+
|
| 86 |
+
## Verification
|
| 87 |
+
|
| 88 |
+
The included verifier checks pinned source hashes and tensor inventories, GGUF
|
| 89 |
+
structure and metadata, exact F32/F16 bytes, exact MXFP4 repacking, and Q8_0
|
| 90 |
+
encoding plus reconstruction error against a fresh FP8 source decode.
|
| 91 |
+
|
| 92 |
+
Reference-build results:
|
| 93 |
+
|
| 94 |
+
| Check | Result |
|
| 95 |
+
|---|---:|
|
| 96 |
+
| Tensor count | 81 |
|
| 97 |
+
| Exact F32/F16 tensors | 47 |
|
| 98 |
+
| Exact MXFP4 tensors | 9 |
|
| 99 |
+
| Maximum Q8_0 block-relative error | 0.00396728515625 |
|
| 100 |
+
| Q8_0 limit | 0.01 |
|
| 101 |
+
| Repeat build | byte-identical |
|
| 102 |
+
|
| 103 |
+
Machine-readable observations are in
|
| 104 |
+
[`manifest/validation.json`](manifest/validation.json) and
|
| 105 |
+
[`manifest/build.json`](manifest/build.json).
|
| 106 |
+
|
| 107 |
+
## Compatibility and limitations
|
| 108 |
+
|
| 109 |
+
- This is an auxiliary drafter, not a complete checkpoint.
|
| 110 |
+
- A consumer must explicitly support the
|
| 111 |
+
`deepseek_v4_flash_dspark_draft` GGUF architecture and `dspark.*` tensor
|
| 112 |
+
namespace defined by the checked-in recipe.
|
| 113 |
+
- Generic GGUF support alone is insufficient. No compatibility with llama.cpp,
|
| 114 |
+
Ollama, LM Studio, vLLM, or other runtimes is implied.
|
| 115 |
+
- The drafter must be paired with a compatible DeepSeek V4 Flash 0731 target
|
| 116 |
+
model. No universal quality, acceptance-rate, or speed claim is made across
|
| 117 |
+
target quantizations or runtimes.
|
| 118 |
+
- This is a community conversion, not an official DeepSeek release or
|
| 119 |
+
endorsement.
|
| 120 |
+
|
| 121 |
+
## DSpark configuration
|
| 122 |
+
|
| 123 |
+
The pinned source configuration defines block size 5, target layers 40, 41,
|
| 124 |
+
and 42, Markov rank 256, hidden size 4096, and vocabulary size 129,280. These
|
| 125 |
+
values are embedded in the GGUF metadata and validated during the build.
|
| 126 |
+
|
| 127 |
+
## License and references
|
| 128 |
+
|
| 129 |
+
The source-derived weights remain under DeepSeek's MIT terms in
|
| 130 |
+
[`LICENSE`](LICENSE). The newly authored conversion and verification scripts
|
| 131 |
+
are under [`LICENSE.code`](LICENSE.code). The pinned checkpoint's
|
| 132 |
+
[upstream license](https://huggingface.co/deepseek-ai/DeepSeek-V4-Flash-0731/blob/9e165c30e2704aec5d9d593cce3eebd58bbef1cb/LICENSE)
|
| 133 |
+
and the DS4/GGML attribution in
|
| 134 |
+
[`THIRD_PARTY_NOTICES.md`](THIRD_PARTY_NOTICES.md) provide the applicable notices.
|
| 135 |
+
|
| 136 |
+
- [Official DeepSeek V4 Flash 0731 checkpoint](https://huggingface.co/deepseek-ai/DeepSeek-V4-Flash-0731)
|
| 137 |
+
- [DeepSeek V4 technical report](https://arxiv.org/abs/2606.19348)
|
| 138 |
+
- [DeepSpec](https://github.com/deepseek-ai/DeepSpec)
|
SHA256SUMS
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
5c8a1ce75e4797c170e22ac65080281a1b694c77f2e4fa7a703fbf09c5de0e33 DeepSeek-V4-Flash-0731-DSpark-Drafter-MXFP4-Q8_0.gguf
|
THIRD_PARTY_NOTICES.md
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Third-party notices
|
| 2 |
+
|
| 3 |
+
This repository contains a transformed subset of the weights published in
|
| 4 |
+
[`deepseek-ai/DeepSeek-V4-Flash-0731`](https://huggingface.co/deepseek-ai/DeepSeek-V4-Flash-0731)
|
| 5 |
+
at commit `9e165c30e2704aec5d9d593cce3eebd58bbef1cb`.
|
| 6 |
+
|
| 7 |
+
DeepSeek publishes that repository under the MIT License. Its copyright and
|
| 8 |
+
permission notice are available in the pinned checkpoint's
|
| 9 |
+
[upstream license](https://huggingface.co/deepseek-ai/DeepSeek-V4-Flash-0731/blob/9e165c30e2704aec5d9d593cce3eebd58bbef1cb/LICENSE).
|
| 10 |
+
|
| 11 |
+
The standalone conversion and verification scripts are newly authored for
|
| 12 |
+
this repository and published under the repository's declared MIT license.
|
| 13 |
+
|
| 14 |
+
The conversion scripts implement publicly documented numeric and file formats.
|
| 15 |
+
The Q2_K encoder in `scripts/convert.py` is a Python port of the unweighted
|
| 16 |
+
`ds4q_write_q2_k_block_ref` path and its numeric helpers from
|
| 17 |
+
[`antirez/ds4` at commit `453a5faa899759d0edf7729d595c4c39cddfcfca`](https://github.com/antirez/ds4/blob/453a5faa899759d0edf7729d595c4c39cddfcfca/gguf-tools/quants.c).
|
| 18 |
+
That source identifies its block layout and search procedure as derived from
|
| 19 |
+
the MIT-licensed GGML/llama.cpp quantizers.
|
| 20 |
+
|
| 21 |
+
The applicable DS4/GGML MIT notice is:
|
| 22 |
+
|
| 23 |
+
> Copyright (c) 2026 The ds4.c authors
|
| 24 |
+
> Copyright (c) 2023-2026 The ggml authors
|
| 25 |
+
>
|
| 26 |
+
> Permission is hereby granted, free of charge, to any person obtaining a copy
|
| 27 |
+
> of this software and associated documentation files (the "Software"), to deal
|
| 28 |
+
> in the Software without restriction, including without limitation the rights
|
| 29 |
+
> to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
| 30 |
+
> copies of the Software, and to permit persons to whom the Software is
|
| 31 |
+
> furnished to do so, subject to the following conditions:
|
| 32 |
+
>
|
| 33 |
+
> The above copyright notice and this permission notice shall be included in
|
| 34 |
+
> all copies or substantial portions of the Software.
|
| 35 |
+
>
|
| 36 |
+
> THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
| 37 |
+
> IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
| 38 |
+
> FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
| 39 |
+
> AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
| 40 |
+
> LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
| 41 |
+
> OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
| 42 |
+
> SOFTWARE.
|
| 43 |
+
|
| 44 |
+
No PyTorch, llama.cpp, or GGUF Python package is required at build time.
|
| 45 |
+
|
| 46 |
+
Format references:
|
| 47 |
+
|
| 48 |
+
- [DeepSeek-V3 FP8 block scaling implementation](https://github.com/deepseek-ai/DeepSeek-V3/blob/main/inference/kernel.py)
|
| 49 |
+
- [OCP Microscaling Formats specification](https://www.opencompute.org/documents/ocp-microscaling-formats-mx-v1-0-spec-final-pdf)
|
| 50 |
+
- [GGUF format documentation](https://github.com/ggml-org/ggml/blob/master/docs/gguf.md)
|
| 51 |
+
- [DS4 reference quantizer](https://github.com/antirez/ds4/blob/453a5faa899759d0edf7729d595c4c39cddfcfca/gguf-tools/quants.c)
|
| 52 |
+
- [DeepSpec](https://github.com/deepseek-ai/DeepSpec)
|
manifest/build.json
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"artifact": {
|
| 3 |
+
"filename": "DeepSeek-V4-Flash-0731-DSpark-Drafter-MXFP4-Q8_0.gguf",
|
| 4 |
+
"sha256": "5c8a1ce75e4797c170e22ac65080281a1b694c77f2e4fa7a703fbf09c5de0e33",
|
| 5 |
+
"size": 10897110272
|
| 6 |
+
},
|
| 7 |
+
"build_inputs": {
|
| 8 |
+
"manifest/source.json": "bd7744f0efffd71d45e6233bfd8008e02acf422e01d701f82277d8821938b9c5",
|
| 9 |
+
"requirements-linux-aarch64-py314.lock": "f66db1728c9a0702b511a12bfe8cffbfab0fba4fbbf56f10d0e1d96ee70ee66d",
|
| 10 |
+
"scripts/convert.py": "319dc5083a5dc1b0a3cbce16702ae2378594b415ecf3f4198b2b84662fe31c43",
|
| 11 |
+
"scripts/download_sources.py": "2c39fff99a0c91b3ad099a5d685c394563e7b7a963b0a88cd1f668ee6bee51d1",
|
| 12 |
+
"scripts/formats.py": "4963ff62387bdbdbf0276b9db98e53da04ae621cb2cad231330cfff9f1533149",
|
| 13 |
+
"scripts/recipe.py": "661bf7b14ee5f2fc46770b5494cf6641407b5268c83ef5de6fc2d3e77e25b937",
|
| 14 |
+
"scripts/reproduce.py": "05956fbff7c6e32c9eeab6a959fc3d4dcd29d04a097296ea66cd000c64bfaa45",
|
| 15 |
+
"scripts/verify.py": "9073c95f157bee37c0fdea5d26b029b9495ef63f1c46e1d2022afdceb5cc9cc0"
|
| 16 |
+
},
|
| 17 |
+
"environment": {
|
| 18 |
+
"machine": "aarch64",
|
| 19 |
+
"numpy": "2.5.1",
|
| 20 |
+
"platform": "Linux-6.17.0-1026-nvidia-aarch64-with-glibc2.39",
|
| 21 |
+
"python": "3.14.6"
|
| 22 |
+
},
|
| 23 |
+
"format": {
|
| 24 |
+
"architecture": "deepseek_v4_flash_dspark_draft",
|
| 25 |
+
"gguf_version": 3,
|
| 26 |
+
"recipe": "mxfp4-q8_0",
|
| 27 |
+
"recipe_version": 1
|
| 28 |
+
},
|
| 29 |
+
"repeat_build": {
|
| 30 |
+
"performed": true,
|
| 31 |
+
"sha256": "5c8a1ce75e4797c170e22ac65080281a1b694c77f2e4fa7a703fbf09c5de0e33",
|
| 32 |
+
"status": "byte-identical"
|
| 33 |
+
},
|
| 34 |
+
"source": {
|
| 35 |
+
"repository": "deepseek-ai/DeepSeek-V4-Flash-0731",
|
| 36 |
+
"revision": "9e165c30e2704aec5d9d593cce3eebd58bbef1cb"
|
| 37 |
+
}
|
| 38 |
+
}
|
manifest/source.json
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"files": {
|
| 3 |
+
"model-00046-of-00048.safetensors": {
|
| 4 |
+
"sha256": "5db924ca907e0d93acd975bd5079c3662717f9ac709f23d079bd8f816d29d9dd",
|
| 5 |
+
"size": 3610455184
|
| 6 |
+
},
|
| 7 |
+
"model-00047-of-00048.safetensors": {
|
| 8 |
+
"sha256": "62816173f9f6e136b20b48e3b6f16613ac9ea02b5603f636928b253244a548bd",
|
| 9 |
+
"size": 3560111960
|
| 10 |
+
},
|
| 11 |
+
"model-00048-of-00048.safetensors": {
|
| 12 |
+
"sha256": "cc43742bd24ae6bcdea343a91442f6f66aed2cfebcc6b235470204851ce2f8a9",
|
| 13 |
+
"size": 3692775244
|
| 14 |
+
}
|
| 15 |
+
},
|
| 16 |
+
"repository": "deepseek-ai/DeepSeek-V4-Flash-0731",
|
| 17 |
+
"revision": "9e165c30e2704aec5d9d593cce3eebd58bbef1cb",
|
| 18 |
+
"total_size": 10863342388
|
| 19 |
+
}
|
manifest/validation.json
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"artifact": {
|
| 3 |
+
"filename": "DeepSeek-V4-Flash-0731-DSpark-Drafter-MXFP4-Q8_0.gguf",
|
| 4 |
+
"sha256": "5c8a1ce75e4797c170e22ac65080281a1b694c77f2e4fa7a703fbf09c5de0e33",
|
| 5 |
+
"size": 10897110272
|
| 6 |
+
},
|
| 7 |
+
"gguf": {
|
| 8 |
+
"alignment": 32,
|
| 9 |
+
"data_start": 5824,
|
| 10 |
+
"tensor_count": 81,
|
| 11 |
+
"types": {
|
| 12 |
+
"F16": 2,
|
| 13 |
+
"F32": 45,
|
| 14 |
+
"MXFP4": 9,
|
| 15 |
+
"Q8_0": 25
|
| 16 |
+
},
|
| 17 |
+
"version": 3
|
| 18 |
+
},
|
| 19 |
+
"recipe": "mxfp4-q8_0",
|
| 20 |
+
"source": {
|
| 21 |
+
"files": {
|
| 22 |
+
"model-00046-of-00048.safetensors": {
|
| 23 |
+
"sha256": "5db924ca907e0d93acd975bd5079c3662717f9ac709f23d079bd8f816d29d9dd",
|
| 24 |
+
"size": 3610455184
|
| 25 |
+
},
|
| 26 |
+
"model-00047-of-00048.safetensors": {
|
| 27 |
+
"sha256": "62816173f9f6e136b20b48e3b6f16613ac9ea02b5603f636928b253244a548bd",
|
| 28 |
+
"size": 3560111960
|
| 29 |
+
},
|
| 30 |
+
"model-00048-of-00048.safetensors": {
|
| 31 |
+
"sha256": "cc43742bd24ae6bcdea343a91442f6f66aed2cfebcc6b235470204851ce2f8a9",
|
| 32 |
+
"size": 3692775244
|
| 33 |
+
}
|
| 34 |
+
},
|
| 35 |
+
"repository": "deepseek-ai/DeepSeek-V4-Flash-0731",
|
| 36 |
+
"revision": "9e165c30e2704aec5d9d593cce3eebd58bbef1cb"
|
| 37 |
+
},
|
| 38 |
+
"validation": {
|
| 39 |
+
"f16_exact_tensors": 2,
|
| 40 |
+
"f32_exact_tensors": 45,
|
| 41 |
+
"mxfp4_exact_tensors": 9,
|
| 42 |
+
"q8_0_limit": 0.01,
|
| 43 |
+
"q8_0_max_block_relative_error": 0.00396728515625,
|
| 44 |
+
"status": "pass"
|
| 45 |
+
}
|
| 46 |
+
}
|
requirements-linux-aarch64-py314.lock
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# CPython 3.14 / Linux aarch64 reference-build dependency.
|
| 2 |
+
# Wheel: numpy-2.5.1-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl
|
| 3 |
+
numpy==2.5.1 \
|
| 4 |
+
--hash=sha256:caf3e317d33d60c37986b452613f4ab51246d0691350c03d0cb4a898627f4a95
|
scripts/convert.py
ADDED
|
@@ -0,0 +1,653 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""Deterministically convert the pinned DSpark tensors into one GGUF v3 file."""
|
| 3 |
+
|
| 4 |
+
from __future__ import annotations
|
| 5 |
+
|
| 6 |
+
import argparse
|
| 7 |
+
import math
|
| 8 |
+
import os
|
| 9 |
+
from concurrent.futures import ThreadPoolExecutor
|
| 10 |
+
from pathlib import Path
|
| 11 |
+
from typing import BinaryIO
|
| 12 |
+
|
| 13 |
+
import numpy as np
|
| 14 |
+
|
| 15 |
+
from formats import SafeTensorSet, sha256_path, verify_source_files, write_gguf
|
| 16 |
+
from recipe import (
|
| 17 |
+
DEFAULT_RECIPE,
|
| 18 |
+
KIND_FP8_TO_Q8_0,
|
| 19 |
+
KIND_FUSED_EXPERTS_MXFP4,
|
| 20 |
+
KIND_FUSED_EXPERTS_Q2_K,
|
| 21 |
+
KIND_MARKOV_F16,
|
| 22 |
+
KIND_PLAIN_F32,
|
| 23 |
+
KIND_RELABEL_F32,
|
| 24 |
+
N_EXPERTS,
|
| 25 |
+
RECIPES,
|
| 26 |
+
ArtifactRecipe,
|
| 27 |
+
TensorRecipe,
|
| 28 |
+
build_plan,
|
| 29 |
+
resolve_recipe,
|
| 30 |
+
source_paths,
|
| 31 |
+
)
|
| 32 |
+
|
| 33 |
+
|
| 34 |
+
def _e4m3_value(byte: int) -> np.float32:
|
| 35 |
+
sign = -1.0 if byte & 0x80 else 1.0
|
| 36 |
+
exponent = (byte >> 3) & 0x0F
|
| 37 |
+
mantissa = byte & 0x07
|
| 38 |
+
if exponent == 0x0F and mantissa == 0x07:
|
| 39 |
+
return np.float32(np.nan)
|
| 40 |
+
if exponent == 0:
|
| 41 |
+
return np.float32(sign * (mantissa / 8.0) * math.ldexp(1.0, 1 - 7))
|
| 42 |
+
return np.float32(sign * (1.0 + mantissa / 8.0) * math.ldexp(1.0, exponent - 7))
|
| 43 |
+
|
| 44 |
+
|
| 45 |
+
E4M3_TABLE = np.array([_e4m3_value(byte) for byte in range(256)], dtype=np.float32)
|
| 46 |
+
E8M0_TABLE = np.array(
|
| 47 |
+
[
|
| 48 |
+
np.float32(np.nan) if byte == 0xFF else np.float32(math.ldexp(1.0, byte - 127))
|
| 49 |
+
for byte in range(256)
|
| 50 |
+
],
|
| 51 |
+
dtype=np.float32,
|
| 52 |
+
)
|
| 53 |
+
FP4_TABLE = np.array(
|
| 54 |
+
[
|
| 55 |
+
0.0,
|
| 56 |
+
0.5,
|
| 57 |
+
1.0,
|
| 58 |
+
1.5,
|
| 59 |
+
2.0,
|
| 60 |
+
3.0,
|
| 61 |
+
4.0,
|
| 62 |
+
6.0,
|
| 63 |
+
0.0,
|
| 64 |
+
-0.5,
|
| 65 |
+
-1.0,
|
| 66 |
+
-1.5,
|
| 67 |
+
-2.0,
|
| 68 |
+
-3.0,
|
| 69 |
+
-4.0,
|
| 70 |
+
-6.0,
|
| 71 |
+
],
|
| 72 |
+
dtype=np.float32,
|
| 73 |
+
)
|
| 74 |
+
|
| 75 |
+
Q2_K_BLOCK_SIZE = 256
|
| 76 |
+
Q2_K_BLOCK_BYTES = 84
|
| 77 |
+
# Both routed shapes contain at most 4,096 rows. Each task keeps one decoded
|
| 78 |
+
# expert resident at a time; the bounded worker count limits the larger set of
|
| 79 |
+
# NumPy fitting temporaries while avoiding tiny-band dispatch overhead across
|
| 80 |
+
# the 2,304 experts in the full artifact.
|
| 81 |
+
EXPERT_ROW_BATCH = 4096
|
| 82 |
+
Q2_K_EXPERT_WORKERS = max(1, min(8, os.cpu_count() or 1))
|
| 83 |
+
|
| 84 |
+
|
| 85 |
+
def bf16_bits_to_f32(bits: np.ndarray) -> np.ndarray:
|
| 86 |
+
"""Convert little-endian BF16 payloads exactly by placing them in F32 high bits."""
|
| 87 |
+
|
| 88 |
+
wide = np.asarray(bits, dtype="<u2").astype("<u4")
|
| 89 |
+
wide <<= np.uint32(16)
|
| 90 |
+
return wide.view("<f4")
|
| 91 |
+
|
| 92 |
+
|
| 93 |
+
def round_away_from_zero(values: np.ndarray) -> np.ndarray:
|
| 94 |
+
values = np.asarray(values, dtype=np.float32)
|
| 95 |
+
truncated = np.trunc(values).astype(np.float32, copy=False)
|
| 96 |
+
fraction = np.abs(values - truncated)
|
| 97 |
+
return np.where(
|
| 98 |
+
fraction >= np.float32(0.5),
|
| 99 |
+
truncated + np.copysign(np.float32(1.0), values),
|
| 100 |
+
truncated,
|
| 101 |
+
).astype(np.float32, copy=False)
|
| 102 |
+
|
| 103 |
+
|
| 104 |
+
def nearest_int_f32(values: np.ndarray) -> np.ndarray:
|
| 105 |
+
"""Port ds4q_nearest_int's binary32 round-to-nearest-even bit trick."""
|
| 106 |
+
|
| 107 |
+
f32 = np.asarray(values, dtype=np.float32)
|
| 108 |
+
if not np.isfinite(f32).all() or np.any(np.abs(f32) > np.float32(4_194_303.0)):
|
| 109 |
+
raise ValueError("nearest-int input is outside the DS4 reference range")
|
| 110 |
+
shifted = np.add(f32, np.float32(12_582_912.0), dtype=np.float32)
|
| 111 |
+
bits = shifted.view(np.int32)
|
| 112 |
+
return ((bits & np.int32(0x007F_FFFF)) - np.int32(0x0040_0000)).astype(
|
| 113 |
+
np.int32, copy=False
|
| 114 |
+
)
|
| 115 |
+
|
| 116 |
+
|
| 117 |
+
def _make_qkx2_quants(
|
| 118 |
+
groups: np.ndarray,
|
| 119 |
+
) -> tuple[np.ndarray, np.ndarray, np.ndarray]:
|
| 120 |
+
"""Vectorized binary32 port of DS4's unweighted 16-value Q2_K fit."""
|
| 121 |
+
|
| 122 |
+
x = np.asarray(groups, dtype=np.float32)
|
| 123 |
+
if x.ndim != 2 or x.shape[1] != 16:
|
| 124 |
+
raise ValueError("Q2_K fitting groups must have shape (n, 16)")
|
| 125 |
+
weights = np.abs(x).astype(np.float32, copy=False)
|
| 126 |
+
minimum = x[:, 0].copy()
|
| 127 |
+
maximum = x[:, 0].copy()
|
| 128 |
+
for index in range(1, 16):
|
| 129 |
+
minimum = np.minimum(minimum, x[:, index])
|
| 130 |
+
maximum = np.maximum(maximum, x[:, index])
|
| 131 |
+
minimum = np.where(minimum > 0, np.float32(0.0), minimum).astype(
|
| 132 |
+
np.float32, copy=False
|
| 133 |
+
)
|
| 134 |
+
constant = maximum == minimum
|
| 135 |
+
value_range = np.subtract(maximum, minimum, dtype=np.float32)
|
| 136 |
+
safe_range = np.where(constant, np.float32(1.0), value_range).astype(
|
| 137 |
+
np.float32, copy=False
|
| 138 |
+
)
|
| 139 |
+
inverse_scale = np.divide(np.float32(3.0), safe_range, dtype=np.float32)
|
| 140 |
+
scale = np.divide(np.float32(1.0), inverse_scale, dtype=np.float32)
|
| 141 |
+
normalized = np.multiply(
|
| 142 |
+
np.subtract(x, minimum[:, None], dtype=np.float32),
|
| 143 |
+
inverse_scale[:, None],
|
| 144 |
+
dtype=np.float32,
|
| 145 |
+
)
|
| 146 |
+
labels = np.clip(nearest_int_f32(normalized), 0, 3).astype(np.uint8)
|
| 147 |
+
scale[constant] = np.float32(0.0)
|
| 148 |
+
labels[constant] = np.uint8(0)
|
| 149 |
+
|
| 150 |
+
sum_w = weights[:, 0].copy()
|
| 151 |
+
sum_x = np.multiply(weights[:, 0], x[:, 0], dtype=np.float32)
|
| 152 |
+
for index in range(1, 16):
|
| 153 |
+
sum_w = np.add(sum_w, weights[:, index], dtype=np.float32)
|
| 154 |
+
sum_x = np.add(
|
| 155 |
+
sum_x,
|
| 156 |
+
np.multiply(weights[:, index], x[:, index], dtype=np.float32),
|
| 157 |
+
dtype=np.float32,
|
| 158 |
+
)
|
| 159 |
+
|
| 160 |
+
best_error = np.zeros(x.shape[0], dtype=np.float32)
|
| 161 |
+
for index in range(16):
|
| 162 |
+
reconstructed = np.add(
|
| 163 |
+
np.multiply(scale, labels[:, index], dtype=np.float32),
|
| 164 |
+
minimum,
|
| 165 |
+
dtype=np.float32,
|
| 166 |
+
)
|
| 167 |
+
difference = np.abs(
|
| 168 |
+
np.subtract(reconstructed, x[:, index], dtype=np.float32)
|
| 169 |
+
).astype(np.float32, copy=False)
|
| 170 |
+
best_error = np.add(
|
| 171 |
+
best_error,
|
| 172 |
+
np.multiply(weights[:, index], difference, dtype=np.float32),
|
| 173 |
+
dtype=np.float32,
|
| 174 |
+
)
|
| 175 |
+
|
| 176 |
+
for step in range(16):
|
| 177 |
+
candidate_numerator = np.add(
|
| 178 |
+
np.add(
|
| 179 |
+
np.float32(-0.5),
|
| 180 |
+
np.multiply(np.float32(0.1), np.float32(step), dtype=np.float32),
|
| 181 |
+
dtype=np.float32,
|
| 182 |
+
),
|
| 183 |
+
np.float32(3.0),
|
| 184 |
+
dtype=np.float32,
|
| 185 |
+
)
|
| 186 |
+
candidate_range = np.subtract(maximum, minimum, dtype=np.float32)
|
| 187 |
+
safe_candidate_range = np.where(
|
| 188 |
+
constant, np.float32(1.0), candidate_range
|
| 189 |
+
).astype(np.float32, copy=False)
|
| 190 |
+
candidate_inverse = np.divide(
|
| 191 |
+
candidate_numerator, safe_candidate_range, dtype=np.float32
|
| 192 |
+
)
|
| 193 |
+
candidate_labels = np.clip(
|
| 194 |
+
nearest_int_f32(
|
| 195 |
+
np.multiply(
|
| 196 |
+
np.subtract(x, minimum[:, None], dtype=np.float32),
|
| 197 |
+
candidate_inverse[:, None],
|
| 198 |
+
dtype=np.float32,
|
| 199 |
+
)
|
| 200 |
+
),
|
| 201 |
+
0,
|
| 202 |
+
3,
|
| 203 |
+
).astype(np.uint8)
|
| 204 |
+
|
| 205 |
+
sum_l = np.zeros(x.shape[0], dtype=np.float32)
|
| 206 |
+
sum_l2 = np.zeros(x.shape[0], dtype=np.float32)
|
| 207 |
+
sum_xl = np.zeros(x.shape[0], dtype=np.float32)
|
| 208 |
+
for index in range(16):
|
| 209 |
+
weighted_label = np.multiply(
|
| 210 |
+
weights[:, index], candidate_labels[:, index], dtype=np.float32
|
| 211 |
+
)
|
| 212 |
+
sum_l = np.add(sum_l, weighted_label, dtype=np.float32)
|
| 213 |
+
sum_l2 = np.add(
|
| 214 |
+
sum_l2,
|
| 215 |
+
np.multiply(
|
| 216 |
+
weighted_label, candidate_labels[:, index], dtype=np.float32
|
| 217 |
+
),
|
| 218 |
+
dtype=np.float32,
|
| 219 |
+
)
|
| 220 |
+
sum_xl = np.add(
|
| 221 |
+
sum_xl,
|
| 222 |
+
np.multiply(weighted_label, x[:, index], dtype=np.float32),
|
| 223 |
+
dtype=np.float32,
|
| 224 |
+
)
|
| 225 |
+
|
| 226 |
+
determinant = np.subtract(
|
| 227 |
+
np.multiply(sum_w, sum_l2, dtype=np.float32),
|
| 228 |
+
np.multiply(sum_l, sum_l, dtype=np.float32),
|
| 229 |
+
dtype=np.float32,
|
| 230 |
+
)
|
| 231 |
+
valid = np.logical_and(~constant, determinant > 0)
|
| 232 |
+
safe_determinant = np.where(valid, determinant, np.float32(1.0)).astype(
|
| 233 |
+
np.float32, copy=False
|
| 234 |
+
)
|
| 235 |
+
candidate_scale = np.divide(
|
| 236 |
+
np.subtract(
|
| 237 |
+
np.multiply(sum_w, sum_xl, dtype=np.float32),
|
| 238 |
+
np.multiply(sum_x, sum_l, dtype=np.float32),
|
| 239 |
+
dtype=np.float32,
|
| 240 |
+
),
|
| 241 |
+
safe_determinant,
|
| 242 |
+
dtype=np.float32,
|
| 243 |
+
)
|
| 244 |
+
candidate_minimum = np.divide(
|
| 245 |
+
np.subtract(
|
| 246 |
+
np.multiply(sum_l2, sum_x, dtype=np.float32),
|
| 247 |
+
np.multiply(sum_l, sum_xl, dtype=np.float32),
|
| 248 |
+
dtype=np.float32,
|
| 249 |
+
),
|
| 250 |
+
safe_determinant,
|
| 251 |
+
dtype=np.float32,
|
| 252 |
+
)
|
| 253 |
+
positive_minimum = np.logical_and(valid, candidate_minimum > 0)
|
| 254 |
+
safe_sum_l2 = np.where(positive_minimum, sum_l2, np.float32(1.0)).astype(
|
| 255 |
+
np.float32, copy=False
|
| 256 |
+
)
|
| 257 |
+
zero_minimum_scale = np.divide(sum_xl, safe_sum_l2, dtype=np.float32)
|
| 258 |
+
candidate_scale = np.where(
|
| 259 |
+
positive_minimum, zero_minimum_scale, candidate_scale
|
| 260 |
+
).astype(np.float32, copy=False)
|
| 261 |
+
candidate_minimum = np.where(
|
| 262 |
+
positive_minimum, np.float32(0.0), candidate_minimum
|
| 263 |
+
).astype(np.float32, copy=False)
|
| 264 |
+
|
| 265 |
+
current_error = np.zeros(x.shape[0], dtype=np.float32)
|
| 266 |
+
for index in range(16):
|
| 267 |
+
reconstructed = np.add(
|
| 268 |
+
np.multiply(
|
| 269 |
+
candidate_scale, candidate_labels[:, index], dtype=np.float32
|
| 270 |
+
),
|
| 271 |
+
candidate_minimum,
|
| 272 |
+
dtype=np.float32,
|
| 273 |
+
)
|
| 274 |
+
difference = np.abs(
|
| 275 |
+
np.subtract(reconstructed, x[:, index], dtype=np.float32)
|
| 276 |
+
).astype(np.float32, copy=False)
|
| 277 |
+
current_error = np.add(
|
| 278 |
+
current_error,
|
| 279 |
+
np.multiply(weights[:, index], difference, dtype=np.float32),
|
| 280 |
+
dtype=np.float32,
|
| 281 |
+
)
|
| 282 |
+
improved = np.logical_and(valid, current_error < best_error)
|
| 283 |
+
labels[improved] = candidate_labels[improved]
|
| 284 |
+
best_error[improved] = current_error[improved]
|
| 285 |
+
scale[improved] = candidate_scale[improved]
|
| 286 |
+
minimum[improved] = candidate_minimum[improved]
|
| 287 |
+
|
| 288 |
+
return scale, np.negative(minimum, dtype=np.float32), labels
|
| 289 |
+
|
| 290 |
+
|
| 291 |
+
def quantize_q2_k(values: np.ndarray) -> np.ndarray:
|
| 292 |
+
"""Encode rows as unweighted GGML Q2_K blocks using the DS4 reference path."""
|
| 293 |
+
|
| 294 |
+
rows = np.asarray(values, dtype=np.float32)
|
| 295 |
+
if rows.ndim != 2 or rows.shape[1] % Q2_K_BLOCK_SIZE:
|
| 296 |
+
raise ValueError("Q2_K input must be 2D with columns divisible by 256")
|
| 297 |
+
if not np.isfinite(rows).all():
|
| 298 |
+
raise ValueError("Q2_K input contains NaN or infinity")
|
| 299 |
+
blocks = rows.reshape(-1, Q2_K_BLOCK_SIZE)
|
| 300 |
+
groups = blocks.reshape(-1, 16)
|
| 301 |
+
scales, mins, labels = _make_qkx2_quants(groups)
|
| 302 |
+
scales = scales.reshape(-1, 16)
|
| 303 |
+
mins = mins.reshape(-1, 16)
|
| 304 |
+
labels = labels.reshape(-1, Q2_K_BLOCK_SIZE)
|
| 305 |
+
|
| 306 |
+
max_scale = np.maximum(np.max(scales, axis=1), np.float32(0.0)).astype(
|
| 307 |
+
np.float32, copy=False
|
| 308 |
+
)
|
| 309 |
+
max_min = np.maximum(np.max(mins, axis=1), np.float32(0.0)).astype(
|
| 310 |
+
np.float32, copy=False
|
| 311 |
+
)
|
| 312 |
+
scale_inverse = np.zeros_like(max_scale)
|
| 313 |
+
min_inverse = np.zeros_like(max_min)
|
| 314 |
+
np.divide(
|
| 315 |
+
np.float32(15.0),
|
| 316 |
+
max_scale,
|
| 317 |
+
out=scale_inverse,
|
| 318 |
+
where=max_scale > 0,
|
| 319 |
+
)
|
| 320 |
+
np.divide(np.float32(15.0), max_min, out=min_inverse, where=max_min > 0)
|
| 321 |
+
scale_codes = nearest_int_f32(
|
| 322 |
+
np.multiply(scales, scale_inverse[:, None], dtype=np.float32)
|
| 323 |
+
)
|
| 324 |
+
min_codes = nearest_int_f32(
|
| 325 |
+
np.multiply(mins, min_inverse[:, None], dtype=np.float32)
|
| 326 |
+
)
|
| 327 |
+
if np.any(scale_codes < 0) or np.any(scale_codes > 15):
|
| 328 |
+
raise ValueError("Q2_K scale code is outside four bits")
|
| 329 |
+
if np.any(min_codes < 0) or np.any(min_codes > 15):
|
| 330 |
+
raise ValueError("Q2_K minimum code is outside four bits")
|
| 331 |
+
packed_scales = scale_codes.astype(np.uint8) | (
|
| 332 |
+
min_codes.astype(np.uint8) << np.uint8(4)
|
| 333 |
+
)
|
| 334 |
+
|
| 335 |
+
d_bits = (
|
| 336 |
+
np.divide(max_scale, np.float32(15.0), dtype=np.float32)
|
| 337 |
+
.astype("<f2")
|
| 338 |
+
.view("<u2")
|
| 339 |
+
)
|
| 340 |
+
dmin_bits = (
|
| 341 |
+
np.divide(max_min, np.float32(15.0), dtype=np.float32).astype("<f2").view("<u2")
|
| 342 |
+
)
|
| 343 |
+
d = d_bits.view("<f2").astype(np.float32)
|
| 344 |
+
dmin = dmin_bits.view("<f2").astype(np.float32)
|
| 345 |
+
group_d = np.multiply(
|
| 346 |
+
d[:, None], packed_scales & np.uint8(0x0F), dtype=np.float32
|
| 347 |
+
).reshape(-1)
|
| 348 |
+
group_min = np.multiply(
|
| 349 |
+
dmin[:, None], packed_scales >> np.uint8(4), dtype=np.float32
|
| 350 |
+
).reshape(-1)
|
| 351 |
+
fitted_groups = labels.reshape(-1, 16)
|
| 352 |
+
nonzero = group_d != 0
|
| 353 |
+
normalized = np.divide(
|
| 354 |
+
np.add(groups[nonzero], group_min[nonzero, None], dtype=np.float32),
|
| 355 |
+
group_d[nonzero, None],
|
| 356 |
+
dtype=np.float32,
|
| 357 |
+
)
|
| 358 |
+
fitted_groups[nonzero] = np.clip(nearest_int_f32(normalized), 0, 3).astype(np.uint8)
|
| 359 |
+
|
| 360 |
+
encoded = np.zeros((blocks.shape[0], Q2_K_BLOCK_BYTES), dtype=np.uint8)
|
| 361 |
+
encoded[:, :16] = packed_scales
|
| 362 |
+
for start in (0, 128):
|
| 363 |
+
packed = (
|
| 364 |
+
labels[:, start : start + 32]
|
| 365 |
+
| (labels[:, start + 32 : start + 64] << np.uint8(2))
|
| 366 |
+
| (labels[:, start + 64 : start + 96] << np.uint8(4))
|
| 367 |
+
| (labels[:, start + 96 : start + 128] << np.uint8(6))
|
| 368 |
+
)
|
| 369 |
+
offset = 16 + start // 4
|
| 370 |
+
encoded[:, offset : offset + 32] = packed
|
| 371 |
+
encoded[:, 80:82] = d_bits.astype("<u2", copy=False).view(np.uint8).reshape(-1, 2)
|
| 372 |
+
encoded[:, 82:84] = (
|
| 373 |
+
dmin_bits.astype("<u2", copy=False).view(np.uint8).reshape(-1, 2)
|
| 374 |
+
)
|
| 375 |
+
return encoded
|
| 376 |
+
|
| 377 |
+
|
| 378 |
+
def decode_source_mxfp4(
|
| 379 |
+
packed: np.ndarray,
|
| 380 |
+
scales: np.ndarray,
|
| 381 |
+
rows: int,
|
| 382 |
+
cols: int,
|
| 383 |
+
) -> np.ndarray:
|
| 384 |
+
"""Decode one bounded row band of the pinned OCP E2M1/E8M0 source."""
|
| 385 |
+
|
| 386 |
+
if cols % 32:
|
| 387 |
+
raise ValueError("source MXFP4 column count must be divisible by 32")
|
| 388 |
+
blocks = cols // 32
|
| 389 |
+
packed_blocks = np.asarray(packed, dtype=np.uint8).reshape(rows, blocks, 16)
|
| 390 |
+
scale_codes = np.asarray(scales, dtype=np.uint8).reshape(rows, blocks)
|
| 391 |
+
if np.any(scale_codes == 0xFF):
|
| 392 |
+
raise ValueError("source MXFP4 contains reserved E8M0 NaN scale 0xff")
|
| 393 |
+
codes = np.empty((rows, blocks, 32), dtype=np.uint8)
|
| 394 |
+
codes[..., 0::2] = packed_blocks & np.uint8(0x0F)
|
| 395 |
+
codes[..., 1::2] = packed_blocks >> np.uint8(4)
|
| 396 |
+
decoded = FP4_TABLE[codes]
|
| 397 |
+
decoded *= E8M0_TABLE[scale_codes][..., None]
|
| 398 |
+
return decoded.reshape(rows, cols)
|
| 399 |
+
|
| 400 |
+
|
| 401 |
+
def quantize_q8_0(values: np.ndarray) -> np.ndarray:
|
| 402 |
+
"""Encode flat F32 data as GGML Q8_0 using Rust/C-style half-away rounding."""
|
| 403 |
+
|
| 404 |
+
flat = np.asarray(values, dtype=np.float32).reshape(-1)
|
| 405 |
+
if flat.size % 32:
|
| 406 |
+
raise ValueError("Q8_0 input length must be divisible by 32")
|
| 407 |
+
blocks = flat.reshape(-1, 32)
|
| 408 |
+
if not np.isfinite(blocks).all():
|
| 409 |
+
raise ValueError("Q8_0 input contains NaN or infinity")
|
| 410 |
+
maxima = np.max(np.abs(blocks), axis=1).astype(np.float32, copy=False)
|
| 411 |
+
scales = maxima / np.float32(127.0)
|
| 412 |
+
inverse = np.zeros_like(scales)
|
| 413 |
+
np.divide(np.float32(1.0), scales, out=inverse, where=scales != 0)
|
| 414 |
+
quantized = round_away_from_zero(blocks * inverse[:, None])
|
| 415 |
+
if np.any(quantized < -127) or np.any(quantized > 127):
|
| 416 |
+
raise ValueError("Q8_0 quantization produced an out-of-range code")
|
| 417 |
+
codes = quantized.astype(np.int8)
|
| 418 |
+
encoded = np.empty((blocks.shape[0], 34), dtype=np.uint8)
|
| 419 |
+
encoded[:, :2] = scales.astype("<f2").view(np.uint8).reshape(-1, 2)
|
| 420 |
+
encoded[:, 2:] = codes.view(np.uint8)
|
| 421 |
+
return encoded
|
| 422 |
+
|
| 423 |
+
|
| 424 |
+
def repack_mxfp4(
|
| 425 |
+
packed: np.ndarray,
|
| 426 |
+
scales: np.ndarray,
|
| 427 |
+
rows: int,
|
| 428 |
+
cols: int,
|
| 429 |
+
) -> np.ndarray:
|
| 430 |
+
"""Purely permute source FP4 nibbles into GGML's 17-byte MXFP4 blocks."""
|
| 431 |
+
|
| 432 |
+
if cols % 32:
|
| 433 |
+
raise ValueError("MXFP4 column count must be divisible by 32")
|
| 434 |
+
blocks = cols // 32
|
| 435 |
+
packed_blocks = np.asarray(packed, dtype=np.uint8).reshape(rows, blocks, 16)
|
| 436 |
+
scale_rows = np.asarray(scales, dtype=np.uint8).reshape(rows, blocks)
|
| 437 |
+
if np.any(scale_rows == 0xFF):
|
| 438 |
+
raise ValueError("MXFP4 source contains reserved E8M0 NaN scale 0xff")
|
| 439 |
+
|
| 440 |
+
low_codes = np.empty((rows, blocks, 16), dtype=np.uint8)
|
| 441 |
+
high_codes = np.empty((rows, blocks, 16), dtype=np.uint8)
|
| 442 |
+
low_bytes = packed_blocks[..., :8]
|
| 443 |
+
high_bytes = packed_blocks[..., 8:]
|
| 444 |
+
low_codes[..., 0::2] = low_bytes & np.uint8(0x0F)
|
| 445 |
+
low_codes[..., 1::2] = low_bytes >> np.uint8(4)
|
| 446 |
+
high_codes[..., 0::2] = high_bytes & np.uint8(0x0F)
|
| 447 |
+
high_codes[..., 1::2] = high_bytes >> np.uint8(4)
|
| 448 |
+
|
| 449 |
+
encoded = np.empty((rows, blocks, 17), dtype=np.uint8)
|
| 450 |
+
encoded[..., 0] = scale_rows
|
| 451 |
+
encoded[..., 1:] = low_codes | (high_codes << np.uint8(4))
|
| 452 |
+
return encoded
|
| 453 |
+
|
| 454 |
+
|
| 455 |
+
class TensorProducer:
|
| 456 |
+
def __init__(self, source: SafeTensorSet):
|
| 457 |
+
self.source = source
|
| 458 |
+
|
| 459 |
+
def __call__(self, tensor: TensorRecipe, handle: BinaryIO) -> int:
|
| 460 |
+
print(f"[convert] {tensor.out_name} ({tensor.byte_len:,} bytes)", flush=True)
|
| 461 |
+
if tensor.kind == KIND_PLAIN_F32:
|
| 462 |
+
return self._plain_f32(tensor, handle)
|
| 463 |
+
if tensor.kind == KIND_RELABEL_F32:
|
| 464 |
+
return self._copy_f32(tensor, handle)
|
| 465 |
+
if tensor.kind == KIND_FP8_TO_Q8_0:
|
| 466 |
+
return self._fp8_to_q8_0(tensor, handle)
|
| 467 |
+
if tensor.kind == KIND_FUSED_EXPERTS_MXFP4:
|
| 468 |
+
return self._fused_experts_mxfp4(tensor, handle)
|
| 469 |
+
if tensor.kind == KIND_FUSED_EXPERTS_Q2_K:
|
| 470 |
+
return self._fused_experts_q2_k(tensor, handle)
|
| 471 |
+
if tensor.kind == KIND_MARKOV_F16:
|
| 472 |
+
return self._markov_f16(tensor, handle)
|
| 473 |
+
raise ValueError(f"unsupported tensor recipe kind: {tensor.kind}")
|
| 474 |
+
|
| 475 |
+
def _plain_f32(self, tensor: TensorRecipe, handle: BinaryIO) -> int:
|
| 476 |
+
if not tensor.src_is_bf16:
|
| 477 |
+
raw = self.source.raw(tensor.src_name)
|
| 478 |
+
try:
|
| 479 |
+
return handle.write(raw)
|
| 480 |
+
finally:
|
| 481 |
+
raw.release()
|
| 482 |
+
bits = self.source.array(tensor.src_name, "<u2").reshape(-1)
|
| 483 |
+
data = bf16_bits_to_f32(bits).tobytes(order="C")
|
| 484 |
+
return handle.write(data)
|
| 485 |
+
|
| 486 |
+
def _copy_f32(self, tensor: TensorRecipe, handle: BinaryIO) -> int:
|
| 487 |
+
raw = self.source.raw(tensor.src_name)
|
| 488 |
+
try:
|
| 489 |
+
return handle.write(raw)
|
| 490 |
+
finally:
|
| 491 |
+
raw.release()
|
| 492 |
+
|
| 493 |
+
def _fp8_to_q8_0(self, tensor: TensorRecipe, handle: BinaryIO) -> int:
|
| 494 |
+
weights = self.source.array(tensor.weight_name, np.uint8).reshape(
|
| 495 |
+
tensor.rows, tensor.cols
|
| 496 |
+
)
|
| 497 |
+
scale_codes = self.source.array(tensor.scale_name, np.uint8).reshape(
|
| 498 |
+
(tensor.rows + 127) // 128,
|
| 499 |
+
(tensor.cols + 127) // 128,
|
| 500 |
+
)
|
| 501 |
+
written = 0
|
| 502 |
+
for block_row in range(scale_codes.shape[0]):
|
| 503 |
+
row_start = block_row * 128
|
| 504 |
+
row_end = min(row_start + 128, tensor.rows)
|
| 505 |
+
decoded = E4M3_TABLE[weights[row_start:row_end]]
|
| 506 |
+
column_scales = np.repeat(E8M0_TABLE[scale_codes[block_row]], 128)[
|
| 507 |
+
: tensor.cols
|
| 508 |
+
]
|
| 509 |
+
if not np.isfinite(column_scales).all():
|
| 510 |
+
raise ValueError(f"{tensor.scale_name}: contains reserved E8M0 NaN")
|
| 511 |
+
decoded *= column_scales[None, :]
|
| 512 |
+
encoded = quantize_q8_0(decoded)
|
| 513 |
+
written += handle.write(encoded.tobytes(order="C"))
|
| 514 |
+
return written
|
| 515 |
+
|
| 516 |
+
def _fused_experts_mxfp4(self, tensor: TensorRecipe, handle: BinaryIO) -> int:
|
| 517 |
+
written = 0
|
| 518 |
+
for expert in range(N_EXPERTS):
|
| 519 |
+
prefix = f"{tensor.name_prefix}.ffn.experts.{expert}.{tensor.expert_kind}"
|
| 520 |
+
packed = self.source.array(f"{prefix}.weight", np.uint8)
|
| 521 |
+
scales = self.source.array(f"{prefix}.scale", np.uint8)
|
| 522 |
+
encoded = repack_mxfp4(
|
| 523 |
+
packed,
|
| 524 |
+
scales,
|
| 525 |
+
tensor.rows,
|
| 526 |
+
tensor.cols,
|
| 527 |
+
)
|
| 528 |
+
written += handle.write(encoded.tobytes(order="C"))
|
| 529 |
+
return written
|
| 530 |
+
|
| 531 |
+
def _fused_experts_q2_k(self, tensor: TensorRecipe, handle: BinaryIO) -> int:
|
| 532 |
+
written = 0
|
| 533 |
+
with ThreadPoolExecutor(max_workers=Q2_K_EXPERT_WORKERS) as executor:
|
| 534 |
+
for start in range(0, N_EXPERTS, Q2_K_EXPERT_WORKERS):
|
| 535 |
+
futures = [
|
| 536 |
+
executor.submit(self._encode_q2_k_expert, tensor, expert)
|
| 537 |
+
for expert in range(
|
| 538 |
+
start, min(start + Q2_K_EXPERT_WORKERS, N_EXPERTS)
|
| 539 |
+
)
|
| 540 |
+
]
|
| 541 |
+
# Results are written in expert order, independent of worker
|
| 542 |
+
# completion order, so concurrency cannot change GGUF bytes.
|
| 543 |
+
for future in futures:
|
| 544 |
+
written += handle.write(future.result())
|
| 545 |
+
return written
|
| 546 |
+
|
| 547 |
+
def _encode_q2_k_expert(self, tensor: TensorRecipe, expert: int) -> bytes:
|
| 548 |
+
prefix = f"{tensor.name_prefix}.ffn.experts.{expert}.{tensor.expert_kind}"
|
| 549 |
+
packed = self.source.array(f"{prefix}.weight", np.uint8).reshape(
|
| 550 |
+
tensor.rows, tensor.cols // 2
|
| 551 |
+
)
|
| 552 |
+
scales = self.source.array(f"{prefix}.scale", np.uint8).reshape(
|
| 553 |
+
tensor.rows, tensor.cols // 32
|
| 554 |
+
)
|
| 555 |
+
chunks: list[bytes] = []
|
| 556 |
+
for row_start in range(0, tensor.rows, EXPERT_ROW_BATCH):
|
| 557 |
+
row_end = min(row_start + EXPERT_ROW_BATCH, tensor.rows)
|
| 558 |
+
decoded = decode_source_mxfp4(
|
| 559 |
+
packed[row_start:row_end],
|
| 560 |
+
scales[row_start:row_end],
|
| 561 |
+
row_end - row_start,
|
| 562 |
+
tensor.cols,
|
| 563 |
+
)
|
| 564 |
+
chunks.append(quantize_q2_k(decoded).tobytes(order="C"))
|
| 565 |
+
return b"".join(chunks)
|
| 566 |
+
|
| 567 |
+
def _markov_f16(self, tensor: TensorRecipe, handle: BinaryIO) -> int:
|
| 568 |
+
bits = self.source.array(tensor.src_name, "<u2").reshape(-1)
|
| 569 |
+
chunk_elements = 8 * 1024 * 1024
|
| 570 |
+
written = 0
|
| 571 |
+
for start in range(0, bits.size, chunk_elements):
|
| 572 |
+
f32 = bf16_bits_to_f32(bits[start : start + chunk_elements])
|
| 573 |
+
f16 = f32.astype("<f2")
|
| 574 |
+
written += handle.write(f16.tobytes(order="C"))
|
| 575 |
+
return written
|
| 576 |
+
|
| 577 |
+
|
| 578 |
+
def convert(
|
| 579 |
+
source_dir: Path,
|
| 580 |
+
output: Path,
|
| 581 |
+
*,
|
| 582 |
+
force: bool = False,
|
| 583 |
+
recipe: ArtifactRecipe | str | None = None,
|
| 584 |
+
) -> tuple[int, str]:
|
| 585 |
+
artifact_recipe = resolve_recipe(recipe)
|
| 586 |
+
source_dir = source_dir.resolve()
|
| 587 |
+
output = output.resolve()
|
| 588 |
+
if output.exists() and not force:
|
| 589 |
+
raise FileExistsError(
|
| 590 |
+
f"output already exists: {output}; pass --force to replace it"
|
| 591 |
+
)
|
| 592 |
+
output.parent.mkdir(parents=True, exist_ok=True)
|
| 593 |
+
print("[convert] validating pinned source sizes and SHA-256 values", flush=True)
|
| 594 |
+
verify_source_files(source_dir)
|
| 595 |
+
plan = build_plan(artifact_recipe)
|
| 596 |
+
temporary = output.with_name(f".{output.name}.partial-{os.getpid()}")
|
| 597 |
+
if temporary.exists():
|
| 598 |
+
raise FileExistsError(f"temporary output already exists: {temporary}")
|
| 599 |
+
try:
|
| 600 |
+
with SafeTensorSet(source_paths(source_dir)) as source:
|
| 601 |
+
source.validate_plan(plan)
|
| 602 |
+
print(
|
| 603 |
+
f"[convert] source layout valid; writing {len(plan)} tensors",
|
| 604 |
+
flush=True,
|
| 605 |
+
)
|
| 606 |
+
write_gguf(temporary, plan, TensorProducer(source))
|
| 607 |
+
size = temporary.stat().st_size
|
| 608 |
+
digest = sha256_path(temporary)
|
| 609 |
+
os.replace(temporary, output)
|
| 610 |
+
directory_fd = os.open(output.parent, os.O_RDONLY | os.O_DIRECTORY)
|
| 611 |
+
try:
|
| 612 |
+
os.fsync(directory_fd)
|
| 613 |
+
finally:
|
| 614 |
+
os.close(directory_fd)
|
| 615 |
+
except Exception:
|
| 616 |
+
temporary.unlink(missing_ok=True)
|
| 617 |
+
raise
|
| 618 |
+
print(f"[convert] wrote {output} ({size:,} bytes)", flush=True)
|
| 619 |
+
print(f"[convert] SHA-256 {digest}", flush=True)
|
| 620 |
+
return size, digest
|
| 621 |
+
|
| 622 |
+
|
| 623 |
+
def parse_args() -> argparse.Namespace:
|
| 624 |
+
parser = argparse.ArgumentParser(description=__doc__)
|
| 625 |
+
parser.add_argument(
|
| 626 |
+
"--sources", type=Path, required=True, help="Pinned shard directory"
|
| 627 |
+
)
|
| 628 |
+
parser.add_argument(
|
| 629 |
+
"--recipe",
|
| 630 |
+
choices=sorted(RECIPES),
|
| 631 |
+
default=DEFAULT_RECIPE.name,
|
| 632 |
+
help=f"Artifact recipe (default: {DEFAULT_RECIPE.name})",
|
| 633 |
+
)
|
| 634 |
+
parser.add_argument(
|
| 635 |
+
"--output",
|
| 636 |
+
type=Path,
|
| 637 |
+
help="Output GGUF path (default: the selected recipe's canonical filename)",
|
| 638 |
+
)
|
| 639 |
+
parser.add_argument(
|
| 640 |
+
"--force", action="store_true", help="Replace an existing output"
|
| 641 |
+
)
|
| 642 |
+
return parser.parse_args()
|
| 643 |
+
|
| 644 |
+
|
| 645 |
+
def main() -> None:
|
| 646 |
+
args = parse_args()
|
| 647 |
+
recipe = resolve_recipe(args.recipe)
|
| 648 |
+
output = args.output if args.output is not None else Path(recipe.output_filename)
|
| 649 |
+
convert(args.sources, output, force=args.force, recipe=recipe)
|
| 650 |
+
|
| 651 |
+
|
| 652 |
+
if __name__ == "__main__":
|
| 653 |
+
main()
|
scripts/download_sources.py
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""Download exactly the three pinned upstream DSpark shards and verify them."""
|
| 3 |
+
|
| 4 |
+
from __future__ import annotations
|
| 5 |
+
|
| 6 |
+
import argparse
|
| 7 |
+
import hashlib
|
| 8 |
+
import shutil
|
| 9 |
+
import subprocess
|
| 10 |
+
from pathlib import Path
|
| 11 |
+
|
| 12 |
+
from recipe import SOURCE_FILES, SOURCE_REPOSITORY, SOURCE_REVISION
|
| 13 |
+
|
| 14 |
+
|
| 15 |
+
def _sha256(path: Path) -> str:
|
| 16 |
+
digest = hashlib.sha256()
|
| 17 |
+
with path.open("rb") as handle:
|
| 18 |
+
while chunk := handle.read(8 * 1024 * 1024):
|
| 19 |
+
digest.update(chunk)
|
| 20 |
+
return digest.hexdigest()
|
| 21 |
+
|
| 22 |
+
|
| 23 |
+
def download(destination: Path, hf_executable: str) -> None:
|
| 24 |
+
destination.mkdir(parents=True, exist_ok=True)
|
| 25 |
+
command = [
|
| 26 |
+
hf_executable,
|
| 27 |
+
"download",
|
| 28 |
+
SOURCE_REPOSITORY,
|
| 29 |
+
"--revision",
|
| 30 |
+
SOURCE_REVISION,
|
| 31 |
+
"--local-dir",
|
| 32 |
+
str(destination),
|
| 33 |
+
]
|
| 34 |
+
for name in SOURCE_FILES:
|
| 35 |
+
command.extend(["--include", name])
|
| 36 |
+
subprocess.run(command, check=True)
|
| 37 |
+
for name, expected in SOURCE_FILES.items():
|
| 38 |
+
path = destination / name
|
| 39 |
+
if not path.is_file():
|
| 40 |
+
raise FileNotFoundError(f"download did not produce {path}")
|
| 41 |
+
size = path.stat().st_size
|
| 42 |
+
digest = _sha256(path)
|
| 43 |
+
if size != expected["size"] or digest != expected["sha256"]:
|
| 44 |
+
raise ValueError(
|
| 45 |
+
f"{name}: expected {expected['size']} bytes/{expected['sha256']}, "
|
| 46 |
+
f"found {size}/{digest}"
|
| 47 |
+
)
|
| 48 |
+
print(f"[download] verified {name}: {size:,} bytes {digest}", flush=True)
|
| 49 |
+
|
| 50 |
+
|
| 51 |
+
def parse_args() -> argparse.Namespace:
|
| 52 |
+
parser = argparse.ArgumentParser(description=__doc__)
|
| 53 |
+
parser.add_argument("--destination", type=Path, default=Path("sources"))
|
| 54 |
+
parser.add_argument(
|
| 55 |
+
"--hf",
|
| 56 |
+
default=shutil.which("hf"),
|
| 57 |
+
help="Path to the hf executable (the pinned public source needs no token)",
|
| 58 |
+
)
|
| 59 |
+
return parser.parse_args()
|
| 60 |
+
|
| 61 |
+
|
| 62 |
+
def main() -> None:
|
| 63 |
+
args = parse_args()
|
| 64 |
+
if not args.hf:
|
| 65 |
+
raise SystemExit("hf CLI not found on PATH; pass --hf /path/to/hf")
|
| 66 |
+
download(args.destination, args.hf)
|
| 67 |
+
|
| 68 |
+
|
| 69 |
+
if __name__ == "__main__":
|
| 70 |
+
main()
|
scripts/formats.py
ADDED
|
@@ -0,0 +1,456 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""Minimal safetensors and GGUF v3 I/O used by the reproducible build."""
|
| 3 |
+
|
| 4 |
+
from __future__ import annotations
|
| 5 |
+
|
| 6 |
+
import hashlib
|
| 7 |
+
import json
|
| 8 |
+
import mmap
|
| 9 |
+
import os
|
| 10 |
+
import struct
|
| 11 |
+
from dataclasses import dataclass
|
| 12 |
+
from pathlib import Path
|
| 13 |
+
from typing import BinaryIO, Callable, Iterable
|
| 14 |
+
|
| 15 |
+
import numpy as np
|
| 16 |
+
|
| 17 |
+
from recipe import (
|
| 18 |
+
GGML_F16,
|
| 19 |
+
GGML_F32,
|
| 20 |
+
GGML_MXFP4,
|
| 21 |
+
GGML_Q2_K,
|
| 22 |
+
GGML_Q8_0,
|
| 23 |
+
GGUF_METADATA,
|
| 24 |
+
SOURCE_FILES,
|
| 25 |
+
TensorRecipe,
|
| 26 |
+
source_expectations,
|
| 27 |
+
)
|
| 28 |
+
|
| 29 |
+
|
| 30 |
+
ALIGNMENT = 32
|
| 31 |
+
GGUF_VALUE_U32 = 4
|
| 32 |
+
GGUF_VALUE_I32 = 5
|
| 33 |
+
GGUF_VALUE_STRING = 8
|
| 34 |
+
GGUF_VALUE_ARRAY = 9
|
| 35 |
+
|
| 36 |
+
_DTYPE_BYTES = {
|
| 37 |
+
"F8_E4M3": 1,
|
| 38 |
+
"F8_E8M0": 1,
|
| 39 |
+
"I8": 1,
|
| 40 |
+
"BF16": 2,
|
| 41 |
+
"F16": 2,
|
| 42 |
+
"F32": 4,
|
| 43 |
+
}
|
| 44 |
+
|
| 45 |
+
|
| 46 |
+
def align_up(value: int, alignment: int = ALIGNMENT) -> int:
|
| 47 |
+
return (value + alignment - 1) // alignment * alignment
|
| 48 |
+
|
| 49 |
+
|
| 50 |
+
def sha256_path(path: Path, chunk_size: int = 8 * 1024 * 1024) -> str:
|
| 51 |
+
digest = hashlib.sha256()
|
| 52 |
+
with path.open("rb") as handle:
|
| 53 |
+
while chunk := handle.read(chunk_size):
|
| 54 |
+
digest.update(chunk)
|
| 55 |
+
return digest.hexdigest()
|
| 56 |
+
|
| 57 |
+
|
| 58 |
+
def verify_source_files(source_dir: Path) -> dict[str, dict[str, int | str]]:
|
| 59 |
+
observed: dict[str, dict[str, int | str]] = {}
|
| 60 |
+
for name, expected in SOURCE_FILES.items():
|
| 61 |
+
path = source_dir / name
|
| 62 |
+
if not path.is_file():
|
| 63 |
+
raise FileNotFoundError(f"missing pinned source shard: {path}")
|
| 64 |
+
size = path.stat().st_size
|
| 65 |
+
if size != expected["size"]:
|
| 66 |
+
raise ValueError(f"{name}: expected {expected['size']} bytes, found {size}")
|
| 67 |
+
digest = sha256_path(path)
|
| 68 |
+
if digest != expected["sha256"]:
|
| 69 |
+
raise ValueError(
|
| 70 |
+
f"{name}: expected SHA-256 {expected['sha256']}, found {digest}"
|
| 71 |
+
)
|
| 72 |
+
observed[name] = {"size": size, "sha256": digest}
|
| 73 |
+
return observed
|
| 74 |
+
|
| 75 |
+
|
| 76 |
+
@dataclass(frozen=True)
|
| 77 |
+
class SafeTensorInfo:
|
| 78 |
+
dtype: str
|
| 79 |
+
shape: tuple[int, ...]
|
| 80 |
+
start: int
|
| 81 |
+
end: int
|
| 82 |
+
shard_index: int
|
| 83 |
+
|
| 84 |
+
|
| 85 |
+
class SafeTensorSet:
|
| 86 |
+
"""Read-only mmap index spanning the three pinned safetensors shards."""
|
| 87 |
+
|
| 88 |
+
def __init__(self, paths: Iterable[Path]):
|
| 89 |
+
self._files: list[BinaryIO] = []
|
| 90 |
+
self._maps: list[mmap.mmap] = []
|
| 91 |
+
self._index: dict[str, SafeTensorInfo] = {}
|
| 92 |
+
try:
|
| 93 |
+
for shard_index, path in enumerate(paths):
|
| 94 |
+
handle = path.open("rb")
|
| 95 |
+
mapping = mmap.mmap(handle.fileno(), 0, access=mmap.ACCESS_READ)
|
| 96 |
+
self._files.append(handle)
|
| 97 |
+
self._maps.append(mapping)
|
| 98 |
+
self._index_shard(path, shard_index, mapping)
|
| 99 |
+
except Exception:
|
| 100 |
+
self.close()
|
| 101 |
+
raise
|
| 102 |
+
|
| 103 |
+
def _index_shard(self, path: Path, shard_index: int, mapping: mmap.mmap) -> None:
|
| 104 |
+
if len(mapping) < 8:
|
| 105 |
+
raise ValueError(f"{path}: shorter than safetensors header length")
|
| 106 |
+
header_len = struct.unpack_from("<Q", mapping, 0)[0]
|
| 107 |
+
data_start = 8 + header_len
|
| 108 |
+
if data_start > len(mapping):
|
| 109 |
+
raise ValueError(f"{path}: safetensors header overruns file")
|
| 110 |
+
try:
|
| 111 |
+
header = json.loads(mapping[8:data_start])
|
| 112 |
+
except (UnicodeDecodeError, json.JSONDecodeError) as error:
|
| 113 |
+
raise ValueError(f"{path}: invalid safetensors header: {error}") from error
|
| 114 |
+
if not isinstance(header, dict):
|
| 115 |
+
raise ValueError(f"{path}: safetensors header is not an object")
|
| 116 |
+
for name, raw in header.items():
|
| 117 |
+
if name == "__metadata__" or not name.startswith("mtp."):
|
| 118 |
+
continue
|
| 119 |
+
if name in self._index:
|
| 120 |
+
raise ValueError(f"duplicate tensor across shards: {name}")
|
| 121 |
+
if not isinstance(raw, dict):
|
| 122 |
+
raise ValueError(f"{path}: tensor {name} metadata is not an object")
|
| 123 |
+
dtype = raw.get("dtype")
|
| 124 |
+
shape = raw.get("shape")
|
| 125 |
+
offsets = raw.get("data_offsets")
|
| 126 |
+
if (
|
| 127 |
+
not isinstance(dtype, str)
|
| 128 |
+
or not isinstance(shape, list)
|
| 129 |
+
or not all(isinstance(dim, int) and dim >= 0 for dim in shape)
|
| 130 |
+
or not isinstance(offsets, list)
|
| 131 |
+
or len(offsets) != 2
|
| 132 |
+
or not all(
|
| 133 |
+
isinstance(offset, int) and offset >= 0 for offset in offsets
|
| 134 |
+
)
|
| 135 |
+
):
|
| 136 |
+
raise ValueError(f"{path}: malformed metadata for tensor {name}")
|
| 137 |
+
relative_start, relative_end = offsets
|
| 138 |
+
start = data_start + relative_start
|
| 139 |
+
end = data_start + relative_end
|
| 140 |
+
if relative_start > relative_end or end > len(mapping):
|
| 141 |
+
raise ValueError(f"{path}: tensor {name} data range overruns shard")
|
| 142 |
+
if dtype in _DTYPE_BYTES:
|
| 143 |
+
count = 1
|
| 144 |
+
for dim in shape:
|
| 145 |
+
count *= dim
|
| 146 |
+
expected_bytes = count * _DTYPE_BYTES[dtype]
|
| 147 |
+
if end - start != expected_bytes:
|
| 148 |
+
raise ValueError(
|
| 149 |
+
f"{path}: tensor {name} needs {expected_bytes} bytes, "
|
| 150 |
+
f"data range contains {end - start}"
|
| 151 |
+
)
|
| 152 |
+
self._index[name] = SafeTensorInfo(
|
| 153 |
+
dtype=dtype,
|
| 154 |
+
shape=tuple(shape),
|
| 155 |
+
start=start,
|
| 156 |
+
end=end,
|
| 157 |
+
shard_index=shard_index,
|
| 158 |
+
)
|
| 159 |
+
|
| 160 |
+
def close(self) -> None:
|
| 161 |
+
for mapping in reversed(getattr(self, "_maps", [])):
|
| 162 |
+
try:
|
| 163 |
+
mapping.close()
|
| 164 |
+
except BufferError:
|
| 165 |
+
pass
|
| 166 |
+
for handle in reversed(getattr(self, "_files", [])):
|
| 167 |
+
handle.close()
|
| 168 |
+
self._maps = []
|
| 169 |
+
self._files = []
|
| 170 |
+
|
| 171 |
+
def __enter__(self) -> "SafeTensorSet":
|
| 172 |
+
return self
|
| 173 |
+
|
| 174 |
+
def __exit__(self, *_: object) -> None:
|
| 175 |
+
self.close()
|
| 176 |
+
|
| 177 |
+
def info(self, name: str) -> SafeTensorInfo:
|
| 178 |
+
try:
|
| 179 |
+
return self._index[name]
|
| 180 |
+
except KeyError as error:
|
| 181 |
+
raise KeyError(f"source tensor not found: {name}") from error
|
| 182 |
+
|
| 183 |
+
def raw(self, name: str) -> memoryview:
|
| 184 |
+
info = self.info(name)
|
| 185 |
+
return memoryview(self._maps[info.shard_index])[info.start : info.end]
|
| 186 |
+
|
| 187 |
+
def array(self, name: str, dtype: np.dtype | str) -> np.ndarray:
|
| 188 |
+
info = self.info(name)
|
| 189 |
+
return np.frombuffer(
|
| 190 |
+
self._maps[info.shard_index],
|
| 191 |
+
dtype=dtype,
|
| 192 |
+
count=(info.end - info.start) // np.dtype(dtype).itemsize,
|
| 193 |
+
offset=info.start,
|
| 194 |
+
).reshape(info.shape)
|
| 195 |
+
|
| 196 |
+
def validate_plan(self, plan: Iterable[TensorRecipe]) -> None:
|
| 197 |
+
expected_names: set[str] = set()
|
| 198 |
+
for tensor in plan:
|
| 199 |
+
for name, dtype, shape in source_expectations(tensor):
|
| 200 |
+
if name in expected_names:
|
| 201 |
+
raise ValueError(
|
| 202 |
+
f"recipe consumes source tensor more than once: {name}"
|
| 203 |
+
)
|
| 204 |
+
expected_names.add(name)
|
| 205 |
+
info = self.info(name)
|
| 206 |
+
if info.dtype != dtype:
|
| 207 |
+
raise ValueError(
|
| 208 |
+
f"{name}: expected dtype {dtype}, found {info.dtype}"
|
| 209 |
+
)
|
| 210 |
+
if info.shape != shape:
|
| 211 |
+
raise ValueError(
|
| 212 |
+
f"{name}: expected shape {shape}, found {info.shape}"
|
| 213 |
+
)
|
| 214 |
+
observed_names = self.names()
|
| 215 |
+
if observed_names != expected_names:
|
| 216 |
+
missing = sorted(expected_names - observed_names)
|
| 217 |
+
unexpected = sorted(observed_names - expected_names)
|
| 218 |
+
raise ValueError(
|
| 219 |
+
"source tensor inventory differs from the recipe: "
|
| 220 |
+
f"missing={missing[:5]}, unexpected={unexpected[:5]}"
|
| 221 |
+
)
|
| 222 |
+
|
| 223 |
+
def names(self) -> frozenset[str]:
|
| 224 |
+
return frozenset(self._index)
|
| 225 |
+
|
| 226 |
+
|
| 227 |
+
def _write_string(handle: BinaryIO, value: str) -> None:
|
| 228 |
+
encoded = value.encode("utf-8")
|
| 229 |
+
handle.write(struct.pack("<Q", len(encoded)))
|
| 230 |
+
handle.write(encoded)
|
| 231 |
+
|
| 232 |
+
|
| 233 |
+
def write_gguf(
|
| 234 |
+
path: Path,
|
| 235 |
+
plan: tuple[TensorRecipe, ...],
|
| 236 |
+
produce: Callable[[TensorRecipe, BinaryIO], int],
|
| 237 |
+
) -> None:
|
| 238 |
+
"""Write deterministic GGUF v3 data, streaming each tensor in plan order."""
|
| 239 |
+
|
| 240 |
+
with path.open("wb", buffering=8 * 1024 * 1024) as handle:
|
| 241 |
+
handle.write(b"GGUF")
|
| 242 |
+
handle.write(struct.pack("<IQQ", 3, len(plan), len(GGUF_METADATA)))
|
| 243 |
+
for key, kind, value in GGUF_METADATA:
|
| 244 |
+
_write_string(handle, key)
|
| 245 |
+
if kind == "u32":
|
| 246 |
+
handle.write(struct.pack("<II", GGUF_VALUE_U32, int(value)))
|
| 247 |
+
elif kind == "string":
|
| 248 |
+
handle.write(struct.pack("<I", GGUF_VALUE_STRING))
|
| 249 |
+
_write_string(handle, str(value))
|
| 250 |
+
elif kind == "array_i32":
|
| 251 |
+
values = tuple(int(item) for item in value)
|
| 252 |
+
handle.write(
|
| 253 |
+
struct.pack("<IIQ", GGUF_VALUE_ARRAY, GGUF_VALUE_I32, len(values))
|
| 254 |
+
)
|
| 255 |
+
for item in values:
|
| 256 |
+
handle.write(struct.pack("<i", item))
|
| 257 |
+
else:
|
| 258 |
+
raise ValueError(f"unsupported GGUF metadata kind: {kind}")
|
| 259 |
+
|
| 260 |
+
relative_offset = 0
|
| 261 |
+
relative_offsets: list[int] = []
|
| 262 |
+
for tensor in plan:
|
| 263 |
+
relative_offsets.append(relative_offset)
|
| 264 |
+
relative_offset = align_up(relative_offset + tensor.byte_len)
|
| 265 |
+
for tensor, offset in zip(plan, relative_offsets, strict=True):
|
| 266 |
+
_write_string(handle, tensor.out_name)
|
| 267 |
+
handle.write(struct.pack("<I", len(tensor.dims)))
|
| 268 |
+
for dim in tensor.dims:
|
| 269 |
+
handle.write(struct.pack("<Q", dim))
|
| 270 |
+
handle.write(struct.pack("<IQ", tensor.ggml_type, offset))
|
| 271 |
+
|
| 272 |
+
data_start = align_up(handle.tell())
|
| 273 |
+
handle.write(b"\0" * (data_start - handle.tell()))
|
| 274 |
+
relative_written = 0
|
| 275 |
+
for tensor in plan:
|
| 276 |
+
before = handle.tell()
|
| 277 |
+
produced = produce(tensor, handle)
|
| 278 |
+
observed = handle.tell() - before
|
| 279 |
+
if produced != observed or observed != tensor.byte_len:
|
| 280 |
+
raise ValueError(
|
| 281 |
+
f"{tensor.out_name}: expected {tensor.byte_len} bytes, "
|
| 282 |
+
f"producer reported {produced} and wrote {observed}"
|
| 283 |
+
)
|
| 284 |
+
relative_written += observed
|
| 285 |
+
padded = align_up(relative_written)
|
| 286 |
+
handle.write(b"\0" * (padded - relative_written))
|
| 287 |
+
relative_written = padded
|
| 288 |
+
handle.flush()
|
| 289 |
+
os.fsync(handle.fileno())
|
| 290 |
+
|
| 291 |
+
|
| 292 |
+
@dataclass(frozen=True)
|
| 293 |
+
class GgufTensor:
|
| 294 |
+
name: str
|
| 295 |
+
dims: tuple[int, ...]
|
| 296 |
+
ggml_type: int
|
| 297 |
+
relative_offset: int
|
| 298 |
+
byte_len: int
|
| 299 |
+
absolute_offset: int
|
| 300 |
+
|
| 301 |
+
|
| 302 |
+
class GgufReader:
|
| 303 |
+
def __init__(self, path: Path):
|
| 304 |
+
self.path = path
|
| 305 |
+
self._file = path.open("rb")
|
| 306 |
+
self._map = mmap.mmap(self._file.fileno(), 0, access=mmap.ACCESS_READ)
|
| 307 |
+
self.metadata: dict[str, object] = {}
|
| 308 |
+
self.tensors: tuple[GgufTensor, ...] = ()
|
| 309 |
+
self.directory_end = 0
|
| 310 |
+
self.data_start = 0
|
| 311 |
+
try:
|
| 312 |
+
self._parse()
|
| 313 |
+
except Exception:
|
| 314 |
+
self.close()
|
| 315 |
+
raise
|
| 316 |
+
|
| 317 |
+
def close(self) -> None:
|
| 318 |
+
try:
|
| 319 |
+
self._map.close()
|
| 320 |
+
except BufferError:
|
| 321 |
+
pass
|
| 322 |
+
self._file.close()
|
| 323 |
+
|
| 324 |
+
def __enter__(self) -> "GgufReader":
|
| 325 |
+
return self
|
| 326 |
+
|
| 327 |
+
def __exit__(self, *_: object) -> None:
|
| 328 |
+
self.close()
|
| 329 |
+
|
| 330 |
+
def _unpack(self, fmt: str, cursor: int) -> tuple[tuple[object, ...], int]:
|
| 331 |
+
size = struct.calcsize(fmt)
|
| 332 |
+
if cursor + size > len(self._map):
|
| 333 |
+
raise ValueError("GGUF directory is truncated")
|
| 334 |
+
return struct.unpack_from(fmt, self._map, cursor), cursor + size
|
| 335 |
+
|
| 336 |
+
def _string(self, cursor: int) -> tuple[str, int]:
|
| 337 |
+
(length,), cursor = self._unpack("<Q", cursor)
|
| 338 |
+
end = cursor + int(length)
|
| 339 |
+
if end > len(self._map):
|
| 340 |
+
raise ValueError("GGUF string overruns file")
|
| 341 |
+
try:
|
| 342 |
+
value = self._map[cursor:end].decode("utf-8")
|
| 343 |
+
except UnicodeDecodeError as error:
|
| 344 |
+
raise ValueError(f"GGUF string is not UTF-8: {error}") from error
|
| 345 |
+
return value, end
|
| 346 |
+
|
| 347 |
+
def _parse(self) -> None:
|
| 348 |
+
if self._map[:4] != b"GGUF":
|
| 349 |
+
raise ValueError("not a GGUF file")
|
| 350 |
+
(version, tensor_count, kv_count), cursor = self._unpack("<IQQ", 4)
|
| 351 |
+
if version != 3:
|
| 352 |
+
raise ValueError(f"expected GGUF v3, found v{version}")
|
| 353 |
+
for _ in range(int(kv_count)):
|
| 354 |
+
key, cursor = self._string(cursor)
|
| 355 |
+
(value_type,), cursor = self._unpack("<I", cursor)
|
| 356 |
+
if value_type == GGUF_VALUE_U32:
|
| 357 |
+
(value,), cursor = self._unpack("<I", cursor)
|
| 358 |
+
elif value_type == GGUF_VALUE_STRING:
|
| 359 |
+
value, cursor = self._string(cursor)
|
| 360 |
+
elif value_type == GGUF_VALUE_ARRAY:
|
| 361 |
+
(element_type, length), cursor = self._unpack("<IQ", cursor)
|
| 362 |
+
if element_type != GGUF_VALUE_I32:
|
| 363 |
+
raise ValueError(f"unsupported GGUF array type {element_type}")
|
| 364 |
+
values = []
|
| 365 |
+
for _ in range(int(length)):
|
| 366 |
+
(item,), cursor = self._unpack("<i", cursor)
|
| 367 |
+
values.append(item)
|
| 368 |
+
value = tuple(values)
|
| 369 |
+
else:
|
| 370 |
+
raise ValueError(f"unsupported GGUF metadata type {value_type}")
|
| 371 |
+
if key in self.metadata:
|
| 372 |
+
raise ValueError(f"duplicate GGUF metadata key {key}")
|
| 373 |
+
self.metadata[key] = value
|
| 374 |
+
|
| 375 |
+
directory: list[tuple[str, tuple[int, ...], int, int]] = []
|
| 376 |
+
for _ in range(int(tensor_count)):
|
| 377 |
+
name, cursor = self._string(cursor)
|
| 378 |
+
(ndim,), cursor = self._unpack("<I", cursor)
|
| 379 |
+
dims = []
|
| 380 |
+
for _ in range(int(ndim)):
|
| 381 |
+
(dim,), cursor = self._unpack("<Q", cursor)
|
| 382 |
+
dims.append(int(dim))
|
| 383 |
+
(ggml_type, relative_offset), cursor = self._unpack("<IQ", cursor)
|
| 384 |
+
directory.append((name, tuple(dims), int(ggml_type), int(relative_offset)))
|
| 385 |
+
|
| 386 |
+
self.directory_end = cursor
|
| 387 |
+
self.data_start = align_up(cursor)
|
| 388 |
+
tensors: list[GgufTensor] = []
|
| 389 |
+
seen: set[str] = set()
|
| 390 |
+
previous_end = self.data_start
|
| 391 |
+
for name, dims, ggml_type, relative_offset in directory:
|
| 392 |
+
if name in seen:
|
| 393 |
+
raise ValueError(f"duplicate GGUF tensor {name}")
|
| 394 |
+
seen.add(name)
|
| 395 |
+
byte_len = ggml_nbytes(dims, ggml_type)
|
| 396 |
+
absolute_offset = self.data_start + relative_offset
|
| 397 |
+
if absolute_offset % ALIGNMENT:
|
| 398 |
+
raise ValueError(f"tensor {name} is not {ALIGNMENT}-byte aligned")
|
| 399 |
+
if absolute_offset < previous_end:
|
| 400 |
+
raise ValueError(f"tensor {name} overlaps the previous tensor")
|
| 401 |
+
if absolute_offset + byte_len > len(self._map):
|
| 402 |
+
raise ValueError(f"tensor {name} overruns GGUF file")
|
| 403 |
+
tensors.append(
|
| 404 |
+
GgufTensor(
|
| 405 |
+
name=name,
|
| 406 |
+
dims=dims,
|
| 407 |
+
ggml_type=ggml_type,
|
| 408 |
+
relative_offset=relative_offset,
|
| 409 |
+
byte_len=byte_len,
|
| 410 |
+
absolute_offset=absolute_offset,
|
| 411 |
+
)
|
| 412 |
+
)
|
| 413 |
+
previous_end = absolute_offset + byte_len
|
| 414 |
+
self.tensors = tuple(tensors)
|
| 415 |
+
|
| 416 |
+
def tensor(self, name: str) -> GgufTensor:
|
| 417 |
+
for tensor in self.tensors:
|
| 418 |
+
if tensor.name == name:
|
| 419 |
+
return tensor
|
| 420 |
+
raise KeyError(f"GGUF tensor not found: {name}")
|
| 421 |
+
|
| 422 |
+
def tensor_array(self, tensor: GgufTensor, dtype: np.dtype | str) -> np.ndarray:
|
| 423 |
+
return np.frombuffer(
|
| 424 |
+
self._map,
|
| 425 |
+
dtype=dtype,
|
| 426 |
+
count=tensor.byte_len // np.dtype(dtype).itemsize,
|
| 427 |
+
offset=tensor.absolute_offset,
|
| 428 |
+
)
|
| 429 |
+
|
| 430 |
+
def bytes_at(self, start: int, end: int) -> bytes:
|
| 431 |
+
if start < 0 or start > end or end > len(self._map):
|
| 432 |
+
raise ValueError("GGUF byte range is outside the file")
|
| 433 |
+
return self._map[start:end]
|
| 434 |
+
|
| 435 |
+
|
| 436 |
+
def ggml_nbytes(dims: tuple[int, ...], ggml_type: int) -> int:
|
| 437 |
+
elements = 1
|
| 438 |
+
for dim in dims:
|
| 439 |
+
elements *= dim
|
| 440 |
+
if ggml_type == GGML_F32:
|
| 441 |
+
return elements * 4
|
| 442 |
+
if ggml_type == GGML_F16:
|
| 443 |
+
return elements * 2
|
| 444 |
+
if ggml_type == GGML_Q8_0:
|
| 445 |
+
if elements % 32:
|
| 446 |
+
raise ValueError("Q8_0 tensor is not block aligned")
|
| 447 |
+
return elements // 32 * 34
|
| 448 |
+
if ggml_type == GGML_Q2_K:
|
| 449 |
+
if elements % 256:
|
| 450 |
+
raise ValueError("Q2_K tensor is not block aligned")
|
| 451 |
+
return elements // 256 * 84
|
| 452 |
+
if ggml_type == GGML_MXFP4:
|
| 453 |
+
if elements % 32:
|
| 454 |
+
raise ValueError("MXFP4 tensor is not block aligned")
|
| 455 |
+
return elements // 32 * 17
|
| 456 |
+
raise ValueError(f"unsupported GGML tensor type {ggml_type}")
|
scripts/recipe.py
ADDED
|
@@ -0,0 +1,519 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""Immutable source manifest and deterministic DSpark GGUF tensor recipe."""
|
| 3 |
+
|
| 4 |
+
from __future__ import annotations
|
| 5 |
+
|
| 6 |
+
import json
|
| 7 |
+
from dataclasses import dataclass
|
| 8 |
+
from pathlib import Path
|
| 9 |
+
from typing import Iterator
|
| 10 |
+
|
| 11 |
+
|
| 12 |
+
REPOSITORY_ROOT = Path(__file__).resolve().parents[1]
|
| 13 |
+
SOURCE_MANIFEST_PATH = REPOSITORY_ROOT / "manifest" / "source.json"
|
| 14 |
+
with SOURCE_MANIFEST_PATH.open(encoding="utf-8") as _manifest_handle:
|
| 15 |
+
_SOURCE_MANIFEST = json.load(_manifest_handle)
|
| 16 |
+
|
| 17 |
+
SOURCE_REPOSITORY = str(_SOURCE_MANIFEST["repository"])
|
| 18 |
+
SOURCE_REVISION = str(_SOURCE_MANIFEST["revision"])
|
| 19 |
+
SOURCE_FILES = {
|
| 20 |
+
str(name): {"size": int(properties["size"]), "sha256": str(properties["sha256"])}
|
| 21 |
+
for name, properties in _SOURCE_MANIFEST["files"].items()
|
| 22 |
+
}
|
| 23 |
+
if sum(int(properties["size"]) for properties in SOURCE_FILES.values()) != int(
|
| 24 |
+
_SOURCE_MANIFEST["total_size"]
|
| 25 |
+
):
|
| 26 |
+
raise ValueError(
|
| 27 |
+
f"{SOURCE_MANIFEST_PATH}: total_size does not match its file entries"
|
| 28 |
+
)
|
| 29 |
+
|
| 30 |
+
ARCHITECTURE = "deepseek_v4_flash_dspark_draft"
|
| 31 |
+
GENERAL_NAME = "DeepSeek-V4-Flash-0731-DSpark-Drafter"
|
| 32 |
+
RECIPE_VERSION = 1
|
| 33 |
+
|
| 34 |
+
BLOCK_SIZE = 5
|
| 35 |
+
MARKOV_RANK = 256
|
| 36 |
+
NOISE_TOKEN_ID = 128_799
|
| 37 |
+
TARGET_LAYER_IDS = (40, 41, 42)
|
| 38 |
+
LAYER_COUNT = 3
|
| 39 |
+
HEAD_LAYER = 2
|
| 40 |
+
N_EMBD = 4096
|
| 41 |
+
N_EXPERTS = 256
|
| 42 |
+
VOCAB_SIZE = 129_280
|
| 43 |
+
|
| 44 |
+
GGML_F32 = 0
|
| 45 |
+
GGML_F16 = 1
|
| 46 |
+
GGML_Q8_0 = 8
|
| 47 |
+
GGML_Q2_K = 10
|
| 48 |
+
GGML_MXFP4 = 39
|
| 49 |
+
GGML_TYPE_NAMES = {
|
| 50 |
+
GGML_F32: "F32",
|
| 51 |
+
GGML_F16: "F16",
|
| 52 |
+
GGML_Q8_0: "Q8_0",
|
| 53 |
+
GGML_Q2_K: "Q2_K",
|
| 54 |
+
GGML_MXFP4: "MXFP4",
|
| 55 |
+
}
|
| 56 |
+
|
| 57 |
+
KIND_PLAIN_F32 = "plain_f32"
|
| 58 |
+
KIND_RELABEL_F32 = "relabel_f32"
|
| 59 |
+
KIND_FP8_TO_Q8_0 = "fp8_to_q8_0"
|
| 60 |
+
KIND_FUSED_EXPERTS_MXFP4 = "fused_experts_mxfp4"
|
| 61 |
+
KIND_FUSED_EXPERTS_Q2_K = "fused_experts_q2_k"
|
| 62 |
+
KIND_MARKOV_F16 = "markov_f16"
|
| 63 |
+
|
| 64 |
+
RECIPE_MXFP4_Q8_0 = "mxfp4-q8_0"
|
| 65 |
+
RECIPE_Q2_K_Q8_0 = "q2_k-q8_0"
|
| 66 |
+
|
| 67 |
+
|
| 68 |
+
@dataclass(frozen=True)
|
| 69 |
+
class ArtifactRecipe:
|
| 70 |
+
"""One output policy over the shared canonical tensor/source plan."""
|
| 71 |
+
|
| 72 |
+
name: str
|
| 73 |
+
output_filename: str
|
| 74 |
+
routed_expert_kind: str
|
| 75 |
+
routed_expert_type: int
|
| 76 |
+
expected_tensor_bytes: int
|
| 77 |
+
expected_file_size: int
|
| 78 |
+
expected_type_counts: tuple[tuple[int, int], ...]
|
| 79 |
+
q2_k_error_limit: float | None = None
|
| 80 |
+
|
| 81 |
+
def manifest_filename(self, kind: str) -> str:
|
| 82 |
+
if kind not in {"build", "validation"}:
|
| 83 |
+
raise ValueError(f"unknown manifest kind: {kind}")
|
| 84 |
+
if self.name == RECIPE_MXFP4_Q8_0:
|
| 85 |
+
return f"{kind}.json"
|
| 86 |
+
return f"{kind}-{self.name}.json"
|
| 87 |
+
|
| 88 |
+
|
| 89 |
+
MXFP4_Q8_0_RECIPE = ArtifactRecipe(
|
| 90 |
+
name=RECIPE_MXFP4_Q8_0,
|
| 91 |
+
output_filename="DeepSeek-V4-Flash-0731-DSpark-Drafter-MXFP4-Q8_0.gguf",
|
| 92 |
+
routed_expert_kind=KIND_FUSED_EXPERTS_MXFP4,
|
| 93 |
+
routed_expert_type=GGML_MXFP4,
|
| 94 |
+
expected_tensor_bytes=10_897_104_284,
|
| 95 |
+
expected_file_size=10_897_110_272,
|
| 96 |
+
expected_type_counts=(
|
| 97 |
+
(GGML_F32, 45),
|
| 98 |
+
(GGML_F16, 2),
|
| 99 |
+
(GGML_Q8_0, 25),
|
| 100 |
+
(GGML_MXFP4, 9),
|
| 101 |
+
),
|
| 102 |
+
)
|
| 103 |
+
Q2_K_Q8_0_RECIPE = ArtifactRecipe(
|
| 104 |
+
name=RECIPE_Q2_K_Q8_0,
|
| 105 |
+
output_filename="DeepSeek-V4-Flash-0731-DSpark-Drafter-Q2_K-Q8_0.gguf",
|
| 106 |
+
routed_expert_kind=KIND_FUSED_EXPERTS_Q2_K,
|
| 107 |
+
routed_expert_type=GGML_Q2_K,
|
| 108 |
+
expected_tensor_bytes=6_971_235_740,
|
| 109 |
+
expected_file_size=6_971_241_728,
|
| 110 |
+
expected_type_counts=(
|
| 111 |
+
(GGML_F32, 45),
|
| 112 |
+
(GGML_F16, 2),
|
| 113 |
+
(GGML_Q8_0, 25),
|
| 114 |
+
(GGML_Q2_K, 9),
|
| 115 |
+
),
|
| 116 |
+
q2_k_error_limit=0.50,
|
| 117 |
+
)
|
| 118 |
+
RECIPES = {recipe.name: recipe for recipe in (MXFP4_Q8_0_RECIPE, Q2_K_Q8_0_RECIPE)}
|
| 119 |
+
DEFAULT_RECIPE = MXFP4_Q8_0_RECIPE
|
| 120 |
+
OUTPUT_FILENAME = DEFAULT_RECIPE.output_filename
|
| 121 |
+
|
| 122 |
+
|
| 123 |
+
def resolve_recipe(recipe: ArtifactRecipe | str | None = None) -> ArtifactRecipe:
|
| 124 |
+
if recipe is None:
|
| 125 |
+
return DEFAULT_RECIPE
|
| 126 |
+
if isinstance(recipe, ArtifactRecipe):
|
| 127 |
+
return recipe
|
| 128 |
+
try:
|
| 129 |
+
return RECIPES[recipe]
|
| 130 |
+
except KeyError as error:
|
| 131 |
+
choices = ", ".join(sorted(RECIPES))
|
| 132 |
+
raise ValueError(
|
| 133 |
+
f"unknown recipe {recipe!r}; choose one of: {choices}"
|
| 134 |
+
) from error
|
| 135 |
+
|
| 136 |
+
|
| 137 |
+
@dataclass(frozen=True)
|
| 138 |
+
class TensorRecipe:
|
| 139 |
+
out_name: str
|
| 140 |
+
kind: str
|
| 141 |
+
dims: tuple[int, ...]
|
| 142 |
+
ggml_type: int
|
| 143 |
+
src_name: str = ""
|
| 144 |
+
src_is_bf16: bool = False
|
| 145 |
+
src_shape: tuple[int, ...] = ()
|
| 146 |
+
weight_name: str = ""
|
| 147 |
+
scale_name: str = ""
|
| 148 |
+
rows: int = 0
|
| 149 |
+
cols: int = 0
|
| 150 |
+
name_prefix: str = ""
|
| 151 |
+
expert_kind: str = ""
|
| 152 |
+
|
| 153 |
+
@property
|
| 154 |
+
def elements(self) -> int:
|
| 155 |
+
value = 1
|
| 156 |
+
for dim in self.dims:
|
| 157 |
+
value *= dim
|
| 158 |
+
return value
|
| 159 |
+
|
| 160 |
+
@property
|
| 161 |
+
def byte_len(self) -> int:
|
| 162 |
+
if self.ggml_type == GGML_F32:
|
| 163 |
+
return self.elements * 4
|
| 164 |
+
if self.ggml_type == GGML_F16:
|
| 165 |
+
return self.elements * 2
|
| 166 |
+
if self.ggml_type == GGML_Q8_0:
|
| 167 |
+
if self.elements % 32:
|
| 168 |
+
raise ValueError(
|
| 169 |
+
f"{self.out_name}: Q8_0 element count is not block aligned"
|
| 170 |
+
)
|
| 171 |
+
return self.elements // 32 * 34
|
| 172 |
+
if self.ggml_type == GGML_Q2_K:
|
| 173 |
+
if self.elements % 256:
|
| 174 |
+
raise ValueError(
|
| 175 |
+
f"{self.out_name}: Q2_K element count is not block aligned"
|
| 176 |
+
)
|
| 177 |
+
return self.elements // 256 * 84
|
| 178 |
+
if self.ggml_type == GGML_MXFP4:
|
| 179 |
+
if self.elements % 32:
|
| 180 |
+
raise ValueError(
|
| 181 |
+
f"{self.out_name}: MXFP4 element count is not block aligned"
|
| 182 |
+
)
|
| 183 |
+
return self.elements // 32 * 17
|
| 184 |
+
raise ValueError(f"{self.out_name}: unsupported GGML type {self.ggml_type}")
|
| 185 |
+
|
| 186 |
+
|
| 187 |
+
def _plain(
|
| 188 |
+
out_name: str,
|
| 189 |
+
src_name: str,
|
| 190 |
+
*,
|
| 191 |
+
bf16: bool,
|
| 192 |
+
dims: tuple[int, ...],
|
| 193 |
+
src_shape: tuple[int, ...] | None = None,
|
| 194 |
+
) -> TensorRecipe:
|
| 195 |
+
return TensorRecipe(
|
| 196 |
+
out_name=out_name,
|
| 197 |
+
kind=KIND_PLAIN_F32,
|
| 198 |
+
dims=dims,
|
| 199 |
+
ggml_type=GGML_F32,
|
| 200 |
+
src_name=src_name,
|
| 201 |
+
src_is_bf16=bf16,
|
| 202 |
+
src_shape=dims if src_shape is None else src_shape,
|
| 203 |
+
)
|
| 204 |
+
|
| 205 |
+
|
| 206 |
+
def _fp8(
|
| 207 |
+
out_name: str,
|
| 208 |
+
weight_name: str,
|
| 209 |
+
scale_name: str,
|
| 210 |
+
rows: int,
|
| 211 |
+
cols: int,
|
| 212 |
+
) -> TensorRecipe:
|
| 213 |
+
return TensorRecipe(
|
| 214 |
+
out_name=out_name,
|
| 215 |
+
kind=KIND_FP8_TO_Q8_0,
|
| 216 |
+
dims=(cols, rows),
|
| 217 |
+
ggml_type=GGML_Q8_0,
|
| 218 |
+
weight_name=weight_name,
|
| 219 |
+
scale_name=scale_name,
|
| 220 |
+
rows=rows,
|
| 221 |
+
cols=cols,
|
| 222 |
+
)
|
| 223 |
+
|
| 224 |
+
|
| 225 |
+
def _layer_plan(layer: int, artifact_recipe: ArtifactRecipe) -> list[TensorRecipe]:
|
| 226 |
+
prefix = f"mtp.{layer}"
|
| 227 |
+
out: list[TensorRecipe] = [
|
| 228 |
+
_plain(
|
| 229 |
+
f"dspark.{layer}.attn_sinks.weight",
|
| 230 |
+
f"{prefix}.attn.attn_sink",
|
| 231 |
+
bf16=False,
|
| 232 |
+
dims=(64,),
|
| 233 |
+
),
|
| 234 |
+
_plain(
|
| 235 |
+
f"dspark.{layer}.attn_norm.weight",
|
| 236 |
+
f"{prefix}.attn_norm.weight",
|
| 237 |
+
bf16=True,
|
| 238 |
+
dims=(N_EMBD,),
|
| 239 |
+
),
|
| 240 |
+
_plain(
|
| 241 |
+
f"dspark.{layer}.ffn_norm.weight",
|
| 242 |
+
f"{prefix}.ffn_norm.weight",
|
| 243 |
+
bf16=True,
|
| 244 |
+
dims=(N_EMBD,),
|
| 245 |
+
),
|
| 246 |
+
_plain(
|
| 247 |
+
f"dspark.{layer}.attn_kv_a_norm.weight",
|
| 248 |
+
f"{prefix}.attn.kv_norm.weight",
|
| 249 |
+
bf16=True,
|
| 250 |
+
dims=(512,),
|
| 251 |
+
),
|
| 252 |
+
_plain(
|
| 253 |
+
f"dspark.{layer}.attn_q_a_norm.weight",
|
| 254 |
+
f"{prefix}.attn.q_norm.weight",
|
| 255 |
+
bf16=True,
|
| 256 |
+
dims=(1024,),
|
| 257 |
+
),
|
| 258 |
+
_fp8(
|
| 259 |
+
f"dspark.{layer}.attn_kv.weight",
|
| 260 |
+
f"{prefix}.attn.wkv.weight",
|
| 261 |
+
f"{prefix}.attn.wkv.scale",
|
| 262 |
+
512,
|
| 263 |
+
N_EMBD,
|
| 264 |
+
),
|
| 265 |
+
_fp8(
|
| 266 |
+
f"dspark.{layer}.attn_q_a.weight",
|
| 267 |
+
f"{prefix}.attn.wq_a.weight",
|
| 268 |
+
f"{prefix}.attn.wq_a.scale",
|
| 269 |
+
1024,
|
| 270 |
+
N_EMBD,
|
| 271 |
+
),
|
| 272 |
+
_fp8(
|
| 273 |
+
f"dspark.{layer}.attn_q_b.weight",
|
| 274 |
+
f"{prefix}.attn.wq_b.weight",
|
| 275 |
+
f"{prefix}.attn.wq_b.scale",
|
| 276 |
+
32768,
|
| 277 |
+
1024,
|
| 278 |
+
),
|
| 279 |
+
_fp8(
|
| 280 |
+
f"dspark.{layer}.attn_output_a.weight",
|
| 281 |
+
f"{prefix}.attn.wo_a.weight",
|
| 282 |
+
f"{prefix}.attn.wo_a.scale",
|
| 283 |
+
8192,
|
| 284 |
+
N_EMBD,
|
| 285 |
+
),
|
| 286 |
+
_fp8(
|
| 287 |
+
f"dspark.{layer}.attn_output_b.weight",
|
| 288 |
+
f"{prefix}.attn.wo_b.weight",
|
| 289 |
+
f"{prefix}.attn.wo_b.scale",
|
| 290 |
+
N_EMBD,
|
| 291 |
+
8192,
|
| 292 |
+
),
|
| 293 |
+
_fp8(
|
| 294 |
+
f"dspark.{layer}.ffn_gate_shexp.weight",
|
| 295 |
+
f"{prefix}.ffn.shared_experts.w1.weight",
|
| 296 |
+
f"{prefix}.ffn.shared_experts.w1.scale",
|
| 297 |
+
2048,
|
| 298 |
+
N_EMBD,
|
| 299 |
+
),
|
| 300 |
+
_fp8(
|
| 301 |
+
f"dspark.{layer}.ffn_up_shexp.weight",
|
| 302 |
+
f"{prefix}.ffn.shared_experts.w3.weight",
|
| 303 |
+
f"{prefix}.ffn.shared_experts.w3.scale",
|
| 304 |
+
2048,
|
| 305 |
+
N_EMBD,
|
| 306 |
+
),
|
| 307 |
+
_fp8(
|
| 308 |
+
f"dspark.{layer}.ffn_down_shexp.weight",
|
| 309 |
+
f"{prefix}.ffn.shared_experts.w2.weight",
|
| 310 |
+
f"{prefix}.ffn.shared_experts.w2.scale",
|
| 311 |
+
N_EMBD,
|
| 312 |
+
2048,
|
| 313 |
+
),
|
| 314 |
+
_plain(
|
| 315 |
+
f"dspark.{layer}.ffn_gate_inp.weight",
|
| 316 |
+
f"{prefix}.ffn.gate.weight",
|
| 317 |
+
bf16=True,
|
| 318 |
+
dims=(N_EMBD, N_EXPERTS),
|
| 319 |
+
src_shape=(N_EXPERTS, N_EMBD),
|
| 320 |
+
),
|
| 321 |
+
_plain(
|
| 322 |
+
f"dspark.{layer}.exp_probs_b.bias",
|
| 323 |
+
f"{prefix}.ffn.gate.bias",
|
| 324 |
+
bf16=False,
|
| 325 |
+
dims=(N_EXPERTS,),
|
| 326 |
+
),
|
| 327 |
+
]
|
| 328 |
+
|
| 329 |
+
for expert_kind, output_suffix, rows, cols in (
|
| 330 |
+
("w1", "ffn_gate_exps", 2048, N_EMBD),
|
| 331 |
+
("w3", "ffn_up_exps", 2048, N_EMBD),
|
| 332 |
+
("w2", "ffn_down_exps", N_EMBD, 2048),
|
| 333 |
+
):
|
| 334 |
+
out.append(
|
| 335 |
+
TensorRecipe(
|
| 336 |
+
out_name=f"dspark.{layer}.{output_suffix}.weight",
|
| 337 |
+
kind=artifact_recipe.routed_expert_kind,
|
| 338 |
+
dims=(cols, rows, N_EXPERTS),
|
| 339 |
+
ggml_type=artifact_recipe.routed_expert_type,
|
| 340 |
+
rows=rows,
|
| 341 |
+
cols=cols,
|
| 342 |
+
name_prefix=prefix,
|
| 343 |
+
expert_kind=expert_kind,
|
| 344 |
+
)
|
| 345 |
+
)
|
| 346 |
+
|
| 347 |
+
for suffix in ("hc_attn_fn", "hc_ffn_fn"):
|
| 348 |
+
out.append(
|
| 349 |
+
TensorRecipe(
|
| 350 |
+
out_name=f"dspark.{layer}.{suffix}.weight",
|
| 351 |
+
kind=KIND_RELABEL_F32,
|
| 352 |
+
dims=(16384, 24),
|
| 353 |
+
ggml_type=GGML_F32,
|
| 354 |
+
src_name=f"{prefix}.{suffix}",
|
| 355 |
+
rows=24,
|
| 356 |
+
cols=16384,
|
| 357 |
+
)
|
| 358 |
+
)
|
| 359 |
+
for suffix in ("hc_attn_base", "hc_ffn_base"):
|
| 360 |
+
out.append(
|
| 361 |
+
_plain(
|
| 362 |
+
f"dspark.{layer}.{suffix}.weight",
|
| 363 |
+
f"{prefix}.{suffix}",
|
| 364 |
+
bf16=False,
|
| 365 |
+
dims=(24,),
|
| 366 |
+
)
|
| 367 |
+
)
|
| 368 |
+
for suffix in ("hc_attn_scale", "hc_ffn_scale"):
|
| 369 |
+
out.append(
|
| 370 |
+
_plain(
|
| 371 |
+
f"dspark.{layer}.{suffix}.weight",
|
| 372 |
+
f"{prefix}.{suffix}",
|
| 373 |
+
bf16=False,
|
| 374 |
+
dims=(3,),
|
| 375 |
+
)
|
| 376 |
+
)
|
| 377 |
+
return out
|
| 378 |
+
|
| 379 |
+
|
| 380 |
+
def _global_plan() -> list[TensorRecipe]:
|
| 381 |
+
head = f"mtp.{HEAD_LAYER}"
|
| 382 |
+
out = [
|
| 383 |
+
_fp8(
|
| 384 |
+
"dspark.main_proj.weight",
|
| 385 |
+
"mtp.0.main_proj.weight",
|
| 386 |
+
"mtp.0.main_proj.scale",
|
| 387 |
+
N_EMBD,
|
| 388 |
+
12288,
|
| 389 |
+
),
|
| 390 |
+
_plain(
|
| 391 |
+
"dspark.main_norm.weight",
|
| 392 |
+
"mtp.0.main_norm.weight",
|
| 393 |
+
bf16=True,
|
| 394 |
+
dims=(N_EMBD,),
|
| 395 |
+
),
|
| 396 |
+
_plain(
|
| 397 |
+
"dspark.norm.weight",
|
| 398 |
+
f"{head}.norm.weight",
|
| 399 |
+
bf16=True,
|
| 400 |
+
dims=(N_EMBD,),
|
| 401 |
+
),
|
| 402 |
+
]
|
| 403 |
+
for suffix in ("markov_w1", "markov_w2"):
|
| 404 |
+
out.append(
|
| 405 |
+
TensorRecipe(
|
| 406 |
+
out_name=f"dspark.{suffix}.weight",
|
| 407 |
+
kind=KIND_MARKOV_F16,
|
| 408 |
+
dims=(MARKOV_RANK, VOCAB_SIZE),
|
| 409 |
+
ggml_type=GGML_F16,
|
| 410 |
+
src_name=f"{head}.markov_head.{suffix}.weight",
|
| 411 |
+
)
|
| 412 |
+
)
|
| 413 |
+
out.extend(
|
| 414 |
+
[
|
| 415 |
+
TensorRecipe(
|
| 416 |
+
out_name="dspark.hc_head_fn.weight",
|
| 417 |
+
kind=KIND_RELABEL_F32,
|
| 418 |
+
dims=(16384, 4),
|
| 419 |
+
ggml_type=GGML_F32,
|
| 420 |
+
src_name=f"{head}.hc_head_fn",
|
| 421 |
+
rows=4,
|
| 422 |
+
cols=16384,
|
| 423 |
+
),
|
| 424 |
+
_plain(
|
| 425 |
+
"dspark.hc_head_base.weight",
|
| 426 |
+
f"{head}.hc_head_base",
|
| 427 |
+
bf16=False,
|
| 428 |
+
dims=(4,),
|
| 429 |
+
),
|
| 430 |
+
_plain(
|
| 431 |
+
"dspark.hc_head_scale.weight",
|
| 432 |
+
f"{head}.hc_head_scale",
|
| 433 |
+
bf16=False,
|
| 434 |
+
dims=(1,),
|
| 435 |
+
),
|
| 436 |
+
_plain(
|
| 437 |
+
"dspark.confidence_head.weight",
|
| 438 |
+
f"{head}.confidence_head.proj.weight",
|
| 439 |
+
bf16=True,
|
| 440 |
+
dims=(4352,),
|
| 441 |
+
src_shape=(1, 4352),
|
| 442 |
+
),
|
| 443 |
+
]
|
| 444 |
+
)
|
| 445 |
+
return out
|
| 446 |
+
|
| 447 |
+
|
| 448 |
+
def build_plan(
|
| 449 |
+
recipe: ArtifactRecipe | str | None = None,
|
| 450 |
+
) -> tuple[TensorRecipe, ...]:
|
| 451 |
+
artifact_recipe = resolve_recipe(recipe)
|
| 452 |
+
tensors: list[TensorRecipe] = []
|
| 453 |
+
for layer in range(LAYER_COUNT):
|
| 454 |
+
tensors.extend(_layer_plan(layer, artifact_recipe))
|
| 455 |
+
tensors.extend(_global_plan())
|
| 456 |
+
if len(tensors) != 81:
|
| 457 |
+
raise AssertionError(f"recipe contains {len(tensors)} tensors, expected 81")
|
| 458 |
+
if len({tensor.out_name for tensor in tensors}) != len(tensors):
|
| 459 |
+
raise AssertionError("recipe contains duplicate output tensor names")
|
| 460 |
+
tensor_bytes = sum(tensor.byte_len for tensor in tensors)
|
| 461 |
+
if tensor_bytes != artifact_recipe.expected_tensor_bytes:
|
| 462 |
+
raise AssertionError(
|
| 463 |
+
f"{artifact_recipe.name}: tensor payload is {tensor_bytes}, "
|
| 464 |
+
f"expected {artifact_recipe.expected_tensor_bytes}"
|
| 465 |
+
)
|
| 466 |
+
type_counts = tuple(
|
| 467 |
+
(ggml_type, sum(tensor.ggml_type == ggml_type for tensor in tensors))
|
| 468 |
+
for ggml_type, _expected in artifact_recipe.expected_type_counts
|
| 469 |
+
)
|
| 470 |
+
if type_counts != artifact_recipe.expected_type_counts:
|
| 471 |
+
raise AssertionError(
|
| 472 |
+
f"{artifact_recipe.name}: type inventory {type_counts} does not match "
|
| 473 |
+
f"{artifact_recipe.expected_type_counts}"
|
| 474 |
+
)
|
| 475 |
+
return tuple(tensors)
|
| 476 |
+
|
| 477 |
+
|
| 478 |
+
def source_expectations(
|
| 479 |
+
tensor: TensorRecipe,
|
| 480 |
+
) -> Iterator[tuple[str, str, tuple[int, ...]]]:
|
| 481 |
+
if tensor.kind == KIND_PLAIN_F32:
|
| 482 |
+
yield tensor.src_name, "BF16" if tensor.src_is_bf16 else "F32", tensor.src_shape
|
| 483 |
+
elif tensor.kind == KIND_RELABEL_F32:
|
| 484 |
+
yield tensor.src_name, "F32", (tensor.rows, tensor.cols)
|
| 485 |
+
elif tensor.kind == KIND_FP8_TO_Q8_0:
|
| 486 |
+
yield tensor.weight_name, "F8_E4M3", (tensor.rows, tensor.cols)
|
| 487 |
+
yield (
|
| 488 |
+
tensor.scale_name,
|
| 489 |
+
"F8_E8M0",
|
| 490 |
+
((tensor.rows + 127) // 128, (tensor.cols + 127) // 128),
|
| 491 |
+
)
|
| 492 |
+
elif tensor.kind in {KIND_FUSED_EXPERTS_MXFP4, KIND_FUSED_EXPERTS_Q2_K}:
|
| 493 |
+
for expert in range(N_EXPERTS):
|
| 494 |
+
prefix = f"{tensor.name_prefix}.ffn.experts.{expert}.{tensor.expert_kind}"
|
| 495 |
+
yield f"{prefix}.weight", "I8", (tensor.rows, tensor.cols // 2)
|
| 496 |
+
yield f"{prefix}.scale", "F8_E8M0", (tensor.rows, tensor.cols // 32)
|
| 497 |
+
elif tensor.kind == KIND_MARKOV_F16:
|
| 498 |
+
yield tensor.src_name, "BF16", (VOCAB_SIZE, MARKOV_RANK)
|
| 499 |
+
else:
|
| 500 |
+
raise ValueError(f"unknown recipe kind {tensor.kind}")
|
| 501 |
+
|
| 502 |
+
|
| 503 |
+
def source_paths(source_dir: Path) -> tuple[Path, ...]:
|
| 504 |
+
return tuple(source_dir / name for name in SOURCE_FILES)
|
| 505 |
+
|
| 506 |
+
|
| 507 |
+
GGUF_METADATA = (
|
| 508 |
+
("general.architecture", "string", ARCHITECTURE),
|
| 509 |
+
("general.name", "string", GENERAL_NAME),
|
| 510 |
+
("general.source.url", "string", f"https://huggingface.co/{SOURCE_REPOSITORY}"),
|
| 511 |
+
("general.source.revision", "string", SOURCE_REVISION),
|
| 512 |
+
("general.license", "string", "MIT"),
|
| 513 |
+
("dspark.recipe_version", "u32", RECIPE_VERSION),
|
| 514 |
+
("dspark.block_size", "u32", BLOCK_SIZE),
|
| 515 |
+
("dspark.markov_rank", "u32", MARKOV_RANK),
|
| 516 |
+
("dspark.noise_token_id", "u32", NOISE_TOKEN_ID),
|
| 517 |
+
("dspark.target_layer_ids", "array_i32", TARGET_LAYER_IDS),
|
| 518 |
+
("dspark.layer_count", "u32", LAYER_COUNT),
|
| 519 |
+
)
|
scripts/reproduce.py
ADDED
|
@@ -0,0 +1,227 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""Run the pinned conversion, included verification, and optional repeat build."""
|
| 3 |
+
|
| 4 |
+
from __future__ import annotations
|
| 5 |
+
|
| 6 |
+
import argparse
|
| 7 |
+
import hashlib
|
| 8 |
+
import json
|
| 9 |
+
import os
|
| 10 |
+
import platform
|
| 11 |
+
import sys
|
| 12 |
+
import tempfile
|
| 13 |
+
from pathlib import Path
|
| 14 |
+
|
| 15 |
+
import numpy as np
|
| 16 |
+
|
| 17 |
+
from convert import convert
|
| 18 |
+
from recipe import (
|
| 19 |
+
ARCHITECTURE,
|
| 20 |
+
DEFAULT_RECIPE,
|
| 21 |
+
RECIPES,
|
| 22 |
+
REPOSITORY_ROOT,
|
| 23 |
+
RECIPE_VERSION,
|
| 24 |
+
SOURCE_REPOSITORY,
|
| 25 |
+
SOURCE_REVISION,
|
| 26 |
+
ArtifactRecipe,
|
| 27 |
+
resolve_recipe,
|
| 28 |
+
)
|
| 29 |
+
from verify import verify, write_report
|
| 30 |
+
|
| 31 |
+
|
| 32 |
+
def _write_json(path: Path, value: object) -> None:
|
| 33 |
+
path.parent.mkdir(parents=True, exist_ok=True)
|
| 34 |
+
temporary = path.with_name(f".{path.name}.partial-{os.getpid()}")
|
| 35 |
+
with temporary.open("w", encoding="utf-8", newline="\n") as handle:
|
| 36 |
+
json.dump(value, handle, indent=2, sort_keys=True)
|
| 37 |
+
handle.write("\n")
|
| 38 |
+
handle.flush()
|
| 39 |
+
os.fsync(handle.fileno())
|
| 40 |
+
os.replace(temporary, path)
|
| 41 |
+
|
| 42 |
+
|
| 43 |
+
def _update_sha256sums(path: Path, digest: str, filename: str) -> None:
|
| 44 |
+
entries: dict[str, str] = {}
|
| 45 |
+
if path.exists():
|
| 46 |
+
with path.open(encoding="ascii") as handle:
|
| 47 |
+
for line_number, line in enumerate(handle, 1):
|
| 48 |
+
stripped = line.rstrip("\n")
|
| 49 |
+
if not stripped:
|
| 50 |
+
continue
|
| 51 |
+
existing_digest, separator, existing_filename = stripped.partition(" ")
|
| 52 |
+
if (
|
| 53 |
+
separator != " "
|
| 54 |
+
or len(existing_digest) != 64
|
| 55 |
+
or any(
|
| 56 |
+
character not in "0123456789abcdef"
|
| 57 |
+
for character in existing_digest
|
| 58 |
+
)
|
| 59 |
+
or not existing_filename
|
| 60 |
+
):
|
| 61 |
+
raise ValueError(
|
| 62 |
+
f"{path}:{line_number}: malformed SHA256SUMS entry"
|
| 63 |
+
)
|
| 64 |
+
entries[existing_filename] = existing_digest
|
| 65 |
+
entries[filename] = digest
|
| 66 |
+
temporary = path.with_name(f".{path.name}.partial-{os.getpid()}")
|
| 67 |
+
with temporary.open("w", encoding="ascii", newline="\n") as handle:
|
| 68 |
+
for entry_filename in sorted(entries):
|
| 69 |
+
handle.write(f"{entries[entry_filename]} {entry_filename}\n")
|
| 70 |
+
handle.flush()
|
| 71 |
+
os.fsync(handle.fileno())
|
| 72 |
+
os.replace(temporary, path)
|
| 73 |
+
|
| 74 |
+
|
| 75 |
+
def _build_input_hashes() -> dict[str, str]:
|
| 76 |
+
relative_paths = [
|
| 77 |
+
Path("requirements-linux-aarch64-py314.lock"),
|
| 78 |
+
Path("manifest/source.json"),
|
| 79 |
+
*(
|
| 80 |
+
Path("scripts") / name
|
| 81 |
+
for name in (
|
| 82 |
+
"convert.py",
|
| 83 |
+
"download_sources.py",
|
| 84 |
+
"formats.py",
|
| 85 |
+
"recipe.py",
|
| 86 |
+
"reproduce.py",
|
| 87 |
+
"verify.py",
|
| 88 |
+
)
|
| 89 |
+
),
|
| 90 |
+
]
|
| 91 |
+
hashes: dict[str, str] = {}
|
| 92 |
+
for relative_path in relative_paths:
|
| 93 |
+
path = REPOSITORY_ROOT / relative_path
|
| 94 |
+
digest = hashlib.sha256()
|
| 95 |
+
with path.open("rb") as handle:
|
| 96 |
+
while chunk := handle.read(1024 * 1024):
|
| 97 |
+
digest.update(chunk)
|
| 98 |
+
hashes[relative_path.as_posix()] = digest.hexdigest()
|
| 99 |
+
return hashes
|
| 100 |
+
|
| 101 |
+
|
| 102 |
+
def reproduce(
|
| 103 |
+
source_dir: Path,
|
| 104 |
+
output: Path,
|
| 105 |
+
manifest_dir: Path,
|
| 106 |
+
*,
|
| 107 |
+
force: bool,
|
| 108 |
+
repeat_check: bool,
|
| 109 |
+
recipe: ArtifactRecipe | str | None = None,
|
| 110 |
+
) -> None:
|
| 111 |
+
artifact_recipe = resolve_recipe(recipe)
|
| 112 |
+
output = output.resolve()
|
| 113 |
+
if output.exists() and not force:
|
| 114 |
+
raise FileExistsError(
|
| 115 |
+
f"output already exists: {output}; pass --force to replace it"
|
| 116 |
+
)
|
| 117 |
+
output.parent.mkdir(parents=True, exist_ok=True)
|
| 118 |
+
build_input_hashes = _build_input_hashes()
|
| 119 |
+
|
| 120 |
+
repeat_digest: str | None = None
|
| 121 |
+
with tempfile.TemporaryDirectory(
|
| 122 |
+
prefix=f".{output.name}.candidate-", dir=output.parent
|
| 123 |
+
) as candidate_directory:
|
| 124 |
+
candidate = Path(candidate_directory) / output.name
|
| 125 |
+
size, digest = convert(source_dir, candidate, recipe=artifact_recipe)
|
| 126 |
+
report = verify(source_dir, candidate, recipe=artifact_recipe)
|
| 127 |
+
artifact = report["artifact"]
|
| 128 |
+
if artifact["size"] != size or artifact["sha256"] != digest:
|
| 129 |
+
raise ValueError("converter and verifier disagree on artifact identity")
|
| 130 |
+
|
| 131 |
+
if repeat_check:
|
| 132 |
+
with tempfile.TemporaryDirectory(
|
| 133 |
+
prefix=f".{output.name}.repeat-", dir=output.parent
|
| 134 |
+
) as repeat_directory:
|
| 135 |
+
repeat_output = Path(repeat_directory) / output.name
|
| 136 |
+
repeat_size, repeat_digest = convert(
|
| 137 |
+
source_dir, repeat_output, recipe=artifact_recipe
|
| 138 |
+
)
|
| 139 |
+
if repeat_size != size or repeat_digest != digest:
|
| 140 |
+
raise ValueError(
|
| 141 |
+
"repeat build is not byte-identical: "
|
| 142 |
+
f"first={size}/{digest}, second={repeat_size}/{repeat_digest}"
|
| 143 |
+
)
|
| 144 |
+
|
| 145 |
+
os.replace(candidate, output)
|
| 146 |
+
directory_fd = os.open(output.parent, os.O_RDONLY | os.O_DIRECTORY)
|
| 147 |
+
try:
|
| 148 |
+
os.fsync(directory_fd)
|
| 149 |
+
finally:
|
| 150 |
+
os.close(directory_fd)
|
| 151 |
+
|
| 152 |
+
manifest_dir = manifest_dir.resolve()
|
| 153 |
+
write_report(manifest_dir / artifact_recipe.manifest_filename("validation"), report)
|
| 154 |
+
build = {
|
| 155 |
+
"artifact": artifact,
|
| 156 |
+
"build_inputs": build_input_hashes,
|
| 157 |
+
"environment": {
|
| 158 |
+
"machine": platform.machine(),
|
| 159 |
+
"numpy": np.__version__,
|
| 160 |
+
"platform": platform.platform(),
|
| 161 |
+
"python": platform.python_version(),
|
| 162 |
+
},
|
| 163 |
+
"format": {
|
| 164 |
+
"architecture": ARCHITECTURE,
|
| 165 |
+
"gguf_version": 3,
|
| 166 |
+
"recipe": artifact_recipe.name,
|
| 167 |
+
"recipe_version": RECIPE_VERSION,
|
| 168 |
+
},
|
| 169 |
+
"repeat_build": {
|
| 170 |
+
"performed": repeat_check,
|
| 171 |
+
"sha256": repeat_digest,
|
| 172 |
+
"status": "byte-identical" if repeat_check else "not-run",
|
| 173 |
+
},
|
| 174 |
+
"source": {
|
| 175 |
+
"repository": SOURCE_REPOSITORY,
|
| 176 |
+
"revision": SOURCE_REVISION,
|
| 177 |
+
},
|
| 178 |
+
}
|
| 179 |
+
_write_json(manifest_dir / artifact_recipe.manifest_filename("build"), build)
|
| 180 |
+
_update_sha256sums(output.parent / "SHA256SUMS", digest, output.name)
|
| 181 |
+
print(f"[reproduce] PASS: {output.name} {size:,} bytes {digest}", flush=True)
|
| 182 |
+
|
| 183 |
+
|
| 184 |
+
def parse_args() -> argparse.Namespace:
|
| 185 |
+
parser = argparse.ArgumentParser(description=__doc__)
|
| 186 |
+
parser.add_argument("--sources", type=Path, required=True)
|
| 187 |
+
parser.add_argument(
|
| 188 |
+
"--recipe",
|
| 189 |
+
choices=sorted(RECIPES),
|
| 190 |
+
default=DEFAULT_RECIPE.name,
|
| 191 |
+
help=f"Artifact recipe (default: {DEFAULT_RECIPE.name})",
|
| 192 |
+
)
|
| 193 |
+
parser.add_argument(
|
| 194 |
+
"--output",
|
| 195 |
+
type=Path,
|
| 196 |
+
help="Output path (default: the selected recipe's canonical filename)",
|
| 197 |
+
)
|
| 198 |
+
parser.add_argument("--manifest-dir", type=Path, default=Path("manifest"))
|
| 199 |
+
parser.add_argument("--force", action="store_true")
|
| 200 |
+
parser.add_argument(
|
| 201 |
+
"--repeat-check",
|
| 202 |
+
action="store_true",
|
| 203 |
+
help="Build a second clean GGUF and require a byte-identical SHA-256",
|
| 204 |
+
)
|
| 205 |
+
return parser.parse_args()
|
| 206 |
+
|
| 207 |
+
|
| 208 |
+
def main() -> None:
|
| 209 |
+
args = parse_args()
|
| 210 |
+
recipe = resolve_recipe(args.recipe)
|
| 211 |
+
output = args.output if args.output is not None else Path(recipe.output_filename)
|
| 212 |
+
try:
|
| 213 |
+
reproduce(
|
| 214 |
+
args.sources,
|
| 215 |
+
output,
|
| 216 |
+
args.manifest_dir,
|
| 217 |
+
force=args.force,
|
| 218 |
+
repeat_check=args.repeat_check,
|
| 219 |
+
recipe=recipe,
|
| 220 |
+
)
|
| 221 |
+
except Exception as error:
|
| 222 |
+
print(f"[reproduce] ERROR: {error}", file=sys.stderr)
|
| 223 |
+
raise SystemExit(1) from error
|
| 224 |
+
|
| 225 |
+
|
| 226 |
+
if __name__ == "__main__":
|
| 227 |
+
main()
|
scripts/verify.py
ADDED
|
@@ -0,0 +1,613 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""Independently verify GGUF structure and every tensor against pinned sources."""
|
| 3 |
+
|
| 4 |
+
from __future__ import annotations
|
| 5 |
+
|
| 6 |
+
import argparse
|
| 7 |
+
import json
|
| 8 |
+
import math
|
| 9 |
+
import os
|
| 10 |
+
from pathlib import Path
|
| 11 |
+
|
| 12 |
+
import numpy as np
|
| 13 |
+
|
| 14 |
+
from formats import (
|
| 15 |
+
ALIGNMENT,
|
| 16 |
+
GgufReader,
|
| 17 |
+
SafeTensorSet,
|
| 18 |
+
align_up,
|
| 19 |
+
sha256_path,
|
| 20 |
+
verify_source_files,
|
| 21 |
+
)
|
| 22 |
+
from recipe import (
|
| 23 |
+
DEFAULT_RECIPE,
|
| 24 |
+
GGML_F16,
|
| 25 |
+
GGML_F32,
|
| 26 |
+
GGML_MXFP4,
|
| 27 |
+
GGML_Q2_K,
|
| 28 |
+
GGML_Q8_0,
|
| 29 |
+
GGML_TYPE_NAMES,
|
| 30 |
+
GGUF_METADATA,
|
| 31 |
+
KIND_PLAIN_F32,
|
| 32 |
+
N_EXPERTS,
|
| 33 |
+
RECIPES,
|
| 34 |
+
SOURCE_REPOSITORY,
|
| 35 |
+
SOURCE_REVISION,
|
| 36 |
+
ArtifactRecipe,
|
| 37 |
+
TensorRecipe,
|
| 38 |
+
build_plan,
|
| 39 |
+
resolve_recipe,
|
| 40 |
+
source_paths,
|
| 41 |
+
)
|
| 42 |
+
|
| 43 |
+
|
| 44 |
+
def _decode_e4m3_table() -> np.ndarray:
|
| 45 |
+
values = []
|
| 46 |
+
for byte in range(256):
|
| 47 |
+
sign = -1.0 if byte & 0x80 else 1.0
|
| 48 |
+
exponent = (byte >> 3) & 0x0F
|
| 49 |
+
mantissa = byte & 0x07
|
| 50 |
+
if exponent == 0x0F and mantissa == 0x07:
|
| 51 |
+
value = np.nan
|
| 52 |
+
elif exponent == 0:
|
| 53 |
+
value = sign * (mantissa / 8.0) * math.ldexp(1.0, -6)
|
| 54 |
+
else:
|
| 55 |
+
value = sign * (1.0 + mantissa / 8.0) * math.ldexp(1.0, exponent - 7)
|
| 56 |
+
values.append(np.float32(value))
|
| 57 |
+
return np.array(values, dtype=np.float32)
|
| 58 |
+
|
| 59 |
+
|
| 60 |
+
VERIFY_E4M3 = _decode_e4m3_table()
|
| 61 |
+
VERIFY_E8M0 = np.array(
|
| 62 |
+
[
|
| 63 |
+
np.float32(np.nan) if code == 0xFF else np.float32(math.ldexp(1.0, code - 127))
|
| 64 |
+
for code in range(256)
|
| 65 |
+
],
|
| 66 |
+
dtype=np.float32,
|
| 67 |
+
)
|
| 68 |
+
VERIFY_FP4 = np.array(
|
| 69 |
+
[
|
| 70 |
+
0.0,
|
| 71 |
+
0.5,
|
| 72 |
+
1.0,
|
| 73 |
+
1.5,
|
| 74 |
+
2.0,
|
| 75 |
+
3.0,
|
| 76 |
+
4.0,
|
| 77 |
+
6.0,
|
| 78 |
+
0.0,
|
| 79 |
+
-0.5,
|
| 80 |
+
-1.0,
|
| 81 |
+
-1.5,
|
| 82 |
+
-2.0,
|
| 83 |
+
-3.0,
|
| 84 |
+
-4.0,
|
| 85 |
+
-6.0,
|
| 86 |
+
],
|
| 87 |
+
dtype=np.float32,
|
| 88 |
+
)
|
| 89 |
+
# Independently decode and compare at most one routed expert at a time.
|
| 90 |
+
VERIFY_EXPERT_ROW_BATCH = 4096
|
| 91 |
+
|
| 92 |
+
|
| 93 |
+
def decode_q2_k_blocks(encoded: np.ndarray) -> np.ndarray:
|
| 94 |
+
"""Independently decode GGML Q2_K's 84-byte/256-value block layout."""
|
| 95 |
+
|
| 96 |
+
blocks = np.asarray(encoded, dtype=np.uint8)
|
| 97 |
+
if blocks.ndim != 2 or blocks.shape[1] != 84:
|
| 98 |
+
raise ValueError("Q2_K payload must have shape (n, 84)")
|
| 99 |
+
scale_bytes = blocks[:, :16]
|
| 100 |
+
d = blocks[:, 80:82].copy().reshape(-1).view("<f2").astype(np.float32)
|
| 101 |
+
dmin = blocks[:, 82:84].copy().reshape(-1).view("<f2").astype(np.float32)
|
| 102 |
+
if not np.isfinite(d).all() or not np.isfinite(dmin).all():
|
| 103 |
+
raise ValueError("Q2_K payload contains a non-finite block scale")
|
| 104 |
+
|
| 105 |
+
codes = np.empty((blocks.shape[0], 256), dtype=np.uint8)
|
| 106 |
+
for start in (0, 128):
|
| 107 |
+
offset = 16 + start // 4
|
| 108 |
+
packed = blocks[:, offset : offset + 32]
|
| 109 |
+
codes[:, start : start + 32] = packed & np.uint8(0x03)
|
| 110 |
+
codes[:, start + 32 : start + 64] = (packed >> np.uint8(2)) & np.uint8(0x03)
|
| 111 |
+
codes[:, start + 64 : start + 96] = (packed >> np.uint8(4)) & np.uint8(0x03)
|
| 112 |
+
codes[:, start + 96 : start + 128] = packed >> np.uint8(6)
|
| 113 |
+
|
| 114 |
+
group_scales = np.multiply(
|
| 115 |
+
d[:, None], scale_bytes & np.uint8(0x0F), dtype=np.float32
|
| 116 |
+
)
|
| 117 |
+
group_mins = np.multiply(
|
| 118 |
+
dmin[:, None], scale_bytes >> np.uint8(4), dtype=np.float32
|
| 119 |
+
)
|
| 120 |
+
decoded = np.subtract(
|
| 121 |
+
np.multiply(
|
| 122 |
+
group_scales[..., None],
|
| 123 |
+
codes.reshape(-1, 16, 16),
|
| 124 |
+
dtype=np.float32,
|
| 125 |
+
),
|
| 126 |
+
group_mins[..., None],
|
| 127 |
+
dtype=np.float32,
|
| 128 |
+
)
|
| 129 |
+
return decoded.reshape(-1, 256)
|
| 130 |
+
|
| 131 |
+
|
| 132 |
+
def _decode_source_mxfp4_rows(
|
| 133 |
+
packed: np.ndarray,
|
| 134 |
+
scale_codes: np.ndarray,
|
| 135 |
+
rows: int,
|
| 136 |
+
cols: int,
|
| 137 |
+
) -> np.ndarray:
|
| 138 |
+
if cols % 32:
|
| 139 |
+
raise ValueError("source MXFP4 column count must be divisible by 32")
|
| 140 |
+
block_count = cols // 32
|
| 141 |
+
source = np.asarray(packed, dtype=np.uint8).reshape(rows, block_count, 16)
|
| 142 |
+
scales = np.asarray(scale_codes, dtype=np.uint8).reshape(rows, block_count)
|
| 143 |
+
if np.any(scales == 0xFF):
|
| 144 |
+
raise ValueError("source MXFP4 contains reserved E8M0 NaN scale 0xff")
|
| 145 |
+
codes = np.empty((rows, block_count, 32), dtype=np.uint8)
|
| 146 |
+
codes[..., 0::2] = source & np.uint8(0x0F)
|
| 147 |
+
codes[..., 1::2] = source >> np.uint8(4)
|
| 148 |
+
decoded = VERIFY_FP4[codes]
|
| 149 |
+
decoded *= VERIFY_E8M0[scales][..., None]
|
| 150 |
+
return decoded.reshape(rows, cols)
|
| 151 |
+
|
| 152 |
+
|
| 153 |
+
def _expected_metadata() -> dict[str, object]:
|
| 154 |
+
expected: dict[str, object] = {}
|
| 155 |
+
for key, kind, value in GGUF_METADATA:
|
| 156 |
+
if kind == "array_i32":
|
| 157 |
+
expected[key] = tuple(int(item) for item in value)
|
| 158 |
+
elif kind == "u32":
|
| 159 |
+
expected[key] = int(value)
|
| 160 |
+
else:
|
| 161 |
+
expected[key] = str(value)
|
| 162 |
+
return expected
|
| 163 |
+
|
| 164 |
+
|
| 165 |
+
def _assert_equal(name: str, observed: np.ndarray, expected: np.ndarray) -> None:
|
| 166 |
+
if observed.shape != expected.shape:
|
| 167 |
+
raise ValueError(
|
| 168 |
+
f"{name}: expected shape {expected.shape}, found {observed.shape}"
|
| 169 |
+
)
|
| 170 |
+
if not np.array_equal(observed, expected):
|
| 171 |
+
mismatch = np.flatnonzero(observed.reshape(-1) != expected.reshape(-1))
|
| 172 |
+
first = int(mismatch[0]) if mismatch.size else -1
|
| 173 |
+
raise ValueError(f"{name}: exact comparison failed at element {first}")
|
| 174 |
+
|
| 175 |
+
|
| 176 |
+
def _verify_f32(
|
| 177 |
+
source: SafeTensorSet,
|
| 178 |
+
reader: GgufReader,
|
| 179 |
+
tensor: TensorRecipe,
|
| 180 |
+
) -> None:
|
| 181 |
+
output = reader.tensor_array(reader.tensor(tensor.out_name), "<u4")
|
| 182 |
+
if tensor.kind == KIND_PLAIN_F32 and tensor.src_is_bf16:
|
| 183 |
+
bits = source.array(tensor.src_name, "<u2").reshape(-1)
|
| 184 |
+
expected = bits.astype("<u4") << np.uint32(16)
|
| 185 |
+
else:
|
| 186 |
+
expected = source.array(tensor.src_name, "<u4").reshape(-1)
|
| 187 |
+
_assert_equal(tensor.out_name, output, expected)
|
| 188 |
+
|
| 189 |
+
|
| 190 |
+
def _verify_markov_f16(
|
| 191 |
+
source: SafeTensorSet,
|
| 192 |
+
reader: GgufReader,
|
| 193 |
+
tensor: TensorRecipe,
|
| 194 |
+
) -> None:
|
| 195 |
+
output = reader.tensor_array(reader.tensor(tensor.out_name), "<u2")
|
| 196 |
+
bits = source.array(tensor.src_name, "<u2").reshape(-1)
|
| 197 |
+
chunk = 8 * 1024 * 1024
|
| 198 |
+
for start in range(0, bits.size, chunk):
|
| 199 |
+
stop = min(start + chunk, bits.size)
|
| 200 |
+
f32_bits = bits[start:stop].astype("<u4") << np.uint32(16)
|
| 201 |
+
expected = f32_bits.view("<f4").astype("<f2").view("<u2")
|
| 202 |
+
_assert_equal(
|
| 203 |
+
f"{tensor.out_name}[{start}:{stop}]",
|
| 204 |
+
output[start:stop],
|
| 205 |
+
expected,
|
| 206 |
+
)
|
| 207 |
+
|
| 208 |
+
|
| 209 |
+
def _verify_q8_0(
|
| 210 |
+
source: SafeTensorSet,
|
| 211 |
+
reader: GgufReader,
|
| 212 |
+
tensor: TensorRecipe,
|
| 213 |
+
) -> float:
|
| 214 |
+
info = reader.tensor(tensor.out_name)
|
| 215 |
+
encoded = reader.tensor_array(info, np.uint8).reshape(-1, 34)
|
| 216 |
+
weights = source.array(tensor.weight_name, np.uint8).reshape(
|
| 217 |
+
tensor.rows, tensor.cols
|
| 218 |
+
)
|
| 219 |
+
scale_codes = source.array(tensor.scale_name, np.uint8).reshape(
|
| 220 |
+
(tensor.rows + 127) // 128,
|
| 221 |
+
(tensor.cols + 127) // 128,
|
| 222 |
+
)
|
| 223 |
+
max_relative_error = np.float32(0.0)
|
| 224 |
+
block_cursor = 0
|
| 225 |
+
for block_row in range(scale_codes.shape[0]):
|
| 226 |
+
row_start = block_row * 128
|
| 227 |
+
row_end = min(row_start + 128, tensor.rows)
|
| 228 |
+
reference = VERIFY_E4M3[weights[row_start:row_end]]
|
| 229 |
+
column_scales = np.repeat(VERIFY_E8M0[scale_codes[block_row]], 128)[
|
| 230 |
+
: tensor.cols
|
| 231 |
+
]
|
| 232 |
+
if not np.isfinite(reference).all() or not np.isfinite(column_scales).all():
|
| 233 |
+
raise ValueError(f"{tensor.out_name}: non-finite pinned FP8 source")
|
| 234 |
+
reference *= column_scales[None, :]
|
| 235 |
+
reference_blocks = reference.reshape(-1, 32)
|
| 236 |
+
block_count = reference_blocks.shape[0]
|
| 237 |
+
output_blocks = encoded[block_cursor : block_cursor + block_count]
|
| 238 |
+
maxima = np.max(np.abs(reference_blocks), axis=1).astype(np.float32, copy=False)
|
| 239 |
+
expected_scales = maxima / np.float32(127.0)
|
| 240 |
+
expected_scale_bits = expected_scales.astype("<f2").view("<u2")
|
| 241 |
+
observed_scale_bits = output_blocks[:, :2].copy().reshape(-1).view("<u2")
|
| 242 |
+
_assert_equal(
|
| 243 |
+
f"{tensor.out_name}/scale-bits-{block_cursor}",
|
| 244 |
+
observed_scale_bits,
|
| 245 |
+
expected_scale_bits,
|
| 246 |
+
)
|
| 247 |
+
inverse = np.zeros_like(expected_scales)
|
| 248 |
+
np.divide(
|
| 249 |
+
np.float32(1.0),
|
| 250 |
+
expected_scales,
|
| 251 |
+
out=inverse,
|
| 252 |
+
where=expected_scales != 0,
|
| 253 |
+
)
|
| 254 |
+
normalized = reference_blocks * inverse[:, None]
|
| 255 |
+
truncated = np.trunc(normalized).astype(np.float32, copy=False)
|
| 256 |
+
fraction = np.abs(normalized - truncated)
|
| 257 |
+
expected_codes = np.where(
|
| 258 |
+
fraction >= np.float32(0.5),
|
| 259 |
+
truncated + np.copysign(np.float32(1.0), normalized),
|
| 260 |
+
truncated,
|
| 261 |
+
).astype(np.int8)
|
| 262 |
+
observed_codes = output_blocks[:, 2:].view(np.int8)
|
| 263 |
+
_assert_equal(
|
| 264 |
+
f"{tensor.out_name}/codes-{block_cursor}",
|
| 265 |
+
observed_codes,
|
| 266 |
+
expected_codes,
|
| 267 |
+
)
|
| 268 |
+
stored_scale = (
|
| 269 |
+
output_blocks[:, :2].copy().reshape(-1).view("<f2").astype(np.float32)
|
| 270 |
+
)
|
| 271 |
+
if not np.isfinite(stored_scale).all():
|
| 272 |
+
raise ValueError(f"{tensor.out_name}: non-finite Q8_0 scale")
|
| 273 |
+
codes = output_blocks[:, 2:].view(np.int8).astype(np.float32)
|
| 274 |
+
decoded = stored_scale[:, None] * codes
|
| 275 |
+
denominator = np.maximum(
|
| 276 |
+
np.max(np.abs(reference_blocks), axis=1), np.float32(1e-6)
|
| 277 |
+
)
|
| 278 |
+
relative = np.abs(reference_blocks - decoded) / denominator[:, None]
|
| 279 |
+
if not np.isfinite(relative).all():
|
| 280 |
+
raise ValueError(f"{tensor.out_name}: non-finite Q8_0 error")
|
| 281 |
+
max_relative_error = np.maximum(max_relative_error, np.max(relative))
|
| 282 |
+
block_cursor += block_count
|
| 283 |
+
if block_cursor != encoded.shape[0]:
|
| 284 |
+
raise ValueError(f"{tensor.out_name}: Q8_0 block count mismatch")
|
| 285 |
+
result = float(max_relative_error)
|
| 286 |
+
if result > 0.01:
|
| 287 |
+
raise ValueError(
|
| 288 |
+
f"{tensor.out_name}: Q8_0 max relative error {result:.6f} > 0.01"
|
| 289 |
+
)
|
| 290 |
+
return result
|
| 291 |
+
|
| 292 |
+
|
| 293 |
+
def _expected_mxfp4_codes(packed: np.ndarray, rows: int, cols: int) -> np.ndarray:
|
| 294 |
+
blocks = cols // 32
|
| 295 |
+
packed_blocks = np.asarray(packed, dtype=np.uint8).reshape(rows, blocks, 16)
|
| 296 |
+
low = np.empty((rows, blocks, 16), dtype=np.uint8)
|
| 297 |
+
high = np.empty((rows, blocks, 16), dtype=np.uint8)
|
| 298 |
+
low[..., 0::2] = packed_blocks[..., :8] & np.uint8(0x0F)
|
| 299 |
+
low[..., 1::2] = packed_blocks[..., :8] >> np.uint8(4)
|
| 300 |
+
high[..., 0::2] = packed_blocks[..., 8:] & np.uint8(0x0F)
|
| 301 |
+
high[..., 1::2] = packed_blocks[..., 8:] >> np.uint8(4)
|
| 302 |
+
return low | (high << np.uint8(4))
|
| 303 |
+
|
| 304 |
+
|
| 305 |
+
def _verify_mxfp4(
|
| 306 |
+
source: SafeTensorSet,
|
| 307 |
+
reader: GgufReader,
|
| 308 |
+
tensor: TensorRecipe,
|
| 309 |
+
) -> None:
|
| 310 |
+
blocks = tensor.cols // 32
|
| 311 |
+
output = reader.tensor_array(reader.tensor(tensor.out_name), np.uint8).reshape(
|
| 312 |
+
N_EXPERTS, tensor.rows, blocks, 17
|
| 313 |
+
)
|
| 314 |
+
for expert in range(N_EXPERTS):
|
| 315 |
+
prefix = f"{tensor.name_prefix}.ffn.experts.{expert}.{tensor.expert_kind}"
|
| 316 |
+
packed = source.array(f"{prefix}.weight", np.uint8)
|
| 317 |
+
scales = source.array(f"{prefix}.scale", np.uint8).reshape(tensor.rows, blocks)
|
| 318 |
+
if np.any(scales == 0xFF):
|
| 319 |
+
raise ValueError(f"{prefix}.scale: contains reserved E8M0 NaN")
|
| 320 |
+
_assert_equal(
|
| 321 |
+
f"{tensor.out_name}/expert-{expert}/scale",
|
| 322 |
+
output[expert, ..., 0],
|
| 323 |
+
scales,
|
| 324 |
+
)
|
| 325 |
+
expected_codes = _expected_mxfp4_codes(packed, tensor.rows, tensor.cols)
|
| 326 |
+
_assert_equal(
|
| 327 |
+
f"{tensor.out_name}/expert-{expert}/codes",
|
| 328 |
+
output[expert, ..., 1:],
|
| 329 |
+
expected_codes,
|
| 330 |
+
)
|
| 331 |
+
|
| 332 |
+
|
| 333 |
+
def _verify_q2_k(
|
| 334 |
+
source: SafeTensorSet,
|
| 335 |
+
reader: GgufReader,
|
| 336 |
+
tensor: TensorRecipe,
|
| 337 |
+
error_limit: float,
|
| 338 |
+
) -> float:
|
| 339 |
+
blocks_per_row = tensor.cols // 256
|
| 340 |
+
output = reader.tensor_array(reader.tensor(tensor.out_name), np.uint8).reshape(
|
| 341 |
+
N_EXPERTS, tensor.rows, blocks_per_row, 84
|
| 342 |
+
)
|
| 343 |
+
max_relative_error = np.float32(0.0)
|
| 344 |
+
source_scale_blocks = tensor.cols // 32
|
| 345 |
+
for expert in range(N_EXPERTS):
|
| 346 |
+
prefix = f"{tensor.name_prefix}.ffn.experts.{expert}.{tensor.expert_kind}"
|
| 347 |
+
packed = source.array(f"{prefix}.weight", np.uint8).reshape(
|
| 348 |
+
tensor.rows, tensor.cols // 2
|
| 349 |
+
)
|
| 350 |
+
scales = source.array(f"{prefix}.scale", np.uint8).reshape(
|
| 351 |
+
tensor.rows, source_scale_blocks
|
| 352 |
+
)
|
| 353 |
+
for row_start in range(0, tensor.rows, VERIFY_EXPERT_ROW_BATCH):
|
| 354 |
+
row_end = min(row_start + VERIFY_EXPERT_ROW_BATCH, tensor.rows)
|
| 355 |
+
row_count = row_end - row_start
|
| 356 |
+
reference = _decode_source_mxfp4_rows(
|
| 357 |
+
packed[row_start:row_end],
|
| 358 |
+
scales[row_start:row_end],
|
| 359 |
+
row_count,
|
| 360 |
+
tensor.cols,
|
| 361 |
+
).reshape(-1, 256)
|
| 362 |
+
encoded = output[expert, row_start:row_end].reshape(-1, 84)
|
| 363 |
+
decoded = decode_q2_k_blocks(encoded)
|
| 364 |
+
if not np.isfinite(reference).all() or not np.isfinite(decoded).all():
|
| 365 |
+
raise ValueError(f"{tensor.out_name}: non-finite Q2_K comparison")
|
| 366 |
+
denominator = np.max(np.abs(reference), axis=1)
|
| 367 |
+
absolute_error = np.max(np.abs(reference - decoded), axis=1)
|
| 368 |
+
relative = np.zeros_like(absolute_error)
|
| 369 |
+
np.divide(
|
| 370 |
+
absolute_error,
|
| 371 |
+
denominator,
|
| 372 |
+
out=relative,
|
| 373 |
+
where=denominator != 0,
|
| 374 |
+
)
|
| 375 |
+
relative[np.logical_and(denominator == 0, absolute_error != 0)] = np.inf
|
| 376 |
+
max_relative_error = np.maximum(max_relative_error, np.max(relative))
|
| 377 |
+
result = float(max_relative_error)
|
| 378 |
+
if result > error_limit:
|
| 379 |
+
raise ValueError(
|
| 380 |
+
f"{tensor.out_name}: Q2_K max block-relative error "
|
| 381 |
+
f"{result:.6f} > {error_limit:.6f}"
|
| 382 |
+
)
|
| 383 |
+
return result
|
| 384 |
+
|
| 385 |
+
|
| 386 |
+
def _verify_zero_padding(reader: GgufReader) -> None:
|
| 387 |
+
header_padding = reader.bytes_at(reader.directory_end, reader.data_start)
|
| 388 |
+
if any(header_padding):
|
| 389 |
+
raise ValueError("non-zero GGUF padding before tensor data")
|
| 390 |
+
tensors = reader.tensors
|
| 391 |
+
for index, tensor in enumerate(tensors):
|
| 392 |
+
data_end = tensor.absolute_offset + tensor.byte_len
|
| 393 |
+
next_start = (
|
| 394 |
+
tensors[index + 1].absolute_offset
|
| 395 |
+
if index + 1 < len(tensors)
|
| 396 |
+
else reader.data_start + align_up(tensor.relative_offset + tensor.byte_len)
|
| 397 |
+
)
|
| 398 |
+
padding = reader.bytes_at(data_end, next_start)
|
| 399 |
+
if any(padding):
|
| 400 |
+
raise ValueError(f"non-zero GGUF alignment padding after {tensor.name}")
|
| 401 |
+
|
| 402 |
+
|
| 403 |
+
def verify(
|
| 404 |
+
source_dir: Path,
|
| 405 |
+
gguf_path: Path,
|
| 406 |
+
*,
|
| 407 |
+
recipe: ArtifactRecipe | str | None = None,
|
| 408 |
+
) -> dict[str, object]:
|
| 409 |
+
artifact_recipe = resolve_recipe(recipe)
|
| 410 |
+
source_dir = source_dir.resolve()
|
| 411 |
+
gguf_path = gguf_path.resolve()
|
| 412 |
+
print("[verify] validating pinned source sizes and SHA-256 values", flush=True)
|
| 413 |
+
sources = verify_source_files(source_dir)
|
| 414 |
+
plan = build_plan(artifact_recipe)
|
| 415 |
+
max_q8_error = 0.0
|
| 416 |
+
max_q2_k_error = 0.0
|
| 417 |
+
exact_f32 = 0
|
| 418 |
+
exact_f16 = 0
|
| 419 |
+
exact_mxfp4 = 0
|
| 420 |
+
q2_k_tensors = 0
|
| 421 |
+
q8_tensors = 0
|
| 422 |
+
with (
|
| 423 |
+
SafeTensorSet(source_paths(source_dir)) as source,
|
| 424 |
+
GgufReader(gguf_path) as reader,
|
| 425 |
+
):
|
| 426 |
+
source.validate_plan(plan)
|
| 427 |
+
expected_metadata = _expected_metadata()
|
| 428 |
+
if reader.metadata != expected_metadata:
|
| 429 |
+
raise ValueError(
|
| 430 |
+
f"GGUF metadata mismatch: expected {expected_metadata}, found {reader.metadata}"
|
| 431 |
+
)
|
| 432 |
+
observed_names = tuple(tensor.name for tensor in reader.tensors)
|
| 433 |
+
expected_names = tuple(tensor.out_name for tensor in plan)
|
| 434 |
+
if observed_names != expected_names:
|
| 435 |
+
raise ValueError("GGUF tensor order or names do not match the recipe")
|
| 436 |
+
expected_relative_offset = 0
|
| 437 |
+
for recipe_tensor, gguf_tensor in zip(plan, reader.tensors, strict=True):
|
| 438 |
+
if gguf_tensor.relative_offset != expected_relative_offset:
|
| 439 |
+
raise ValueError(
|
| 440 |
+
f"{recipe_tensor.out_name}: expected relative offset "
|
| 441 |
+
f"{expected_relative_offset}, found {gguf_tensor.relative_offset}"
|
| 442 |
+
)
|
| 443 |
+
expected_relative_offset = align_up(
|
| 444 |
+
expected_relative_offset + recipe_tensor.byte_len
|
| 445 |
+
)
|
| 446 |
+
if gguf_tensor.dims != recipe_tensor.dims:
|
| 447 |
+
raise ValueError(
|
| 448 |
+
f"{recipe_tensor.out_name}: expected dims {recipe_tensor.dims}, "
|
| 449 |
+
f"found {gguf_tensor.dims}"
|
| 450 |
+
)
|
| 451 |
+
if gguf_tensor.ggml_type != recipe_tensor.ggml_type:
|
| 452 |
+
raise ValueError(
|
| 453 |
+
f"{recipe_tensor.out_name}: expected type {recipe_tensor.ggml_type}, "
|
| 454 |
+
f"found {gguf_tensor.ggml_type}"
|
| 455 |
+
)
|
| 456 |
+
print(f"[verify] {recipe_tensor.out_name}", flush=True)
|
| 457 |
+
if recipe_tensor.ggml_type == GGML_F32:
|
| 458 |
+
_verify_f32(source, reader, recipe_tensor)
|
| 459 |
+
exact_f32 += 1
|
| 460 |
+
elif recipe_tensor.ggml_type == GGML_F16:
|
| 461 |
+
_verify_markov_f16(source, reader, recipe_tensor)
|
| 462 |
+
exact_f16 += 1
|
| 463 |
+
elif recipe_tensor.ggml_type == GGML_Q8_0:
|
| 464 |
+
q8_tensors += 1
|
| 465 |
+
max_q8_error = max(
|
| 466 |
+
max_q8_error, _verify_q8_0(source, reader, recipe_tensor)
|
| 467 |
+
)
|
| 468 |
+
elif recipe_tensor.ggml_type == GGML_MXFP4:
|
| 469 |
+
_verify_mxfp4(source, reader, recipe_tensor)
|
| 470 |
+
exact_mxfp4 += 1
|
| 471 |
+
elif recipe_tensor.ggml_type == GGML_Q2_K:
|
| 472 |
+
if artifact_recipe.q2_k_error_limit is None:
|
| 473 |
+
raise ValueError(
|
| 474 |
+
f"{artifact_recipe.name}: Q2_K tensor has no error limit"
|
| 475 |
+
)
|
| 476 |
+
q2_k_tensors += 1
|
| 477 |
+
max_q2_k_error = max(
|
| 478 |
+
max_q2_k_error,
|
| 479 |
+
_verify_q2_k(
|
| 480 |
+
source,
|
| 481 |
+
reader,
|
| 482 |
+
recipe_tensor,
|
| 483 |
+
artifact_recipe.q2_k_error_limit,
|
| 484 |
+
),
|
| 485 |
+
)
|
| 486 |
+
else:
|
| 487 |
+
raise ValueError(f"unsupported GGML type {recipe_tensor.ggml_type}")
|
| 488 |
+
_verify_zero_padding(reader)
|
| 489 |
+
expected_file_size = reader.data_start + align_up(
|
| 490 |
+
plan[-1].byte_len + reader.tensors[-1].relative_offset
|
| 491 |
+
)
|
| 492 |
+
actual_file_size = gguf_path.stat().st_size
|
| 493 |
+
if expected_file_size != artifact_recipe.expected_file_size:
|
| 494 |
+
raise ValueError(
|
| 495 |
+
f"{artifact_recipe.name}: recipe computes file size "
|
| 496 |
+
f"{expected_file_size}, expected {artifact_recipe.expected_file_size}"
|
| 497 |
+
)
|
| 498 |
+
if actual_file_size != expected_file_size:
|
| 499 |
+
raise ValueError(
|
| 500 |
+
f"GGUF size mismatch: expected {expected_file_size}, found {actual_file_size}"
|
| 501 |
+
)
|
| 502 |
+
data_start = reader.data_start
|
| 503 |
+
|
| 504 |
+
all_type_counts = {
|
| 505 |
+
GGML_F32: exact_f32,
|
| 506 |
+
GGML_F16: exact_f16,
|
| 507 |
+
GGML_Q8_0: q8_tensors,
|
| 508 |
+
GGML_Q2_K: q2_k_tensors,
|
| 509 |
+
GGML_MXFP4: exact_mxfp4,
|
| 510 |
+
}
|
| 511 |
+
observed_type_counts = tuple(
|
| 512 |
+
(ggml_type, all_type_counts[ggml_type])
|
| 513 |
+
for ggml_type, _count in artifact_recipe.expected_type_counts
|
| 514 |
+
)
|
| 515 |
+
unexpected_types = {
|
| 516 |
+
ggml_type: count
|
| 517 |
+
for ggml_type, count in all_type_counts.items()
|
| 518 |
+
if count and ggml_type not in dict(artifact_recipe.expected_type_counts)
|
| 519 |
+
}
|
| 520 |
+
if observed_type_counts != artifact_recipe.expected_type_counts or unexpected_types:
|
| 521 |
+
raise ValueError(
|
| 522 |
+
f"unexpected tensor type inventory for {artifact_recipe.name}: "
|
| 523 |
+
f"observed={observed_type_counts}, unexpected={unexpected_types}, "
|
| 524 |
+
f"expected={artifact_recipe.expected_type_counts}"
|
| 525 |
+
)
|
| 526 |
+
|
| 527 |
+
type_report = {
|
| 528 |
+
GGML_TYPE_NAMES[ggml_type]: count
|
| 529 |
+
for ggml_type, count in artifact_recipe.expected_type_counts
|
| 530 |
+
}
|
| 531 |
+
validation: dict[str, object] = {
|
| 532 |
+
"f32_exact_tensors": exact_f32,
|
| 533 |
+
"f16_exact_tensors": exact_f16,
|
| 534 |
+
"q8_0_max_block_relative_error": max_q8_error,
|
| 535 |
+
"q8_0_limit": 0.01,
|
| 536 |
+
"status": "pass",
|
| 537 |
+
}
|
| 538 |
+
if exact_mxfp4:
|
| 539 |
+
validation["mxfp4_exact_tensors"] = exact_mxfp4
|
| 540 |
+
if q2_k_tensors:
|
| 541 |
+
validation.update(
|
| 542 |
+
{
|
| 543 |
+
"q2_k_tensors": q2_k_tensors,
|
| 544 |
+
"q2_k_max_block_relative_error": max_q2_k_error,
|
| 545 |
+
"q2_k_limit": artifact_recipe.q2_k_error_limit,
|
| 546 |
+
}
|
| 547 |
+
)
|
| 548 |
+
|
| 549 |
+
report: dict[str, object] = {
|
| 550 |
+
"artifact": {
|
| 551 |
+
"filename": gguf_path.name,
|
| 552 |
+
"size": gguf_path.stat().st_size,
|
| 553 |
+
"sha256": sha256_path(gguf_path),
|
| 554 |
+
},
|
| 555 |
+
"source": {
|
| 556 |
+
"repository": SOURCE_REPOSITORY,
|
| 557 |
+
"revision": SOURCE_REVISION,
|
| 558 |
+
"files": sources,
|
| 559 |
+
},
|
| 560 |
+
"gguf": {
|
| 561 |
+
"version": 3,
|
| 562 |
+
"data_start": data_start,
|
| 563 |
+
"alignment": ALIGNMENT,
|
| 564 |
+
"tensor_count": len(plan),
|
| 565 |
+
"types": type_report,
|
| 566 |
+
},
|
| 567 |
+
"recipe": artifact_recipe.name,
|
| 568 |
+
"validation": validation,
|
| 569 |
+
}
|
| 570 |
+
print(json.dumps(report, indent=2, sort_keys=True), flush=True)
|
| 571 |
+
return report
|
| 572 |
+
|
| 573 |
+
|
| 574 |
+
def write_report(path: Path, report: dict[str, object]) -> None:
|
| 575 |
+
path.parent.mkdir(parents=True, exist_ok=True)
|
| 576 |
+
temporary = path.with_name(f".{path.name}.partial-{os.getpid()}")
|
| 577 |
+
with temporary.open("w", encoding="utf-8", newline="\n") as handle:
|
| 578 |
+
json.dump(report, handle, indent=2, sort_keys=True)
|
| 579 |
+
handle.write("\n")
|
| 580 |
+
handle.flush()
|
| 581 |
+
os.fsync(handle.fileno())
|
| 582 |
+
os.replace(temporary, path)
|
| 583 |
+
|
| 584 |
+
|
| 585 |
+
def parse_args() -> argparse.Namespace:
|
| 586 |
+
parser = argparse.ArgumentParser(description=__doc__)
|
| 587 |
+
parser.add_argument("--sources", type=Path, required=True)
|
| 588 |
+
parser.add_argument(
|
| 589 |
+
"--recipe",
|
| 590 |
+
choices=sorted(RECIPES),
|
| 591 |
+
default=DEFAULT_RECIPE.name,
|
| 592 |
+
help=f"Artifact recipe (default: {DEFAULT_RECIPE.name})",
|
| 593 |
+
)
|
| 594 |
+
parser.add_argument(
|
| 595 |
+
"--gguf",
|
| 596 |
+
type=Path,
|
| 597 |
+
help="GGUF path (default: the selected recipe's canonical filename)",
|
| 598 |
+
)
|
| 599 |
+
parser.add_argument("--report", type=Path)
|
| 600 |
+
return parser.parse_args()
|
| 601 |
+
|
| 602 |
+
|
| 603 |
+
def main() -> None:
|
| 604 |
+
args = parse_args()
|
| 605 |
+
recipe = resolve_recipe(args.recipe)
|
| 606 |
+
gguf = args.gguf if args.gguf is not None else Path(recipe.output_filename)
|
| 607 |
+
report = verify(args.sources, gguf, recipe=recipe)
|
| 608 |
+
if args.report:
|
| 609 |
+
write_report(args.report, report)
|
| 610 |
+
|
| 611 |
+
|
| 612 |
+
if __name__ == "__main__":
|
| 613 |
+
main()
|
tests/test_recipe.py
ADDED
|
@@ -0,0 +1,306 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
# ruff: noqa: E402
|
| 4 |
+
|
| 5 |
+
import json
|
| 6 |
+
import io
|
| 7 |
+
import struct
|
| 8 |
+
import sys
|
| 9 |
+
import tempfile
|
| 10 |
+
import time
|
| 11 |
+
import unittest
|
| 12 |
+
from pathlib import Path
|
| 13 |
+
from unittest import mock
|
| 14 |
+
|
| 15 |
+
import numpy as np
|
| 16 |
+
|
| 17 |
+
ROOT = Path(__file__).resolve().parents[1]
|
| 18 |
+
sys.path.insert(0, str(ROOT / "scripts"))
|
| 19 |
+
|
| 20 |
+
import convert as convert_module
|
| 21 |
+
from convert import (
|
| 22 |
+
E4M3_TABLE,
|
| 23 |
+
TensorProducer,
|
| 24 |
+
decode_source_mxfp4,
|
| 25 |
+
quantize_q2_k,
|
| 26 |
+
quantize_q8_0,
|
| 27 |
+
repack_mxfp4,
|
| 28 |
+
round_away_from_zero,
|
| 29 |
+
)
|
| 30 |
+
from formats import GgufReader, SafeTensorSet, write_gguf
|
| 31 |
+
from recipe import (
|
| 32 |
+
ARCHITECTURE,
|
| 33 |
+
GGML_Q2_K,
|
| 34 |
+
GGML_F16,
|
| 35 |
+
GGML_F32,
|
| 36 |
+
GGML_MXFP4,
|
| 37 |
+
GGML_Q8_0,
|
| 38 |
+
GGUF_METADATA,
|
| 39 |
+
KIND_PLAIN_F32,
|
| 40 |
+
MXFP4_Q8_0_RECIPE,
|
| 41 |
+
Q2_K_Q8_0_RECIPE,
|
| 42 |
+
TensorRecipe,
|
| 43 |
+
build_plan,
|
| 44 |
+
source_expectations,
|
| 45 |
+
)
|
| 46 |
+
from reproduce import _update_sha256sums
|
| 47 |
+
from verify import decode_q2_k_blocks
|
| 48 |
+
|
| 49 |
+
|
| 50 |
+
class RecipeTests(unittest.TestCase):
|
| 51 |
+
def test_plan_inventory_and_source_coverage(self) -> None:
|
| 52 |
+
mxfp4_plan = build_plan(MXFP4_Q8_0_RECIPE)
|
| 53 |
+
q2_k_plan = build_plan(Q2_K_Q8_0_RECIPE)
|
| 54 |
+
self.assertEqual(len(mxfp4_plan), 81)
|
| 55 |
+
self.assertEqual(len(q2_k_plan), 81)
|
| 56 |
+
mxfp4_counts = {
|
| 57 |
+
kind: sum(tensor.ggml_type == kind for tensor in mxfp4_plan)
|
| 58 |
+
for kind in (GGML_F32, GGML_F16, GGML_Q8_0, GGML_MXFP4)
|
| 59 |
+
}
|
| 60 |
+
self.assertEqual(
|
| 61 |
+
mxfp4_counts,
|
| 62 |
+
{GGML_F32: 45, GGML_F16: 2, GGML_Q8_0: 25, GGML_MXFP4: 9},
|
| 63 |
+
)
|
| 64 |
+
q2_k_counts = {
|
| 65 |
+
kind: sum(tensor.ggml_type == kind for tensor in q2_k_plan)
|
| 66 |
+
for kind in (GGML_F32, GGML_F16, GGML_Q8_0, GGML_Q2_K)
|
| 67 |
+
}
|
| 68 |
+
self.assertEqual(
|
| 69 |
+
q2_k_counts,
|
| 70 |
+
{GGML_F32: 45, GGML_F16: 2, GGML_Q8_0: 25, GGML_Q2_K: 9},
|
| 71 |
+
)
|
| 72 |
+
self.assertEqual(sum(tensor.byte_len for tensor in mxfp4_plan), 10_897_104_284)
|
| 73 |
+
self.assertEqual(sum(tensor.byte_len for tensor in q2_k_plan), 6_971_235_740)
|
| 74 |
+
self.assertEqual(
|
| 75 |
+
tuple((tensor.out_name, tensor.dims) for tensor in q2_k_plan),
|
| 76 |
+
tuple((tensor.out_name, tensor.dims) for tensor in mxfp4_plan),
|
| 77 |
+
)
|
| 78 |
+
inputs = [
|
| 79 |
+
name
|
| 80 |
+
for tensor in mxfp4_plan
|
| 81 |
+
for name, _dtype, _shape in source_expectations(tensor)
|
| 82 |
+
]
|
| 83 |
+
self.assertEqual(len(inputs), 4_705)
|
| 84 |
+
self.assertEqual(len(set(inputs)), 4_705)
|
| 85 |
+
self.assertEqual(mxfp4_plan[0].out_name, "dspark.0.attn_sinks.weight")
|
| 86 |
+
self.assertEqual(mxfp4_plan[-1].out_name, "dspark.confidence_head.weight")
|
| 87 |
+
|
| 88 |
+
def test_0731_metadata_and_recipe_outputs_are_stable(self) -> None:
|
| 89 |
+
self.assertEqual(ARCHITECTURE, "deepseek_v4_flash_dspark_draft")
|
| 90 |
+
self.assertEqual(
|
| 91 |
+
GGUF_METADATA,
|
| 92 |
+
(
|
| 93 |
+
("general.architecture", "string", "deepseek_v4_flash_dspark_draft"),
|
| 94 |
+
("general.name", "string", "DeepSeek-V4-Flash-0731-DSpark-Drafter"),
|
| 95 |
+
(
|
| 96 |
+
"general.source.url",
|
| 97 |
+
"string",
|
| 98 |
+
"https://huggingface.co/deepseek-ai/DeepSeek-V4-Flash-0731",
|
| 99 |
+
),
|
| 100 |
+
(
|
| 101 |
+
"general.source.revision",
|
| 102 |
+
"string",
|
| 103 |
+
"9e165c30e2704aec5d9d593cce3eebd58bbef1cb",
|
| 104 |
+
),
|
| 105 |
+
("general.license", "string", "MIT"),
|
| 106 |
+
("dspark.recipe_version", "u32", 1),
|
| 107 |
+
("dspark.block_size", "u32", 5),
|
| 108 |
+
("dspark.markov_rank", "u32", 256),
|
| 109 |
+
("dspark.noise_token_id", "u32", 128_799),
|
| 110 |
+
("dspark.target_layer_ids", "array_i32", (40, 41, 42)),
|
| 111 |
+
("dspark.layer_count", "u32", 3),
|
| 112 |
+
),
|
| 113 |
+
)
|
| 114 |
+
self.assertEqual(
|
| 115 |
+
MXFP4_Q8_0_RECIPE.output_filename,
|
| 116 |
+
"DeepSeek-V4-Flash-0731-DSpark-Drafter-MXFP4-Q8_0.gguf",
|
| 117 |
+
)
|
| 118 |
+
self.assertEqual(
|
| 119 |
+
Q2_K_Q8_0_RECIPE.output_filename,
|
| 120 |
+
"DeepSeek-V4-Flash-0731-DSpark-Drafter-Q2_K-Q8_0.gguf",
|
| 121 |
+
)
|
| 122 |
+
self.assertEqual(MXFP4_Q8_0_RECIPE.expected_file_size, 10_897_110_272)
|
| 123 |
+
self.assertEqual(Q2_K_Q8_0_RECIPE.expected_file_size, 6_971_241_728)
|
| 124 |
+
self.assertEqual(
|
| 125 |
+
Q2_K_Q8_0_RECIPE.expected_file_size
|
| 126 |
+
- Q2_K_Q8_0_RECIPE.expected_tensor_bytes,
|
| 127 |
+
MXFP4_Q8_0_RECIPE.expected_file_size
|
| 128 |
+
- MXFP4_Q8_0_RECIPE.expected_tensor_bytes,
|
| 129 |
+
)
|
| 130 |
+
self.assertEqual(MXFP4_Q8_0_RECIPE.manifest_filename("build"), "build.json")
|
| 131 |
+
self.assertEqual(
|
| 132 |
+
Q2_K_Q8_0_RECIPE.manifest_filename("build"),
|
| 133 |
+
"build-q2_k-q8_0.json",
|
| 134 |
+
)
|
| 135 |
+
|
| 136 |
+
def test_e4m3_known_values(self) -> None:
|
| 137 |
+
self.assertEqual(float(E4M3_TABLE[0x00]), 0.0)
|
| 138 |
+
self.assertEqual(float(E4M3_TABLE[0x01]), 2**-9)
|
| 139 |
+
self.assertEqual(float(E4M3_TABLE[0x38]), 1.0)
|
| 140 |
+
self.assertEqual(float(E4M3_TABLE[0x40]), 2.0)
|
| 141 |
+
self.assertEqual(float(E4M3_TABLE[0xB8]), -1.0)
|
| 142 |
+
self.assertTrue(np.isnan(E4M3_TABLE[0x7F]))
|
| 143 |
+
|
| 144 |
+
def test_q8_rounds_half_away_from_zero(self) -> None:
|
| 145 |
+
values = np.zeros(32, dtype=np.float32)
|
| 146 |
+
values[:5] = [127.0, 0.5, 1.5, -0.5, -1.5]
|
| 147 |
+
encoded = quantize_q8_0(values)
|
| 148 |
+
scale = encoded[0, :2].copy().view("<f2")[0]
|
| 149 |
+
codes = encoded[0, 2:].view(np.int8)
|
| 150 |
+
self.assertEqual(float(scale), 1.0)
|
| 151 |
+
np.testing.assert_array_equal(codes[:5], [127, 1, 2, -1, -2])
|
| 152 |
+
|
| 153 |
+
def test_q8_rounding_does_not_promote_values_below_half(self) -> None:
|
| 154 |
+
below_half = np.nextafter(np.float32(0.5), np.float32(0.0), dtype=np.float32)
|
| 155 |
+
values = np.array(
|
| 156 |
+
[below_half, -below_half, np.float32(0.5), np.float32(-0.5)],
|
| 157 |
+
dtype=np.float32,
|
| 158 |
+
)
|
| 159 |
+
np.testing.assert_array_equal(
|
| 160 |
+
round_away_from_zero(values),
|
| 161 |
+
np.array([0.0, -0.0, 1.0, -1.0], dtype=np.float32),
|
| 162 |
+
)
|
| 163 |
+
|
| 164 |
+
def test_mxfp4_low16_high16_layout_and_nan_rejection(self) -> None:
|
| 165 |
+
codes = np.arange(32, dtype=np.uint8) & np.uint8(0x0F)
|
| 166 |
+
packed = codes[0::2] | (codes[1::2] << np.uint8(4))
|
| 167 |
+
encoded = repack_mxfp4(packed, np.array([127], dtype=np.uint8), 1, 32)
|
| 168 |
+
self.assertEqual(int(encoded[0, 0, 0]), 127)
|
| 169 |
+
expected = codes[:16] | (codes[16:] << np.uint8(4))
|
| 170 |
+
np.testing.assert_array_equal(encoded[0, 0, 1:], expected)
|
| 171 |
+
with self.assertRaisesRegex(ValueError, "0xff"):
|
| 172 |
+
repack_mxfp4(packed, np.array([0xFF], dtype=np.uint8), 1, 32)
|
| 173 |
+
|
| 174 |
+
def test_q2_k_matches_ds4_reference_block_and_decodes_independently(self) -> None:
|
| 175 |
+
values = np.empty((1, 256), dtype=np.float32)
|
| 176 |
+
for index in range(256):
|
| 177 |
+
raw = ((index * 37) % 31) - 15
|
| 178 |
+
multiplier = 1 << ((index // 16) % 4)
|
| 179 |
+
values[0, index] = (
|
| 180 |
+
np.float32(0.0)
|
| 181 |
+
if index % 19 == 0
|
| 182 |
+
else np.float32(raw) * np.float32(multiplier) * np.float32(0.125)
|
| 183 |
+
)
|
| 184 |
+
expected = bytes.fromhex(
|
| 185 |
+
"123357ee124467fe124357ef124467ff"
|
| 186 |
+
"9194e9294e8294e4294e8294d4294e42"
|
| 187 |
+
"e4e9ba4e93a4e93a4e9b94e5394e53a4"
|
| 188 |
+
"4e8294e4694d8294e4294e4690d4294d9"
|
| 189 |
+
"3a4e53a4f5394a53a4f5394e5fa7b5fe038193c"
|
| 190 |
+
)
|
| 191 |
+
encoded = quantize_q2_k(values)
|
| 192 |
+
self.assertEqual(encoded.tobytes(), expected)
|
| 193 |
+
self.assertEqual(encoded[0, :16].tobytes(), expected[:16])
|
| 194 |
+
decoded = decode_q2_k_blocks(encoded)
|
| 195 |
+
relative_error = np.max(np.abs(values - decoded)) / np.max(np.abs(values))
|
| 196 |
+
self.assertLess(float(relative_error), Q2_K_Q8_0_RECIPE.q2_k_error_limit)
|
| 197 |
+
|
| 198 |
+
batched = quantize_q2_k(np.concatenate((values, -values), axis=0))
|
| 199 |
+
separate = np.concatenate(
|
| 200 |
+
(quantize_q2_k(values), quantize_q2_k(-values)), axis=0
|
| 201 |
+
)
|
| 202 |
+
np.testing.assert_array_equal(batched, separate)
|
| 203 |
+
|
| 204 |
+
def test_threaded_q2_experts_are_written_in_canonical_order(self) -> None:
|
| 205 |
+
rows = 1
|
| 206 |
+
cols = 256
|
| 207 |
+
|
| 208 |
+
def source_arrays(expert: int) -> tuple[np.ndarray, np.ndarray]:
|
| 209 |
+
codes = (np.arange(cols, dtype=np.uint16) + expert * 3).astype(np.uint8)
|
| 210 |
+
codes &= np.uint8(0x0F)
|
| 211 |
+
packed = codes[0::2] | (codes[1::2] << np.uint8(4))
|
| 212 |
+
scales = np.full(cols // 32, 127 + expert, dtype=np.uint8)
|
| 213 |
+
return packed, scales
|
| 214 |
+
|
| 215 |
+
class DelayedSource:
|
| 216 |
+
def array(self, name: str, _dtype: object) -> np.ndarray:
|
| 217 |
+
expert = int(name.split(".experts.", 1)[1].split(".", 1)[0])
|
| 218 |
+
packed, scales = source_arrays(expert)
|
| 219 |
+
if name.endswith(".weight"):
|
| 220 |
+
# Later experts finish first, exercising ordered futures.
|
| 221 |
+
time.sleep(0.01 * (3 - expert))
|
| 222 |
+
return packed
|
| 223 |
+
return scales
|
| 224 |
+
|
| 225 |
+
tensor = TensorRecipe(
|
| 226 |
+
out_name="dspark.test.ffn_gate_exps.weight",
|
| 227 |
+
kind=convert_module.KIND_FUSED_EXPERTS_Q2_K,
|
| 228 |
+
dims=(cols, rows, 4),
|
| 229 |
+
ggml_type=GGML_Q2_K,
|
| 230 |
+
rows=rows,
|
| 231 |
+
cols=cols,
|
| 232 |
+
name_prefix="mtp.blocks.40",
|
| 233 |
+
expert_kind="w1",
|
| 234 |
+
)
|
| 235 |
+
output = io.BytesIO()
|
| 236 |
+
with (
|
| 237 |
+
mock.patch.object(convert_module, "N_EXPERTS", 4),
|
| 238 |
+
mock.patch.object(convert_module, "Q2_K_EXPERT_WORKERS", 4),
|
| 239 |
+
):
|
| 240 |
+
written = TensorProducer(DelayedSource())._fused_experts_q2_k(
|
| 241 |
+
tensor, output
|
| 242 |
+
)
|
| 243 |
+
|
| 244 |
+
expected = bytearray()
|
| 245 |
+
for expert in range(4):
|
| 246 |
+
packed, scales = source_arrays(expert)
|
| 247 |
+
decoded = decode_source_mxfp4(packed, scales, rows, cols)
|
| 248 |
+
expected.extend(quantize_q2_k(decoded).tobytes(order="C"))
|
| 249 |
+
self.assertEqual(written, 4 * 84)
|
| 250 |
+
self.assertEqual(output.getvalue(), bytes(expected))
|
| 251 |
+
|
| 252 |
+
def test_sha256sums_update_preserves_other_recipe(self) -> None:
|
| 253 |
+
with tempfile.TemporaryDirectory() as directory:
|
| 254 |
+
path = Path(directory) / "SHA256SUMS"
|
| 255 |
+
legacy_digest = "1" * 64
|
| 256 |
+
q2_digest = "2" * 64
|
| 257 |
+
_update_sha256sums(path, legacy_digest, "legacy.gguf")
|
| 258 |
+
_update_sha256sums(path, q2_digest, "q2.gguf")
|
| 259 |
+
self.assertEqual(
|
| 260 |
+
path.read_text(encoding="ascii"),
|
| 261 |
+
f"{legacy_digest} legacy.gguf\n{q2_digest} q2.gguf\n",
|
| 262 |
+
)
|
| 263 |
+
|
| 264 |
+
def test_tiny_gguf_round_trip(self) -> None:
|
| 265 |
+
tensor = TensorRecipe(
|
| 266 |
+
out_name="dspark.test.weight",
|
| 267 |
+
kind=KIND_PLAIN_F32,
|
| 268 |
+
dims=(4,),
|
| 269 |
+
ggml_type=GGML_F32,
|
| 270 |
+
src_name="mtp.test",
|
| 271 |
+
src_shape=(4,),
|
| 272 |
+
)
|
| 273 |
+
data = struct.pack("<4f", 1.0, 2.0, 3.0, 4.0)
|
| 274 |
+
with tempfile.TemporaryDirectory() as directory:
|
| 275 |
+
path = Path(directory) / "tiny.gguf"
|
| 276 |
+
|
| 277 |
+
def produce(_tensor: TensorRecipe, handle: object) -> int:
|
| 278 |
+
return handle.write(data)
|
| 279 |
+
|
| 280 |
+
write_gguf(path, (tensor,), produce)
|
| 281 |
+
with GgufReader(path) as reader:
|
| 282 |
+
self.assertEqual(reader.tensors[0].name, tensor.out_name)
|
| 283 |
+
self.assertEqual(reader.tensors[0].dims, tensor.dims)
|
| 284 |
+
self.assertEqual(reader.tensors[0].ggml_type, GGML_F32)
|
| 285 |
+
observed = reader.tensor_array(reader.tensors[0], "<f4").copy()
|
| 286 |
+
np.testing.assert_array_equal(observed, [1.0, 2.0, 3.0, 4.0])
|
| 287 |
+
self.assertEqual(path.stat().st_size % 32, 0)
|
| 288 |
+
|
| 289 |
+
def test_safetensors_rejects_bad_offsets(self) -> None:
|
| 290 |
+
with tempfile.TemporaryDirectory() as directory:
|
| 291 |
+
path = Path(directory) / "bad.safetensors"
|
| 292 |
+
header = {
|
| 293 |
+
"mtp.bad": {
|
| 294 |
+
"dtype": "F32",
|
| 295 |
+
"shape": [1],
|
| 296 |
+
"data_offsets": [0, 8],
|
| 297 |
+
}
|
| 298 |
+
}
|
| 299 |
+
encoded = json.dumps(header, separators=(",", ":")).encode()
|
| 300 |
+
path.write_bytes(struct.pack("<Q", len(encoded)) + encoded + b"\0" * 4)
|
| 301 |
+
with self.assertRaisesRegex(ValueError, "overruns|needs"):
|
| 302 |
+
SafeTensorSet([path])
|
| 303 |
+
|
| 304 |
+
|
| 305 |
+
if __name__ == "__main__":
|
| 306 |
+
unittest.main()
|