Compare commits

..
8 Commits
22 changed files with 1148 additions and 754 deletions
+21
View File
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2026 vado
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+149 -19
View File
@@ -1,27 +1,157 @@
# 1b — Gensyn open-1b
# open-1b: an independent check, a local run, and a Go GGUF converter
Независимая верификация *auditable training* Gensyn **open-1b** и локальный запуск модели.
We checked Gensyn's open-1b and its claim of auditable training, then ran the
model on our own hardware. The audit uses Gensyn's official harness. The
conversion and tooling are Go.
- **Epic:** vado/1b#1
- **Отчёты:** [`reports/`](reports/)
- **Тела issue:** [`docs/issues/`](docs/issues/)
Links: [announcement](https://www.gensyn.ai/news/introducing-open-1b-auditable-training),
[harness](https://github.com/gensyn-ai/open-transformers),
[audit app](https://open1b.gensyn.ai/).
## Итоги (кратко)
## hf2gguf
| Задача | Статус |
|--------|--------|
| Аудит init-unit open-1b (CPU) | ✅ **MATCH** — `state_hash 16554a11…`; см. [`reports/audit-open1b-init.md`](reports/audit-open1b-init.md) |
| Аудит тренировочного интервала | ⛔ заблокирован (нет опубликованного interval-unit + RAM/диск) |
| Локальный inference на Arc B580 | ✅ **работает** — Go-конвертер, GGUF байт-идентичен; **~114 tok/s** decode, ~892 prefill, ~3.6 GiB VRAM; см. [`reports/open1b-local-inference.md`](reports/open1b-local-inference.md) |
`tools/hf2gguf` is the tool at the center of this repo. It converts a Hugging
Face open-1b checkpoint to GGUF and checks the result byte for byte, with no
Python, no torch, and no pip. For open-1b it reproduces llama.cpp's converter
output exactly, down to the same SHA-256.
## Ссылки
Why build it this way:
- Анонс: https://www.gensyn.ai/news/introducing-open-1b-auditable-training
- Harness: https://github.com/gensyn-ai/open-transformers
- Audit app: https://open1b.gensyn.ai/
| | hf2gguf | llama.cpp `convert_hf_to_gguf.py` |
| --- | --- | --- |
| Runtime | one Go binary | Python 3 with torch, numpy, transformers, gguf-py |
| Setup | `go build` | pip or uv, wheels, pinned versions |
| Startup | immediate | imports torch and its chain |
| Memory | streams one tensor at a time | loads tensors through torch |
| Output check | built-in byte-level `verify` | none |
| Output | byte-identical to a reference file | the reference |
| Scope | open-1b only | many architectures |
| Quantization | none, F16 only | many quant types |
## Ограничения (по состоянию на 2026-09)
It is deliberately narrow. It maps one Llama-derived architecture and copies the
GGUF metadata from a reference file, so it is not a general converter. Its value
is that it is small, readable, dependency-free, and able to prove the file it
writes is identical to the reference.
- `repop` (воспроизводимые ядра) — **не open source**, только wheel под «Gensyn Reproducibility License v1.0».
- v3-хэш **не покрывает** RNG, курсор data-stream, spike state и ключи `meta.json`.
- В self-serve audit-ките опубликованы **только init-units**; interval-units — «added per run once … published».
```bash
cd tools/hf2gguf
go build -o hf2gguf .
./hf2gguf convert -ref models/open-1b-sft-f16.gguf \
-safetensors models/open-1b-sft/model.safetensors \
-config models/open-1b-sft/config.json \
-tokenizer-config models/open-1b-sft/tokenizer_config.json \
-tokenizer models/open-1b-sft/tokenizer.json \
-o models/open-1b-sft-f16-go.gguf
./hf2gguf verify -ref models/open-1b-sft-f16.gguf -got models/open-1b-sft-f16-go.gguf
```
`verify` reports 39 of 39 metadata keys and 220 of 220 tensors identical. The two
files share SHA-256
`a18530417c732bb115a3501a7e1388ea7a54bd2a96c70be571d1249aa210fbae`.
See [tools/hf2gguf/README.md](tools/hf2gguf/README.md).
## Results
| Check | Result |
| --- | --- |
| Init-unit audit, x86-64 CPU, no NVIDIA | Match. The published state hash `16554a11...` is reproduced exactly. |
| Training-interval audit | Not run. No interval unit is published, and a replay needs about 24 GB RAM and 60 GB disk. |
| GGUF conversion (Go) | Byte-identical to the reference conversion, same SHA-256. |
| Local inference, one Intel Arc B580 | 114 tok/s decode, 892 tok/s prefill, about 3.6 GiB VRAM. |
Reports: [audit](docs/audit-open1b-init.md), [local run](docs/open1b-local-inference.md),
[benchmarks](docs/benchmarks.md), [inference stack](docs/inference-stack.md).
## How it works
Audit. Replay one published commitment and compare hashes.
```mermaid
flowchart LR
KIT["Gensyn audit kit"] --> SHA{"sha256<br/>against kit.json"}
SHA --> WHL["repop and pretrain wheels"]
WHL --> REPLAY["pretrain-audit-replay<br/>--from-init --device cpu"]
REPLAY --> H1["regenerated init hash"]
KIT --> H2["published commitment"]
H1 --> CMP{"equal"}
H2 --> CMP
CMP -->|yes| OK["match"]
CMP -->|no| BAD["mismatch"]
```
Inference. Convert in Go, serve on one Arc. The engine is a patched llama.cpp.
```mermaid
flowchart LR
HF["HF checkpoint<br/>model.safetensors, fp32"] --> GO["tools/hf2gguf<br/>Go"]
GO --> GGUF["GGUF, f16<br/>identical to reference"]
GGUF --> LC["llama.cpp Vulkan<br/>upstream plus open1b patch"]
LC --> GPU["Intel Arc B580"]
GPU --> API["llama-server HTTP"]
```
## Repository layout
```
tools/hf2gguf/ # the Go converter: dump, convert, verify
docs/
audit-open1b-init.md
open1b-local-inference.md
benchmarks.md
inference-stack.md
llama-cpp-open1b.patch
```
## The audit, and its limits
The init unit regenerates the published initial state hash exactly on a plain
CPU. The installed wheels match the kit manifest, and the repop build commit
matches the manifest.
It does not cover a training step. An init-only unit says nothing about the
forward pass, the backward pass, the optimizer, or the data stream, so the
per-step claim stays untested here. Three more reasons to be careful:
- repop ships as compiled wheels only, under a custom licence. The kernels that
make training reproducible cannot be read or checked.
- The v3 state hash omits the RNG, the data-stream cursor, spike state, and the
meta.json descriptor keys.
- No interval unit is published, so "replay any step" is not yet something an
outsider can do without permission.
A match shows that the pinned artifacts reproduce the published commitment on
our machine. It does not show how the original run was carried out.
## Running the model
Upstream llama.cpp has no open1b architecture, so the model needs a patched
build. Details in the [inference stack](docs/inference-stack.md).
```bash
llama-server -m models/open-1b-sft-f16-go.gguf \
--device Vulkan2 -ngl 99 -c 32768 -fa off --jinja \
--spec-type ngram-map-k,ngram-cache \
-t 16 --parallel 1 --alias open-1b --host 127.0.0.1 --port 8085
```
`-fa off` is required on these Arc/Vulkan builds, which keeps the KV cache in
f16. The requested 32k context is capped to the model's 4096.
## Caveats
- Chat template. With the vendor tail `...assistant<|end_header|>` followed by a
blank line, the released SFT checkpoint emits an end-of-turn token first and
returns an empty reply. Our GGUF is identical to the reference, so this is a
property of the released weights. Seed the assistant turn, set ignore_eos, or
adjust the trailing newline.
- Model quality. open-1b scores 25.4 on the OLMo2 suite, against 31.9 for
OLMo2-1B. It is a test of verifiability, not a strong model.
- Scope. We run and inspect released weights and replay one published
commitment. We do not train or fine-tune anything.
## License
MIT, see [LICENSE](LICENSE). The harness is Apache-2.0. repop, the checkpoints,
and the corpus are distributed by Gensyn under their own terms.
Built on Gensyn's release. Not affiliated with Gensyn.
+132
View File
@@ -0,0 +1,132 @@
# Audit: open-1b init unit on CPU
- Date: 2026-09-28
- Host: brainstation, Linux x86-64, kernel 7.3.0, no NVIDIA GPU
- Harness: `gensyn-ai/open-transformers` at `d7b7b674`
- Workdir: `audit/`
- Verdict: match on the `1b_repop_v2` init unit. No training interval was replayed.
## Kit
The audit app publishes the mapping at `https://open1b.gensyn.ai/manifest.json`:
```
run: 1b_repop_v2
initStateHash 16554a1119745f1b9bf4ac60b03226e5e9ca1247a2bd56169cd4c822fd457aed
audit: kit pt-f91110a0d0c8_rp-c9ca6e71e673
```
All four published kits declare the same init commitment, so the choice of kit
does not change the hash under test.
- pretrain commit: `f91110a0d0c84fa3bad808f365567d9c655d10b2`
- repop commit: `c9ca6e71e673660afc6c87de2592fa6f62c87b15`
- torch: `2.10.0`
## Toolchain
| Component | Version |
| --- | --- |
| Python | 3.11.16, via uv. System Python untouched. |
| uv | 0.12.17 |
| PyTorch | 2.10.0+cpu |
| repop | 0.1.5, build commit `c9ca6e71e673`, matches kit.json |
| pretrain | 0.1.0, kit wheel |
| Go | 1.27, for all glue and checks |
The runbook targets Linux with an NVIDIA GPU and points at the CUDA 12.9 index.
This host has no GPU. The README names CPU as the reference device, so the pinned
torch version was installed from the CPU index. Init is device-independent by
construction, so the result is unaffected.
## Commands
```bash
git clone --depth 1 https://github.com/gensyn-ai/open-transformers
go run ./tools/kitinspect kits/*/
go run ./tools/kitfetch kits/pt-f91110a0d0c8_rp-c9ca6e71e673 kit-pt-f91110a0d0c8_rp-c9ca6e71e673
uv venv --python 3.11 .venv
uv pip install --python .venv --index-url https://download.pytorch.org/whl/cpu "torch==2.10.0"
uv pip install --python .venv \
./kit-pt-f91110a0d0c8_rp-c9ca6e71e673/repop-0.1.5-cp311-cp311-linux_x86_64.whl \
./kit-pt-f91110a0d0c8_rp-c9ca6e71e673/pretrain-0.1.0-cp311-cp311-linux_x86_64.whl
.venv/bin/pretrain-audit-replay --from-init --until-step 0 \
--config-name 1b_repop_v2 --device cpu \
--expect-hash 16554a1119745f1b9bf4ac60b03226e5e9ca1247a2bd56169cd4c822fd457aed
```
## Checksums
All five kit files were hashed before install. The Go fetcher accepts a file only
on a full match and deletes rejected bytes. Expected values come from kit.json.
| File | SHA-256 | Result |
| --- | --- | --- |
| `pretrain-0.1.0-cp311-cp311-linux_x86_64.whl` | `ecc49d1c05c0e2876a59d98d877795769d6e9620eb66e4b4c866b16f695fb174` | match |
| `pretrain-0.1.0-cp311-cp311-macosx_14_0_arm64.whl` | `fbd23a54aa0bf87775e86493d67f759d1c1f4cc1dcd63ad991fba59dcb92491c` | match |
| `repop-0.1.5-cp311-cp311-linux_x86_64.whl` | `15f171643871211b2a772ee83c94833c0be2eef0495790cedc33813c95147ff0` | match |
| `repop-0.1.5-cp311-cp311-macosx_14_0_arm64.whl` | `d8658e0327c7278812ecb6dd455cd4cf8723a974a78788adba41d5f747289c69` | match |
| `trajectory.json` | `9435515c37d8956d0c81d5bc78c0a383bb6bf27c57b5dbaf1f8be564df0b324a` | match |
Only the two linux_x86_64 wheels were installed.
## Result
```json
{
"step": 0,
"state_hash": "16554a1119745f1b9bf4ac60b03226e5e9ca1247a2bd56169cd4c822fd457aed",
"mode": "init",
"device": "cpu",
"expected": "16554a1119745f1b9bf4ac60b03226e5e9ca1247a2bd56169cd4c822fd457aed",
"match": true
}
```
Log line: `from-init: regenerated init state_hash=16554a1119745f1b expected=16554a1119745f1b MATCH=True`.
Resources: 58.3 s wall, peak RSS 16.8 GB, no swap, exit 0.
## Limits
1. No training interval. An init-only unit says nothing about the forward pass,
the backward pass, the optimizer, or the data stream. The per-step claim is
not exercised.
2. repop is closed source. Only compiled wheels ship, under the Gensyn
Reproducibility Licence. The kernels that make training reproducible cannot be
read or checked. The init unit does not even run the kernels changed by the
recent repop repin.
3. Hash coverage. The v3 hash covers weights, optimizer state, gradients, and the
batch digest. It omits the RNG, the data-stream cursor, spike state, and the
meta.json descriptor keys.
4. Init is the easy case. init.py samples Philox on CPU and byte-copies, so it is
device-independent by design. The hard cross-device claim lives in the
training kernels, which this unit does not touch.
5. Provenance of the published run is not established. We only show that the
pinned artifacts reproduce the published commitment on this machine.
## Interval replay was not attempted
This is a decision, not a stall.
- Not defined by the kit. This kit's trajectory.json contains only three init
units (`100m_smoke_repop`, `1b_repop_run3`, `1b_repop_v2`). No interval unit
with a canonical target hash is published, so there is no commitment to check.
- Memory. A step replay needs about 24 GB. This host had about 11 GB free, and
the init unit already peaked at 16.8 GB.
- Disk. One checkpoint `ckpt/step_000000100/` is 19.31 GB; the full `ckpt/` tree
is 15.64 TB. With 39 GB free, a single checkpoint is tight.
Re-attempt on a host with 32 GB or more RAM and 60 GB or more disk, ideally with
a CUDA GPU.
## Files
- `open-transformers/`: harness clone
- `kits/`: manifests
- `kit-pt-f91110a0d0c8_rp-c9ca6e71e673/`: verified wheels
- `.venv/`: Python 3.11 audit environment
- `tools/`: Go glue (`kitinspect`, `trajinspect`, `kitfetch`, `gslist`)
- `init-replay.log`: full replay output
+41
View File
@@ -0,0 +1,41 @@
# Benchmarks
open-1b on our hardware, with the production model, gemma-4-E2B, as reference.
Single stream, one GPU.
## Setup
- GPU: one Intel Arc B580 (BMG G21, 12 GB), Vulkan, llama.cpp build-vulkan (b10893).
- CPU: AMD Ryzen 7 7700X, 29 GB RAM.
- Flags: `-ngl 99 -fa off --jinja --spec-type ngram-map-k,ngram-cache -t 16 --parallel 1`.
- `-fa off` is required on these Arc/Vulkan builds, so the KV cache stays in f16.
## open-1b, Go-built GGUF, f16
| Metric | Value |
| --- | --- |
| Decode | 112 to 117 tok/s |
| Prefill | about 892 tok/s |
| VRAM | about 3.6 GiB |
| Context | 4096, the model limit |
| Weights | f16, about 3.2 GB |
## Reference, one Arc B580
| Model | Weights | Context | Decode | Prefill |
| --- | --- | --- | --- | --- |
| open-1b-sft | f16, about 3.2 GB | 4096 | about 114 tok/s | about 892 tok/s |
| gemma-4-E2B | Q4, about 1.5 GiB | 64k | about 214 tok/s | about 1659 tok/s |
The gemma figures come from our 2026-09-20 benchmark on the same card and build.
gemma is about twice as fast, about 2.4 times smaller, and has sixteen times the
context. For classification, extraction, translation, or tool calls, gemma is the
better choice. open-1b is interesting for its training audit, not for its speed.
## Notes
- Not a controlled comparison. The open-1b numbers are a short first run with
different prompts; the gemma numbers come from a separate controlled benchmark.
- No speculative-decoder-off measurement for open-1b.
- Quality: open-1b scores 25.4 on the OLMo2 suite, against 31.9 for OLMo2-1B.
+89
View File
@@ -0,0 +1,89 @@
# Inference stack: how llama.cpp was built and launched
This records the engine behind the local run. Is the llama.cpp build stock? No.
## Provenance
| | |
| --- | --- |
| Upstream base | `ggerganov/llama.cpp` at `c32d1dabe`, 2026-09-10 |
| Local changes | uncommitted patches adding the `open1b` architecture, 9 files, +299 lines |
| Patch | [llama-cpp-open1b.patch](llama-cpp-open1b.patch) |
| Build | `-DGGML_VULKAN=ON -DGGML_NATIVE=ON -DCMAKE_BUILD_TYPE=Release` |
| Binary | `build-vulkan/bin/llama-server`, `0.4.0-dev`, build 10893 |
Upstream has no `open1b` architecture, so the patch is required. open-1b is
Llama-3-derived, with four differences the loader and graph must know about:
- gain-free per-head RMSNorm on Q and K before RoPE,
- RMSNorm on the token-embedding output,
- hybrid sliding-window attention, 512 tokens, full causal on the last layer of
each group of five and on the final layer,
- untied embedding and LM head, with interleaved-pair RoPE.
## Patch inventory
| File | Change |
| --- | --- |
| `src/models/open1b.cpp` | new: `llama_model_open1b`, hparams, tensors, graph |
| `src/models/models.h` | declare the model |
| `src/llama-arch.h`, `.cpp` | add `LLM_ARCH_OPEN1B` |
| `src/llama-model.cpp` | dispatch the arch, set the RoPE type |
| `gguf-py/gguf/constants.py` | arch enum, name, tensor list |
| `gguf-py/gguf/tensor_mapping.py` | HF to GGUF tensor names |
| `conversion/__init__.py` | register the converter |
| `conversion/open1b.py` | new: `Open1BModel` converter |
## Build
```bash
git clone https://github.com/ggerganov/llama.cpp
cd llama.cpp
git checkout c32d1dabe
git apply /path/to/llama-cpp-open1b.patch
cmake -B build-vulkan -DGGML_VULKAN=ON -DGGML_NATIVE=ON -DCMAKE_BUILD_TYPE=Release
cmake --build build-vulkan --config Release -j
```
## Launch
```bash
setsid --fork ./build-vulkan/bin/llama-server \
-m models/open-1b-sft-f16-go.gguf \
--device Vulkan2 -ngl 99 -c 32768 -fa off --jinja \
--spec-type ngram-map-k,ngram-cache \
-t 16 --parallel 1 --alias open-1b \
--host 127.0.0.1 --port 8085 >/tmp/open1b.log 2>&1 </dev/null &
```
| Flag | Reason |
| --- | --- |
| `--device Vulkan2` | one Intel Arc B580. Vulkan1 to 3 are the three Arc cards; Vulkan0 is the AMD iGPU. |
| `-ngl 99` | offload all layers |
| `-fa off` | Flash-Attention crashes on these Arc/Vulkan builds, so the KV cache stays in f16 |
| `-c 32768` | requested, capped to the model's 4096-token context |
| `--spec-type ngram-map-k,ngram-cache` | about 2.4x decode on small dense models |
| `--jinja` | use the model's chat template |
## Pipeline
```mermaid
flowchart LR
HF["HF checkpoint<br/>model.safetensors, fp32"] --> GO["tools/hf2gguf<br/>Go"]
GO --> GGUF["open-1b-sft-f16-go.gguf<br/>f16"]
GGUF --> LC["llama.cpp Vulkan<br/>upstream plus open1b patch"]
LC --> GPU["Intel Arc B580"]
GPU --> API["llama-server<br/>HTTP /completion"]
```
## Notes
- The patch is ours and uncommitted. The exported patch file makes the build
reproducible. Committing it, or proposing it upstream, would be better.
- The reference GGUF is not an independent artifact. It came from llama.cpp's
`convert_hf_to_gguf.py` with this same patch. The Go converter reproduces it
byte for byte, which is a real reimplementation of the GGUF writer, but the
comparison is against our own patched Python output, not a file published by
Gensyn.
- The patch was written for bring-up. It is not upstream-reviewed.
-26
View File
@@ -1,26 +0,0 @@
# Epic: Gensyn open-1b — верификация auditable training и локальный inference
**Цель.** Независимо проверить заявление Gensyn о *bitwise auditable training* (open-1b) на нашем железе и запустить модель локально; зафиксировать факты, ограничения и решения.
## Контекст
- Анонс 2026-09-15: https://www.gensyn.ai/news/introducing-open-1b-auditable-training (+ PRNewswire 302878850)
- Harness: https://github.com/gensyn-ai/open-transformers (Apache-2.0)
- Audit CLI: https://github.com/gensyn-ai/gensyn-audit
- Audit app: https://open1b.gensyn.ai/ (manifest: https://open1b.gensyn.ai/manifest.json)
- Модель: HF `Gensyn/open-1b-base`, `-midtrained-93B`, `-sft`
- Модель: 1.61B total / 1.08B non-emb, 24 слоя, 400B токенов, 80 957 шагов, 48×H100
- 2dph info-leaf (PO, 2026-09-28): `6703767319c6e2f9fa6d63465934d72e`
## Дети эпика
- `#AUDIT-INIT` — Аудит init-unit open-1b (CPU, Linux x86-64)
- `#AUDIT-INTERVAL` — Аудит тренировочного интервала open-1b (заблокировано)
- `#INFERENCE` — Локальный запуск open-1b на Arc B580 (llama.cpp Vulkan)
- `#REPORT` — Сводный отчёт и запись фактов в 2dph
## Критерий закрытия эпика
Все дочерние issues закрыты с доказательствами (тест/лог/хэш). Заблокированный
интервальный аудит может остаться открытым, если блокер явно задокументирован
(нет опубликованного interval-unit + ресурсы хоста).
-25
View File
@@ -1,25 +0,0 @@
Epic: #EPIC
## Цель
Независимо воспроизвести опубликованный **init-хэш** open-1b официальным harness'ом
на CPU (Linux x86-64, без NVIDIA).
## Критерии приёмки
- [x] kit `pt-f91110a0d0c8_rp-c9ca6e71e673` скачан; sha256 **всех** файлов сверены с `kit.json`
- [x] `repop.build_info().commit == kit.json.repop_commit` (`c9ca6e71e673`)
- [x] `pretrain-audit-replay --from-init --until-step 0 --config-name 1b_repop_v2 --device cpu` → `match=true`
- [x] отчёт с raw-выводом и таблицей sha256
## Результат (2026-09-28)
✅ **MATCH**. `state_hash = 16554a1119745f1b9bf4ac60b03226e5e9ca1247a2bd56169cd4c822fd457aed`,
wall 58.3 s, peak RSS 16.8 GB, exit 0.
Отчёт: `audit/REPORT.md`.
## Ограничение
Init-unit по README **не** проверяет тренировочный интервал — это отдельная задача
`#AUDIT-INTERVAL`.
-29
View File
@@ -1,29 +0,0 @@
Epic: #EPIC
## Цель
Воспроизвести **тренировочный интервал** (replay шага против опубликованного hash) —
именно это доказывает заявление «bitwise auditable training», в отличие от init-unit.
## Критерии приёмки
- [ ] получен interval-unit с canonical target hash (published commitment на шаг)
- [ ] скачан чекпоинт + шарды, потреблённые интервалом
- [ ] `pretrain-audit-replay --checkpoint … --until-step N --gcs-root … --expect-hash …` → `MATCH`
- [ ] отчёт с raw-выводом
## Блокер (2026-09-28)
- ⛔ В kit `pt-f91110a0d0c8_rp-c9ca6e71e673` `trajectory.json` содержит **только init-units**
(`100m_smoke_repop`, `1b_repop_run3`, `1b_repop_v2`). Interval-units «added per run once its
checkpoints and state_hash files are published» — **не опубликованы**. Self-serve кит
не определяет commit на шаг.
- ⛔ Ресурсы хоста: нужно ~24 ГБ RAM (есть ~11 ГБ) и ≥60 ГБ диска (есть 39 ГБ);
один чекпоинт `ckpt/step_000000100/` = 19.31 ГБ.
- Решение по разблокировке (владелец — PO/пользователь): запросить у audit-app interval-unit,
либо выполнять на хосте с ≥32 ГБ RAM / ≥60 ГБ диска (предпочтительно NVIDIA `--device cuda`).
## Примечание
Per-step log доступен на `gs://gensyn-open-1b/logs/state_hashes.jsonl`; harness не умеет
автоподстановку, а audit-app гейтит сабмиты — это тоже часть блокера.
-23
View File
@@ -1,23 +0,0 @@
Epic: #EPIC
## Цель
Поднять open-1b локально и доказать работоспособность на нашем железе
(Intel Arc B580, Vulkan; без NVIDIA/CUDA).
## Критерии приёмки
- [ ] llama.cpp собран с Vulkan; бэкенд активен на Arc B580
- [ ] веса HF (`Gensyn/open-1b-sft`, fallback `-base`) скачаны, provenance = точная ревизия
- [ ] конвертация в GGUF; собран/выбран квант
- [ ] `llama-cli`/`llama-server` даёт связный ответ; замер tok/s и VRAM
- [ ] отчёт с точными командами и caveat'ами
## Прочее
- Сервисы — только docker compose (не systemd).
- Обвязка — Go; Python только через `uv` (канонический `convert_hf_to_gguf.py`).
## Статус
🔄 выполняется фоновым SE-субагентом (2026-09-28).
-16
View File
@@ -1,16 +0,0 @@
Epic: #EPIC
## Цель
Свести результаты в отчёт и записать проверенные факты в 2dph (brain), пометив
что подтверждено, а что остаётся ограничением.
## Критерии приёмки
- [ ] сводный отчёт (что доказано / что нет) в репо
- [ ] info-leaf в 2dph с итогом (source: `po/gensyn`)
- [ ] явно зафиксированы ограничения: `repop` closed-source, v3-hash не покрывает RNG/cursor/spike/meta, interval-unit не опубликован
## Статус
info-leaf записан: `6703767319c6e2f9fa6d63465934d72e`. Отчёт — по завершении #INFERENCE и #AUDIT-INTERVAL.
+437
View File
@@ -0,0 +1,437 @@
diff --git a/conversion/__init__.py b/conversion/__init__.py
index 4d58bcd10..b869b4dc8 100644
--- a/conversion/__init__.py
+++ b/conversion/__init__.py
@@ -201,6 +201,7 @@ TEXT_MODEL_MAP: dict[str, str] = {
"OlmoeForCausalLM": "olmo",
"MuseGlimmerAssistantModel": "muse_glimmer",
"MuseGlimmerForConditionalGeneration": "muse_glimmer",
+ "Open1BForCausalLM": "open1b",
"OpenELMForCausalLM": "openelm",
"OrionForCausalLM": "orion",
"PLMForCausalLM": "plm",
diff --git a/conversion/open1b.py b/conversion/open1b.py
new file mode 100644
index 000000000..7ace1adc7
--- /dev/null
+++ b/conversion/open1b.py
@@ -0,0 +1,52 @@
+from __future__ import annotations
+
+from typing import Iterable, TYPE_CHECKING
+
+if TYPE_CHECKING:
+ from torch import Tensor
+
+from .base import ModelBase, TextModel, gguf, logger
+
+
+@ModelBase.register("Open1BForCausalLM")
+@ModelBase.example("Gensyn/open-1b-sft")
+class Open1BModel(TextModel):
+ model_arch = gguf.MODEL_ARCH.OPEN1B
+
+ def get_vocab_base_pre(self, tokenizer) -> str:
+ # open-1b's tokenizer.json declares a `<Split>` step whose pattern is
+ # serialized with the "String" variant (a literal `\d`, not a regex),
+ # so it is a no-op and the effective pre-tokenizer is plain byte-level
+ # BPE with the GPT-2 regex. That is exactly llama.cpp's "gpt-2" pre.
+ return "gpt-2"
+
+ def set_gguf_parameters(self):
+ super().set_gguf_parameters()
+
+ sliding_window = self.hparams["sliding_window"]
+ self.gguf_writer.add_sliding_window(sliding_window)
+ logger.info(f"gguf: sliding window = {sliding_window}")
+
+ # open-1b is full-causal on the last layer of every `swa_full_every`
+ # group and on the final layer; the runtime applies that exact pattern.
+ swa_full_every = self.hparams["swa_full_every"]
+ self.gguf_writer.add_sliding_window_pattern(swa_full_every)
+ logger.info(f"gguf: sliding window pattern period = {swa_full_every}")
+
+ def modify_tensors(self, data_torch: Tensor, name: str, bid: int | None) -> Iterable[tuple[str, Tensor]]:
+ # LSQ per-channel weight scales are not materialised: the released
+ # master weights are used directly, equivalent to quantized_forward=false.
+ if name.endswith(".weight_scale"):
+ return
+
+ # the checkpoint stores a fused gate+up projection; split it for ggml
+ if name.endswith("ffn.w_gate_up.weight"):
+ assert bid is not None
+ n_ff = data_torch.shape[0] // 2
+ gate = data_torch[:n_ff].contiguous()
+ up = data_torch[n_ff:].contiguous()
+ yield (self.format_tensor_name(gguf.MODEL_TENSOR.FFN_GATE, bid), gate)
+ yield (self.format_tensor_name(gguf.MODEL_TENSOR.FFN_UP, bid), up)
+ return
+
+ yield from super().modify_tensors(data_torch, name, bid)
diff --git a/gguf-py/gguf/constants.py b/gguf-py/gguf/constants.py
index d3a639f37..146d0cebe 100644
--- a/gguf-py/gguf/constants.py
+++ b/gguf-py/gguf/constants.py
@@ -630,6 +630,7 @@ class MODEL_ARCH(IntEnum):
NANBEIGE = auto()
QWEN3TTS = auto()
POCKETTTS = auto()
+ OPEN1B = auto()
class VISION_PROJECTOR_TYPE(IntEnum):
@@ -1385,6 +1386,7 @@ MODEL_ARCH_NAMES: dict[MODEL_ARCH, str] = {
MODEL_ARCH.NANBEIGE: "nanbeige",
MODEL_ARCH.QWEN3TTS: "qwen3tts",
MODEL_ARCH.POCKETTTS: "pockettts",
+ MODEL_ARCH.OPEN1B: "open1b",
}
VISION_PROJECTOR_TYPE_NAMES: dict[VISION_PROJECTOR_TYPE, str] = {
@@ -5528,6 +5530,21 @@ MODEL_TENSORS: dict[MODEL_ARCH, list[MODEL_TENSOR]] = {
MODEL_TENSOR.FFN_DOWN,
MODEL_TENSOR.FFN_UP,
],
+ MODEL_ARCH.OPEN1B: [
+ MODEL_TENSOR.TOKEN_EMBD,
+ MODEL_TENSOR.TOKEN_EMBD_NORM,
+ MODEL_TENSOR.OUTPUT_NORM,
+ MODEL_TENSOR.OUTPUT,
+ MODEL_TENSOR.ATTN_NORM,
+ MODEL_TENSOR.ATTN_Q,
+ MODEL_TENSOR.ATTN_K,
+ MODEL_TENSOR.ATTN_V,
+ MODEL_TENSOR.ATTN_OUT,
+ MODEL_TENSOR.FFN_NORM,
+ MODEL_TENSOR.FFN_GATE,
+ MODEL_TENSOR.FFN_UP,
+ MODEL_TENSOR.FFN_DOWN,
+ ],
}
# tensors that will not be serialized
diff --git a/gguf-py/gguf/tensor_mapping.py b/gguf-py/gguf/tensor_mapping.py
index d2dfeece5..ac5ba7c75 100644
--- a/gguf-py/gguf/tensor_mapping.py
+++ b/gguf-py/gguf/tensor_mapping.py
@@ -35,6 +35,7 @@ class TensorNameMap:
"model.transformer.wte", # llada
"embed_tokens", # qwen3-embedding
"model.embed", # talkie
+ "embedding.tok_embeddings", # open1b
),
# Masked embeddings
@@ -63,6 +64,7 @@ class TensorNameMap:
"model.layers.0.pre_norm", # rwkv7
"backbone.norm", # wavtokenizer
"model.embedding_norm", # lfm2
+ "emb_norm", # open1b
),
# Position embeddings
@@ -86,6 +88,7 @@ class TensorNameMap:
"model.lm_head", # dflash
"model.transformer.ff_out", # llada
"head.decoder", # modern-bert
+ "embedding.output", # open1b
),
MODEL_TENSOR.DENSE_2_OUT: (
"dense_2_out", # embeddinggemma
@@ -116,6 +119,7 @@ class TensorNameMap:
"model.transformer.ln_f", # llada
"final_norm", # modern-bert
"model.norm", # cogvlm
+ "norm_out", # open1b
),
# Rope frequencies
@@ -2761,6 +2765,29 @@ class TensorNameMap:
# architecture-specific block mappings
arch_block_mappings_cfg: dict[MODEL_ARCH, dict[MODEL_TENSOR, tuple[str, ...]]] = {
+ MODEL_ARCH.OPEN1B: {
+ MODEL_TENSOR.ATTN_NORM: (
+ "blocks.{bid}.norm1",
+ ),
+ MODEL_TENSOR.FFN_NORM: (
+ "blocks.{bid}.norm2",
+ ),
+ MODEL_TENSOR.ATTN_Q: (
+ "blocks.{bid}.attn.wq",
+ ),
+ MODEL_TENSOR.ATTN_K: (
+ "blocks.{bid}.attn.wk",
+ ),
+ MODEL_TENSOR.ATTN_V: (
+ "blocks.{bid}.attn.wv",
+ ),
+ MODEL_TENSOR.ATTN_OUT: (
+ "blocks.{bid}.attn.wo",
+ ),
+ MODEL_TENSOR.FFN_DOWN: (
+ "blocks.{bid}.ffn.w_down",
+ ),
+ },
MODEL_ARCH.ARCTIC: {
MODEL_TENSOR.FFN_NORM: (
"model.layers.{bid}.residual_layernorm",
diff --git a/src/llama-arch.cpp b/src/llama-arch.cpp
index b5efb7206..9951cf5ce 100644
--- a/src/llama-arch.cpp
+++ b/src/llama-arch.cpp
@@ -156,6 +156,7 @@ static const std::map<llm_arch, const char *> LLM_ARCH_NAMES = {
{ LLM_ARCH_NANBEIGE, "nanbeige" },
{ LLM_ARCH_QWEN3TTS, "qwen3tts" },
{ LLM_ARCH_POCKETTTS, "pockettts" },
+ { LLM_ARCH_OPEN1B, "open1b" },
{ LLM_ARCH_UNKNOWN, "(unknown)" },
};
diff --git a/src/llama-arch.h b/src/llama-arch.h
index f1d173a57..02a0bee15 100644
--- a/src/llama-arch.h
+++ b/src/llama-arch.h
@@ -161,6 +161,7 @@ enum llm_arch {
LLM_ARCH_QWEN3TTS,
LLM_ARCH_POCKETTTS,
LLM_ARCH_MINIMAX_01,
+ LLM_ARCH_OPEN1B,
LLM_ARCH_UNKNOWN,
};
diff --git a/src/llama-model.cpp b/src/llama-model.cpp
index d10b60afd..b12c43d46 100644
--- a/src/llama-model.cpp
+++ b/src/llama-model.cpp
@@ -108,6 +108,8 @@ static llama_model * llama_model_mapping(llm_arch arch, const llama_model_params
return new llama_model_qwen2vl(params);
case LLM_ARCH_QWEN2MOE:
return new llama_model_qwen2moe(params);
+ case LLM_ARCH_OPEN1B:
+ return new llama_model_open1b(params);
case LLM_ARCH_QWEN3:
return new llama_model_qwen3(params);
case LLM_ARCH_QWEN3MOE:
@@ -2943,6 +2945,8 @@ llama_rope_type llama_model_rope_type(const llama_model * model) {
case LLM_ARCH_DOTS3NOTE:
case LLM_ARCH_NANBEIGE:
case LLM_ARCH_POCKETTTS:
+ // open-1b pairs consecutive head values (interleaved-pair RoPE)
+ case LLM_ARCH_OPEN1B:
// HY_V4 rotates consecutive pairs, matching the reference implementation
case LLM_ARCH_HY_V4:
return LLAMA_ROPE_TYPE_NORM;
diff --git a/src/models/models.h b/src/models/models.h
index 87195fddd..d2e30faab 100644
--- a/src/models/models.h
+++ b/src/models/models.h
@@ -583,6 +583,20 @@ struct llama_model_qwen3 : public llama_model_base {
};
+struct llama_model_open1b : public llama_model_base {
+ llama_model_open1b(const struct llama_model_params & params) : llama_model_base(params) {}
+ void load_arch_hparams(llama_model_loader & ml) override;
+ void load_arch_tensors(llama_model_loader & ml) override;
+
+ template <bool iswa>
+ struct graph : public llm_graph_context {
+ graph(const llama_model & model, const llm_graph_params & params);
+ };
+
+ std::unique_ptr<llm_graph_context> build_arch_graph(const llm_graph_params & params) const override;
+};
+
+
struct llama_model_qwen3moe : public llama_model_base {
llama_model_qwen3moe(const struct llama_model_params & params) : llama_model_base(params) {}
void load_arch_hparams(llama_model_loader & ml) override;
diff --git a/src/models/open1b.cpp b/src/models/open1b.cpp
new file mode 100644
index 000000000..9d8489ffd
--- /dev/null
+++ b/src/models/open1b.cpp
@@ -0,0 +1,182 @@
+#include "models.h"
+
+// Gensyn open-1b: a 1.6B decoder-only LM, Llama-3-derived with these deltas:
+// - gain-free per-head RMSNorm on Q and K before RoPE
+// - RMSNorm on the token-embedding output
+// - hybrid sliding-window attention (last layer of each group + final layer full)
+// - untied input embedding and LM head
+// - interleaved-pair (GGML_ROPE_TYPE_NORM) RoPE
+// All attention/FFN linears were trained with int8 W8A8 LSQ QAT; the released
+// master weights are used directly here (equivalent to quantized_forward=false).
+
+void llama_model_open1b::load_arch_hparams(llama_model_loader & ml) {
+ ml.get_key(LLM_KV_ATTENTION_LAYERNORM_RMS_EPS, hparams.f_norm_rms_eps);
+
+ // hybrid sliding-window attention: the last layer of each group of
+ // `sliding_window_pattern` layers is full-causal, the rest slide; the
+ // final layer is always full-causal.
+ const bool found_swa = ml.get_key(LLM_KV_ATTENTION_SLIDING_WINDOW, hparams.n_swa, false);
+ if (found_swa && hparams.n_swa > 0) {
+ hparams.swa_type = LLAMA_SWA_TYPE_STANDARD;
+
+ uint32_t swa_period = 5;
+ ml.get_key_or_arr(LLM_KV_ATTENTION_SLIDING_WINDOW_PATTERN, swa_period, false);
+ hparams.set_swa_pattern(swa_period);
+ // open-1b forces the final layer to full causal attention
+ hparams.is_swa_impl[hparams.n_layer() - 1] = 0;
+ } else {
+ hparams.swa_type = LLAMA_SWA_TYPE_NONE;
+ }
+
+ type = LLM_TYPE_1B;
+}
+
+void llama_model_open1b::load_arch_tensors(llama_model_loader &) {
+ LLAMA_LOAD_LOCALS;
+
+ tok_embd = create_tensor(tn(LLM_TENSOR_TOKEN_EMBD, "weight"), {n_embd, n_vocab}, 0);
+ tok_norm = create_tensor(tn(LLM_TENSOR_TOKEN_EMBD_NORM, "weight", 0), {n_embd}, 0);
+
+ // output
+ output_norm = create_tensor(tn(LLM_TENSOR_OUTPUT_NORM, "weight"), {n_embd}, 0);
+ output = create_tensor(tn(LLM_TENSOR_OUTPUT, "weight"), {n_embd, n_vocab}, TENSOR_NOT_REQUIRED);
+ // if output is NULL, init from the input tok embed
+ if (output == NULL) {
+ output = create_tensor(tn(LLM_TENSOR_TOKEN_EMBD, "weight"), {n_embd, n_vocab}, TENSOR_DUPLICATED);
+ }
+
+ for (int i = 0; i < n_layer; ++i) {
+ auto & layer = layers[i];
+
+ layer.attn_norm = create_tensor(tn(LLM_TENSOR_ATTN_NORM, "weight", i), {n_embd}, 0);
+
+ create_tensor_qkv(layer, i, n_embd, n_embd_head_k * n_head, n_embd_gqa, n_embd_gqa, 0);
+ layer.wo = create_tensor(tn(LLM_TENSOR_ATTN_OUT, "weight", i), {n_embd_head_k * n_head, n_embd}, 0);
+
+ layer.ffn_norm = create_tensor(tn(LLM_TENSOR_FFN_NORM, "weight", i), {n_embd}, 0);
+ layer.ffn_gate = create_tensor(tn(LLM_TENSOR_FFN_GATE, "weight", i), {n_embd, n_ff}, 0);
+ layer.ffn_up = create_tensor(tn(LLM_TENSOR_FFN_UP, "weight", i), {n_embd, n_ff}, 0);
+ layer.ffn_down = create_tensor(tn(LLM_TENSOR_FFN_DOWN, "weight", i), {n_ff, n_embd}, 0);
+ }
+}
+
+std::unique_ptr<llm_graph_context> llama_model_open1b::build_arch_graph(const llm_graph_params & params) const {
+ if (hparams.swa_type != LLAMA_SWA_TYPE_NONE) {
+ return std::make_unique<graph<true>>(*this, params);
+ }
+ return std::make_unique<graph<false>>(*this, params);
+}
+
+template <bool iswa>
+llama_model_open1b::graph<iswa>::graph(const llama_model & model, const llm_graph_params & params) : llm_graph_context(params) {
+ const int64_t n_embd_head = hparams.n_embd_head_k();
+
+ GGML_ASSERT(n_embd_head == hparams.n_embd_head_v());
+ GGML_ASSERT(n_embd_head == n_rot);
+
+ ggml_tensor * cur;
+ ggml_tensor * inpL;
+
+ inpL = build_inp_embd(model.tok_embd);
+ inpL = build_norm(inpL, model.tok_norm, NULL, LLM_NORM_RMS, -1);
+ cb(inpL, "inp_norm", -1);
+
+ // inp_pos - contains the positions
+ ggml_tensor * inp_pos = build_inp_pos();
+
+ using inp_attn_type = std::conditional_t<iswa, llm_graph_input_attn_kv_iswa, llm_graph_input_attn_kv>;
+ inp_attn_type * inp_attn = nullptr;
+ if constexpr (iswa) {
+ inp_attn = build_attn_inp_kv_iswa();
+ } else {
+ inp_attn = build_attn_inp_kv();
+ }
+
+ ggml_tensor * inp_out_ids = build_inp_out_ids();
+
+ for (int il = 0; il < n_layer; ++il) {
+ float freq_base_l = freq_base;
+ float freq_scale_l = freq_scale;
+
+ if constexpr (iswa) {
+ freq_base_l = model.get_rope_freq_base (cparams, il);
+ freq_scale_l = model.get_rope_freq_scale(cparams, il);
+ }
+
+ res->t_layer_inp[il] = inpL;
+
+ ggml_tensor * inpSA = inpL;
+
+ // norm
+ cur = build_norm(inpL, model.layers[il].attn_norm, NULL, LLM_NORM_RMS, il);
+ cb(cur, "attn_norm", il);
+
+ // self-attention
+ {
+ auto [Qcur, Kcur, Vcur] = build_qkv(model.layers[il], cur,
+ n_embd_head, n_head, n_head_kv, il);
+
+ // gain-free per-head RMSNorm on Q and K before RoPE
+ Qcur = build_norm(Qcur, NULL, NULL, LLM_NORM_RMS, il);
+ Qcur = ggml_rope_ext(
+ ctx0, Qcur, inp_pos, nullptr,
+ n_rot, rope_type, n_ctx_orig, freq_base_l, freq_scale_l,
+ ext_factor, attn_factor, beta_fast, beta_slow);
+ cb(Qcur, "Qcur", il);
+
+ Kcur = build_norm(Kcur, NULL, NULL, LLM_NORM_RMS, il);
+ Kcur = ggml_rope_ext(
+ ctx0, Kcur, inp_pos, nullptr,
+ n_rot, rope_type, n_ctx_orig, freq_base_l, freq_scale_l,
+ ext_factor, attn_factor, beta_fast, beta_slow);
+ cb(Kcur, "Kcur", il);
+ cb(Vcur, "Vcur", il);
+
+ // default scale 1/sqrt(head_dim), as in the reference SDPA
+ cur = build_attn(inp_attn,
+ model.layers[il].wo, NULL, model.layers[il].wo_s,
+ Qcur, Kcur, Vcur, nullptr, nullptr, nullptr, 1.0f/sqrtf(float(n_embd_head)), il);
+ }
+ if (il == n_layer - 1 && inp_out_ids) {
+ cur = ggml_get_rows(ctx0, cur, inp_out_ids);
+ inpSA = ggml_get_rows(ctx0, inpSA, inp_out_ids);
+ }
+ ggml_tensor * ffn_inp = ggml_add(ctx0, cur, inpSA);
+ cb(ffn_inp, "ffn_inp", il);
+
+ // feed-forward network
+ cur = build_norm(ffn_inp, model.layers[il].ffn_norm, NULL, LLM_NORM_RMS, il);
+ cb(cur, "ffn_norm", il);
+
+ cur = build_ffn(cur,
+ model.layers[il].ffn_up, NULL, NULL,
+ model.layers[il].ffn_gate, NULL, NULL,
+ model.layers[il].ffn_down, NULL, NULL,
+ NULL,
+ LLM_FFN_SILU, LLM_FFN_PAR, il);
+ cb(cur, "ffn_out", il);
+
+ cur = ggml_add(ctx0, cur, ffn_inp);
+
+ cur = build_cvec(cur, il);
+ cb(cur, "l_out", il);
+
+ // input for next layer
+ inpL = cur;
+ }
+ cur = inpL;
+
+ cur = build_norm(cur, model.output_norm, NULL, LLM_NORM_RMS, -1);
+ cb(cur, "result_norm", -1);
+ res->t_embd = cur;
+
+ // lm_head
+ cur = build_lora_mm(model.output, cur, model.output_s);
+ cb(cur, "result_output", -1);
+ res->t_logits = cur;
+
+ ggml_build_forward_expand(gf, cur);
+}
+
+template struct llama_model_open1b::graph<false>;
+template struct llama_model_open1b::graph<true>;
+177
View File
@@ -0,0 +1,177 @@
# Local run: open-1b on one Arc B580
- Workdir: `inference/`
- Engine: the shared llama.cpp Vulkan build, with the open1b patch
- Language: Go 1.27
## Summary
- A Go tool, `hf2gguf`, with three commands: `dump`, `convert`, `verify`.
- Validation: 39 of 39 metadata keys and 220 of 220 tensors are byte-identical.
The two files share SHA-256 `a18530417c732bb115a3501a7e1388ea7a54bd2a96c70be571d1249aa210fbae`.
- Served on one Arc (Vulkan2, PCI `07:00.0`): about 114 tok/s decode, about 892
tok/s prefill, about 3.6 GiB VRAM.
## Inputs
| Item | Path | Notes |
| --- | --- | --- |
| HF checkpoint | `models/open-1b-sft/model.safetensors` | 6.43 GB, F32 |
| Reference GGUF | `models/open-1b-sft-f16.gguf` | 3.22 GB, f16, read-only |
| HF config | `models/open-1b-sft/config.json` | see below |
| Engine | `build-vulkan/bin/llama-server` | shared prod tree, not modified |
The config: `model_type=open1b`, 24 layers, hidden 2048, heads 16 and key/value
heads 4, head_dim 128, feed-forward 5632, vocab 128256, rope_theta 500000,
rms_eps 1e-5, hybrid sliding window 512 every 5 layers, gain-free qk_norm,
embedding_norm, untied embeddings.
Devices as the engine lists them:
```
Vulkan0: AMD Ryzen 7 7700X (RADV RAPHAEL_MENDOCINO), 15835 MiB
Vulkan1: Intel Arc B580 (BMG G21), 12216 MiB
Vulkan2: Intel Arc B580 (BMG G21), 12216 MiB
Vulkan3: Intel Arc B580 (BMG G21), 12216 MiB
```
Vulkan2 maps to PCI `0000:07:00.0`, confirmed through
`/sys/class/drm/renderD129/device`.
## Tool
```
inference/hf2gguf/
main.go dispatch, manifest types
rawkv.go read-only GGUF KV walker, array support
safetensors.go safetensors header and tensor bytes
hfconfig.go config and tokenizer parsing, validation
convert.go metadata reproduction, tensor mapping, F32 to F16
verify.go byte-level comparison
convert_test.go tests
```
`go-gguf` returns an empty value for `BTypeArray`, so `rawkv.go` walks the KV
section directly to keep the tokenizer arrays (`tokens`, `merges`, `token_type`)
as raw bytes. Writing still goes through the `go-gguf` writer. GGUF
serialization is not hand-rolled.
Commands:
```bash
go run . dump -gguf models/open-1b-sft-f16.gguf -o models/open-1b-sft-f16.manifest.json
go run . convert -ref models/open-1b-sft-f16.gguf \
-safetensors models/open-1b-sft/model.safetensors \
-config models/open-1b-sft/config.json \
-tokenizer-config models/open-1b-sft/tokenizer_config.json \
-tokenizer models/open-1b-sft/tokenizer.json \
-o models/open-1b-sft-f16-go.gguf
go run . verify -ref models/open-1b-sft-f16.gguf -got models/open-1b-sft-f16-go.gguf
```
## Tensor mapping
The reference names are the contract.
| GGUF tensor | HF source | Shape | Type |
| --- | --- | --- | --- |
| `token_embd.weight` | `embedding.tok_embeddings.weight` | 2048 x 128256 | F16 |
| `token_embd_norm.weight` | `emb_norm.weight` | 2048 | F32 |
| `output.weight` | `embedding.output.weight` | 2048 x 128256 | F16 |
| `output_norm.weight` | `norm_out.weight` | 2048 | F32 |
| `blk.i.attn_q.weight` | `blocks.i.attn.wq.weight` | 2048 x 2048 | F16 |
| `blk.i.attn_k.weight` | `blocks.i.attn.wk.weight` | 2048 x 512 | F16 |
| `blk.i.attn_v.weight` | `blocks.i.attn.wv.weight` | 2048 x 512 | F16 |
| `blk.i.attn_output.weight` | `blocks.i.attn.wo.weight` | 2048 x 2048 | F16 |
| `blk.i.ffn_gate.weight` | `blocks.i.ffn.w_gate_up.weight`, first half | 2048 x 5632 | F16 |
| `blk.i.ffn_up.weight` | `blocks.i.ffn.w_gate_up.weight`, second half | 2048 x 5632 | F16 |
| `blk.i.ffn_down.weight` | `blocks.i.ffn.w_down.weight` | 5632 x 2048 | F16 |
| `blk.i.attn_norm.weight` | `blocks.i.norm1.weight` | 2048 | F32 |
| `blk.i.ffn_norm.weight` | `blocks.i.norm2.weight` | 2048 | F32 |
The fused `w_gate_up` is split at the row boundary. Per-channel `*.weight_scale`
tensors in the checkpoint are dropped; the reference has none. F32 to F16 uses
round-to-nearest, ties to even, matching the numpy and torch casts.
## Validation
```
metadata: 39 checked, 0 mismatches
tensors: 220 checked, 220 byte-identical, 0 mismatches
verification PASSED
```
```
$ cmp models/open-1b-sft-f16.gguf models/open-1b-sft-f16-go.gguf && echo IDENTICAL
IDENTICAL
```
Both files hash to
`a18530417c732bb115a3501a7e1388ea7a54bd2a96c70be571d1249aa210fbae`.
Notable metadata, all byte-identical: `general.architecture=open1b`,
`open1b.block_count=24`, `open1b.context_length=4096`,
`open1b.attention.head_count=16`, `open1b.attention.head_count_kv=4`,
`open1b.rope.freq_base=500000`, `open1b.attention.sliding_window=512`,
`open1b.attention.sliding_window_pattern=5`, `tokenizer.ggml.model=gpt2`,
`tokenizer.ggml.pre=gpt-2`, `tokenizer.ggml.eos_token_id=4`. The full list is in
the manifest JSON.
`go build`, `go vet`, and `go test -race` are clean.
## Serve
```bash
setsid --fork ./build-vulkan/bin/llama-server \
-m models/open-1b-sft-f16-go.gguf \
--device Vulkan2 -ngl 99 -c 32768 -fa off --jinja \
--spec-type ngram-map-k,ngram-cache \
-t 16 --parallel 1 --alias open-1b \
--host 127.0.0.1 --port 8085 >/tmp/open1b.log 2>&1 </dev/null &
```
| Metric | Value |
| --- | --- |
| Decode | 111.7 to 117.2 tok/s |
| Prefill | about 892 tok/s |
| VRAM | about 3.6 GiB, released after stop |
Sample outputs, greedy:
```
prompt: The three primary colors are
out: red, blue, and yellow.
prompt: Q: What is the capital of France? A:
out: Paris
```
Stopped with `kill` after reading the pid from `ss -ltnp`.
## Caveats
1. Chat template. With the vendor tail `...assistant<|end_header|>` followed by a
blank line, this checkpoint emits an end-of-turn token first and returns an
empty reply. Reproduced with and without the speculative decoder, so it is not
the decoder. A single newline, an extra newline, or a seed token produces a
coherent answer. Since our GGUF is byte-identical to the reference, this is a
property of the released checkpoint.
2. Context cap. `open1b.context_length` is 4096, so the requested 32768 is capped.
3. The load warning `special_eos_id is not in special_eog_ids` also appears for
the reference file, so it is not introduced by the conversion.
4. Long greedy generation without a repetition penalty loops on this small model.
5. Process note. During one early check the response was piped through `python3`
to pretty-print JSON. That was a mistake and a breach of the no-Python rule.
The tool itself is entirely Go, and nothing in the pipeline depends on Python.
No Python files or virtualenv were created for the tool.
## Files
| Path | What |
| --- | --- |
| `hf2gguf/` | Go module with tests |
| `hf2gguf/README.md` | usage |
| `models/open-1b-sft-f16-go.gguf` | our output |
| `models/open-1b-sft-f16.manifest.json` | dumped contract |
| `models/open-1b-sft-f16.gguf` | reference |
| `/tmp/open1b.log` | server log |
-214
View File
@@ -1,214 +0,0 @@
# Gensyn OPEN-1B audit — independent replay (CPU, Linux x86-64)
- **Date:** 2026-09-28
- **Host:** `brainstation`, Linux x86-64, kernel `7.3.0-070300rc1-generic` (Ubuntu), **no NVIDIA GPU**
- **Auditor:** SE sub-agent (independent)
- **Workdir:** `/root/projects/gensyn/audit`
- **Harness:** `gensyn-ai/open-transformers` @ `d7b7b674cd851f7a16815229f060658180cb772b`
("Initial commit: the OPEN-1B pretraining and audit-replay harness", 2026-09-15), cloned via git.
- **Verdict:** ✅ **MATCH** on the `1b_repop_v2` init unit. ❌ No training-interval
replay run (not defined by this kit and not feasible on this host — see §7).
---
## 1. Kit selection
Four kits were listed. All four `trajectory.json` files declare the **same**
`1b_repop_v2` init commitment
`16554a1119745f1b9bf4ac60b03226e5e9ca1247a2bd56169cd4c822fd457aed`, so the choice
of kit does not change the init hash under test. The authoritative mapping is
published by the audit app itself at `https://open1b.gensyn.ai/manifest.json`:
```json
"run": { "id": "20260722-213626-ad3276b", "model": "1b_repop_v2",
"initStateHash": "16554a1119745f1b9bf4ac60b03226e5e9ca1247a2bd56169cd4c822fd457aed" },
"audit": { "kit": { "id": "pt-f91110a0d0c8_rp-c9ca6e71e673",
"urlBase": "https://storage.googleapis.com/gensyn-audit-public/audit-kit/pt-f91110a0d0c8_rp-c9ca6e71e673" } }
```
**Kit under test: `pt-f91110a0d0c8_rp-c9ca6e71e673`.**
- `pretrain_commit`: `f91110a0d0c84fa3bad808f365567d9c655d10b2`
- `repop_commit`: `c9ca6e71e673660afc6c87de2592fa6f62c87b15`
- `torch_requirement`: `torch==2.10.0`
(The other three kits — `pt-cb4ac29438bc_rp-…`, `pt-f6bc086dfebd_rp-…`,
`pt-f5eece09abe6_rp-…` — carry the same init commitment but are older
pretrain/repop pins. Two share the `c9ca6e71e673` repop but a different pretrain
commit; the app manifest picks `f91110a0d0c8`.)
## 2. Toolchain
| Component | Version / identity |
|---|---|
| Python | 3.11.16 (managed by `uv`; system Python 3.14 untouched) |
| `uv` | 0.12.17 |
| PyTorch | `2.10.0+cpu` (`torch.version.cuda == None`) |
| `repop` | 0.1.5, `build_info().commit == c9ca6e71e673660afc6c87de2592fa6f62c87b15` ✅ (== kit.json `repop_commit`); backends `["cpu","cuda"]` |
| `pretrain` | 0.1.0 (kit wheel) |
| Go (glue tools) | go1.27.0 (fabric rule: all glue/parsing in Go) |
**Deviation, stated plainly:** the RUNBOOK targets Linux hosts with an NVIDIA GPU
and points them at the CUDA 12.9 PyTorch index. This host has no GPU, and the
README names CPU as the *reference* device, so the exact pin `torch==2.10.0` was
installed from PyTorch's official **CPU** index (`.../whl/cpu`, wheel
`2.10.0+cpu`). The pinned version is unchanged; only the CPU build variant was
chosen deliberately, not by an accidental PyPI fallback. Init is device-independent
by construction (see §6), so this does not weaken the init result.
Python (`uv`-managed) is used **only** because the vendor harness is Python and
ships as wheels; all download/verification/listing glue is Go under `tools/`
(`kitinspect`, `trajinspect`, `kitfetch`, `gslist`).
## 3. Commands run
```bash
# workdir + docs
mkdir -p /root/projects/gensyn/audit && cd /root/projects/gensyn/audit
git clone --depth 1 https://github.com/gensyn-ai/open-transformers
# kit manifests + trajectories (Go inspector)
go run ./tools/kitinspect kits/*/
go run ./tools/trajinspect kits/*/trajectory.json
# download + per-file sha256 verification against kit.json (Go tool,
# downloads to .part, verifies, removes on mismatch)
go run ./tools/kitfetch kits/pt-f91110a0d0c8_rp-c9ca6e71e673 \
kit-pt-f91110a0d0c8_rp-c9ca6e71e673
# Python 3.11 env
uv venv --python 3.11 .venv
uv pip install --python .venv --index-url https://download.pytorch.org/whl/cpu "torch==2.10.0"
uv pip install --python .venv \
./kit-.../repop-0.1.5-cp311-cp311-linux_x86_64.whl \
./kit-.../pretrain-0.1.0-cp311-cp311-linux_x86_64.whl
# provenance assertions
.venv/bin/python -c "import repop,json; print(repop.build_info())"
# THE AUDIT (init unit)
PRETRAIN_AUDIT_MEMLOG=1 /usr/bin/time -v .venv/bin/pretrain-audit-replay \
--from-init --until-step 0 \
--config-name 1b_repop_v2 \
--device cpu \
--expect-hash 16554a1119745f1b9bf4ac60b03226e5e9ca1247a2bd56169cd4c822fd457aed
```
## 4. SHA-256 verification table (all 5 kit files)
Every file was hashed **before** install; the Go fetcher adopts a file only on a
full 64-hex match and deletes rejected bytes. Expected values are read from the
kit's own `kit.json`.
| File | expected (`kit.json`) | computed | result |
|---|---|---|---|
| `pretrain-0.1.0-cp311-cp311-linux_x86_64.whl` | `ecc49d1c05c0e2876a59d98d877795769d6e9620eb66e4b4c866b16f695fb174` | same | ✅ OK |
| `pretrain-0.1.0-cp311-cp311-macosx_14_0_arm64.whl` | `fbd23a54aa0bf87775e86493d67f759d1c1f4cc1dcd63ad991fba59dcb92491c` | same | ✅ OK |
| `repop-0.1.5-cp311-cp311-linux_x86_64.whl` | `15f171643871211b2a772ee83c94833c0be2eef0495790cedc33813c95147ff0` | same | ✅ OK |
| `repop-0.1.5-cp311-cp311-macosx_14_0_arm64.whl` | `d8658e0327c7278812ecb6dd455cd4cf8723a974a78788adba41d5f747289c69` | same | ✅ OK |
| `trajectory.json` | `9435515c37d8956d0c81d5bc78c0a383bb6bf27c57b5dbaf1f8be564df0b324a` | same | ✅ OK |
Byte sizes also matched `kit.json`. Only the two **linux_x86_64** wheels were
installed; the macOS arm64 wheels were downloaded and verified but not installed.
## 5. Init-unit result — MATCH
Raw result object printed by the harness (stdout, exit status 0):
```json
{
"step": 0,
"consumed_tokens": 0,
"state_hash": "16554a1119745f1b9bf4ac60b03226e5e9ca1247a2bd56169cd4c822fd457aed",
"mode": "init",
"repop": { "commit": "c9ca6e71e673660afc6c87de2592fa6f62c87b15",
"backends": ["cpu", "cuda"], "cuda_arch_list": "7.5 8.0 9.0" },
"device": "cpu",
"expected": "16554a1119745f1b9bf4ac60b03226e5e9ca1247a2bd56169cd4c822fd457aed",
"match": true,
"cpu_threads": { "requested": "auto", "source": "runtime-default",
"torch_num_threads": 8, "omp_num_threads": null, "mkl_num_threads": null }
}
```
Log line: `from-init: regenerated init state_hash=16554a1119745f1b expected=16554a1119745f1b MATCH=True`.
Config `1b_repop_v2`, seed `42`, loss path `fused CE+z-loss`. The only warning is
the expected `not distributed + no CUDA — parallelize applies AC only (FSDP
skipped; dev mode)`, i.e. the documented CPU reference path.
**Resource notes:** wall **58.3 s**, user 78.5 s / sys 61.1 s, **peak RSS
16.80 GB** (16,804,956 KB), **0 swaps**, exit 0. (README/RUNBOOK size this unit at
~21.9 GB peak on CPU; we measured 16.8 GB. Free RAM was ~11 GB and the unit still
completed without swapping, so the doc figure is conservative.)
## 6. What this proves — and what it does not
**Proves.** The bytes installed are the bytes `kit.json` published (all five
SHA-256s match). The `repop` kernel build is the one the kit claims
(`build_info().commit == repop_commit`). Running the vendor harness on this
CPU, from seed, the `1b_repop_v2` **initial state** — seeded device-independent
trunc-normal init plus zeroed optimizer moments — reproduces the published
canonical hash `16554a11…` **bit-for-bit**. Independent third-party
reproduction of the run's init commitment: ✅.
**Does not prove.**
1. **No training interval.** Per the README: "An initialization-only unit does
not verify a training interval." A match here says nothing about any forward,
backward, AdamW or data-stream step. The claimed bitwise reproducibility of
*optimizer steps* is **not** tested by this run.
2. **`repop` is closed source.** Only compiled wheels are shipped; the kernel
arithmetic that makes training bitwise reproducible cannot be read or
independently re-derived from this kit. The init unit does not even exercise
the kernels that the recent `c9ca6e71e673` repin changed (the trajectory notes
say exactly this), so a match is *not* evidence for the kernel-level BFR
contract at that commit.
3. **v3-hash coverage.** The README states the v3 hash covers weights, optimizer
state, gradients and the batch digest, but **not** RNG, the data-stream
cursor, spike state, or the `meta.json` descriptor keys. Even a matching
interval hash would leave those unverified.
4. **Init is the easy case by design.** `init.py` samples Philox on CPU and
byte-copies, so it is device-independent; it is expected to match across
devices. The hard cross-device claim lives in the training kernels, which
this unit does not touch.
5. Provenance of the *published run* is not established; only that the pinned
artifacts reproduce the published commitment on this machine.
## 7. Step-interval replay — deliberately not attempted
Stopping after the init unit is a considered decision, not a stall:
- **Not defined by the kit.** This kit's `trajectory.json` (`init-units-v1`)
contains **only 3 init units** — `100m_smoke_repop`, `1b_repop_run3`,
`1b_repop_v2`. Its own note says: "Step-interval units are added per run once
its checkpoints and state_hash files are published." **No interval unit with a
canonical target hash is published here**, so the kit defines no
step-interval commitment to check. (The run's per-step log exists at
`gs://gensyn-open-1b/logs/state_hashes.jsonl`, but the harness has no automatic
lookup and the audit app gates submissions.)
- **Memory.** A step-interval replay needs ~24 GB (README), and the audit's
auto-offload only reaches "every published unit" at that figure. This host had
~11 GB RAM available (29 GB total, 18 GB in use); an init unit already peaked at
16.8 GB. The heavy replay would swap-thrash and would not fit honestly.
- **Disk/size.** One checkpoint `ckpt/step_000000100/` is **19.31 GB**
(150 objects); the full `ckpt/` prefix is **15.64 TB** (810 checkpoint dirs).
With 39 GB free, a single checkpoint is tight against free space and the
documented `--from-init + --gcs-root` combination is broken (wrong shard
anchor), so a from-init interval would first require staging shards via
`--data-root`.
Re-attempting an interval audit should be done on a host with ≥32 GB free RAM
(NVIDIA GPU preferred: `--device cuda`) and ≥60 GB free disk.
## 8. Files produced
- `open-transformers/` — harness clone (docs read: `README.md`,
`docs/audit-replay-usage.md`, `scripts/audit_volunteer/RUNBOOK.md`)
- `kits/<kit>/kit.json`, `kits/<kit>/trajectory.json` — manifests
- `kit-pt-f91110a0d0c8_rp-c9ca6e71e673/` — verified wheels + trajectory
- `.venv/` — Python 3.11 audit env
- `init-replay.log` — full captured replay output + `/usr/bin/time -v`
- `tools/{kitinspect,trajinspect,kitfetch,gslist}` — Go glue
- `REPORT.md` — this file
No secrets were written. Nothing outside the workdir was modified or deleted.
-332
View File
@@ -1,332 +0,0 @@
# Gensyn open-1b (SFT) — local bring-up & proof of run
Epic: `vado/1b#1` / task #4 (SE). Workdir: `/root/projects/gensyn/inference/`.
Engine: existing shared llama.cpp **Vulkan** build (`/root/projects/inference/build-vulkan/bin/llama-server`).
Language: **Go 1.27 only** (see "Process note" re one accidental `python3` call).
## TL;DR
- Built a pure-Go tool `hf2gguf` (module `hf2gguf`) that **dumps** the reference GGUF
(go-gguf + a minimal raw-KV walker for arrays), **converts** `model.safetensors`
→ `models/open-1b-sft-f16-go.gguf`, and **verifies** the two files.
- **Validation: PASS — 39/39 metadata KV byte-identical, 220/220 tensors byte-identical.**
Whole files are byte-for-byte identical (`cmp` + SHA-256 match).
- **Served** on one Arc (Vulkan2 / `07:00.0`) with the existing Vulkan build; coherent
output, **~114 tok/s decode**, **~892 tok/s prefill**, ~**3.6 GiB VRAM**.
- No commit made (all under gitignored `inference/`).
Reference SHA-256 (and our output — identical):
`a18530417c732bb115a3501a7e1388ea7a54bd2a96c70be571d1249aa210fbae`
---
## 1. Inputs & topology
| Item | Path | Notes |
|------|------|-------|
| HF checkpoint (fp32) | `models/open-1b-sft/model.safetensors` | 6,432,081,672 B; dtype **F32**; header 34560 B |
| Reference GGUF (f16) | `models/open-1b-sft-f16.gguf` | 3,220,153,856 B; vendor converter; **read-only oracle** |
| HF config | `models/open-1b-sft/config.json` | `model_type=open1b`, 24 layers, hidden 2048, heads 16/4, head_dim 128, ffn 5632, vocab 128256, rope_theta 5e5, rms_eps 1e-5, swa 512 every 5, qk_norm gain-free, embedding_norm, untied |
| Vendor Python | `models/open-1b-sft/modeling_open1b.py` | read only, never executed |
| Engine | `/root/projects/inference/build-vulkan/bin/llama-server` | symlink → `/mnt/2TB/projects/build-vulkan`; shared prod tree, untouched |
GPU devices as reported by the engine (`--list-devices`):
```
Vulkan0: AMD Ryzen 7 7700X (RADV RAPHAEL_MENDOCINO) (15835 MiB)
Vulkan1: Intel(R) Arc(tm) B580 Graphics (BMG G21) (12216 MiB)
Vulkan2: Intel(R) Arc(tm) B580 Graphics (BMG G21) (12216 MiB)
Vulkan3: Intel(R) Arc(tm) B580 Graphics (BMG G21) (12216 MiB)
```
`Vulkan2` was confirmed to map to PCI `0000:07:00.0` (Arc #2) via
`/sys/class/drm/renderD129/device`.
---
## 2. Tool: `hf2gguf` (Go 1.27, `github.com/cymertek/go-gguf`)
```
inference/hf2gguf/
go.mod, go.sum
main.go # subcommand dispatch + JSON manifest types
rawkv.go # minimal read-only GGUF KV walker (fills go-gguf's array gap)
safetensors.go # safetensors header + tensor byte access
hfconfig.go # config.json / tokenizer_config.json / tokenizer.json validation
convert.go # metadata reproduction + HF→GGUF tensor mapping + F32→F16
verify.go # byte-level metadata & tensor comparison
convert_test.go
```
### Why a raw KV walker
`go-gguf`'s `MetadataEntry.Value()` intentionally returns an empty value for
`BTypeArray` (see `readLazyValue` in the library). The tokenizer arrays
(`tokens`, `scores`→`token_type`, `merges`) are exactly the values we must copy
verbatim, so `rawkv.go` reads the KV section directly and preserves each array's
`elem_type`, element count, and **raw element bytes**. Those raw bytes are fed
straight into go-gguf's writer `Value{Raw: ...}`, so serialization is still done
by the library (nothing hand-rolled on the write path).
### Subcommands
```bash
# 2a. dump — emit the reproduction contract as JSON
go run . dump -gguf models/open-1b-sft-f16.gguf -o models/open-1b-sft-f16.manifest.json
# 2b/c. convert — reproduce metadata + map safetensors→GGUF (fp32→f16)
go run . convert \
-ref models/open-1b-sft-f16.gguf \
-safetensors models/open-1b-sft/model.safetensors \
-config models/open-1b-sft/config.json \
-tokenizer-config models/open-1b-sft/tokenizer_config.json \
-tokenizer models/open-1b-sft/tokenizer.json \
-o models/open-1b-sft-f16-go.gguf
# 3. verify — byte-level equality against the reference
go run . verify -ref models/open-1b-sft-f16.gguf -got models/open-1b-sft-f16-go.gguf
```
`dump` output (the contract) is at
`models/open-1b-sft-f16.manifest.json` (39 metadata entries + 220 tensors,
with per-array `raw_sha256`).
### Tensor mapping (reference names are the contract)
Shapes are the reference's; ggml `ne0` is the innermost (= HF input) dim, so HF
bytes are copied unpermuted with the dims reversed.
| Reference tensor | HF source | Shape | Type | Count |
|---|---|---|---|---|
| `token_embd.weight` | `embedding.tok_embeddings.weight` | [2048,128256] | F16 | 1 |
| `token_embd_norm.weight` | `emb_norm.weight` | [2048] | F32 | 1 |
| `output.weight` | `embedding.output.weight` | [2048,128256] | F16 | 1 |
| `output_norm.weight` | `norm_out.weight` | [2048] | F32 | 1 |
| `blk.{i}.attn_q.weight` | `blocks.{i}.attn.wq.weight` | [2048,2048] | F16 | 24 |
| `blk.{i}.attn_k.weight` | `blocks.{i}.attn.wk.weight` | [2048,512] | F16 | 24 |
| `blk.{i}.attn_v.weight` | `blocks.{i}.attn.wv.weight` | [2048,512] | F16 | 24 |
| `blk.{i}.attn_output.weight` | `blocks.{i}.attn.wo.weight` | [2048,2048] | F16 | 24 |
| `blk.{i}.ffn_gate.weight` | `blocks.{i}.ffn.w_gate_up.weight` (first half) | [2048,5632] | F16 | 24 |
| `blk.{i}.ffn_up.weight` | `blocks.{i}.ffn.w_gate_up.weight` (second half) | [2048,5632] | F16 | 24 |
| `blk.{i}.ffn_down.weight` | `blocks.{i}.ffn.w_down.weight` | [5632,2048] | F16 | 24 |
| `blk.{i}.attn_norm.weight` | `blocks.{i}.norm1.weight` | [2048] | F32 | 24 |
| `blk.{i}.ffn_norm.weight` | `blocks.{i}.norm2.weight` | [2048] | F32 | 24 |
- The fused `w_gate_up` is split at the row boundary (`chunk(2,-1)` → first half
`gate`, second half `up`), matching the reference.
- Quantized per-channel `*.weight_scale` tensors in the checkpoint are **dropped**
(the reference has none).
- F32→F16 uses IEEE round-to-nearest, ties-to-even (`f32bitsToF16`), matching
numpy/torch casts; the reference tensors are byte-identical to ours, which
empirically proves the rounding matched.
- HF config/tokenizer are **parsed and validated** against the reference, but the
reference remains authoritative for every emitted value.
---
## 3. Validation
`go run . verify` streams both files and compares on raw wire bytes:
```
metadata: 39 checked, 0 mismatches
tensors: 220 checked, 220 byte-identical, 0 mismatches
verification PASSED: 39 metadata keys and 220 tensors byte-identical
```
Whole-file check:
```
$ cmp models/open-1b-sft-f16.gguf models/open-1b-sft-f16-go.gguf && echo IDENTICAL
IDENTICAL
$ sha256sum models/open-1b-sft-f16.gguf models/open-1b-sft-f16-go.gguf
a18530417c732bb115a3501a7e1388ea7a54bd2a96c70be571d1249aa210fbae open-1b-sft-f16.gguf
a18530417c732bb115a3501a7e1388ea7a54bd2a96c70be571d1249aa210fbae open-1b-sft-f16-go.gguf
```
The per-tensor diff is empty (0 mismatches) — every tensor's payload is
byte-identical, in the same order, with the same shape and ggml type.
### Metadata contract (all 39 keys, all byte-identical)
| Key | Type | Value |
|---|---|---|
| `general.architecture` | STRING | `open1b` |
| `general.type` | STRING | `model` |
| `general.name` | STRING | `Open 1b Sft` |
| `general.finetune` | STRING | `sft` |
| `general.basename` | STRING | `open` |
| `general.size_label` | STRING | `1B` |
| `general.license` | STRING | `apache-2.0` |
| `general.base_model.count` | UINT32 | 1 |
| `general.base_model.0.name` | STRING | `Open 1b Midtrained 93B` |
| `general.base_model.0.organization` | STRING | `Gensyn` |
| `general.base_model.0.repo_url` | STRING | `https://huggingface.co/Gensyn/open-1b-midtrained-93B` |
| `general.dataset.count` | UINT32 | 1 |
| `general.dataset.0.name` | STRING | `Tulu 3 Sft Olmo 2 Mixture 0225` |
| `general.dataset.0.version` | STRING | `0225` |
| `general.dataset.0.organization` | STRING | `Allenai` |
| `general.dataset.0.repo_url` | STRING | `https://huggingface.co/allenai/tulu-3-sft-olmo-2-mixture-0225` |
| `general.tags` | ARRAY[STRING] | 5 (`gensyn`, `verifiable-training`, `reproducible-training`, `repops`, `text-generation`) |
| `general.languages` | ARRAY[STRING] | 1 (`en`) |
| `open1b.block_count` | UINT32 | 24 |
| `open1b.context_length` | UINT32 | 4096 |
| `open1b.embedding_length` | UINT32 | 2048 |
| `open1b.feed_forward_length` | UINT32 | 5632 |
| `open1b.attention.head_count` | UINT32 | 16 |
| `open1b.attention.head_count_kv` | UINT32 | 4 |
| `open1b.rope.freq_base` | FLOAT32 | 500000 |
| `open1b.attention.layer_norm_rms_epsilon` | FLOAT32 | 1e-05 |
| `open1b.attention.key_length` | UINT32 | 128 |
| `open1b.attention.value_length` | UINT32 | 128 |
| `general.file_type` | UINT32 | 1 (F16) |
| `open1b.attention.sliding_window` | UINT32 | 512 |
| `open1b.attention.sliding_window_pattern` | UINT32 | 5 |
| `general.quantization_version` | UINT32 | 2 |
| `tokenizer.ggml.model` | STRING | `gpt2` |
| `tokenizer.ggml.pre` | STRING | `gpt-2` |
| `tokenizer.ggml.tokens` | ARRAY[STRING] | 128256 (sha256 `30bab437…`) |
| `tokenizer.ggml.token_type` | ARRAY[INT32] | 128256 (sha256 `5ca8c567…`) |
| `tokenizer.ggml.merges` | ARRAY[STRING] | 127994 (sha256 `9e3a7dd4…`) |
| `tokenizer.ggml.eos_token_id` | UINT32 | 4 |
| `tokenizer.chat_template` | STRING | (501-byte Jinja template) |
Array `raw_sha256` values are in `models/open-1b-sft-f16.manifest.json`.
### HF cross-checks performed by `convert`
```
config.json OK: arch=open1b layers=24 hidden=2048 heads=16/4 head_dim=128 ffn=5632 vocab=128256 swa=512/5 tie=false qk_norm=true(gain=false) emb_norm=true
tokenizer_config.json OK: eos=<|eot|> chat_template=501 bytes
tokenizer.json OK: type=BPE vocab=128256 +added=6 -> 128256 tokens, 127994 merges
```
### Go build / vet / test
```
$ go build ./... # OK
$ go vet ./... # OK
$ go test -race -v ./...
--- PASS: TestF32BitsToF16 (0.00s)
--- PASS: TestF32ToF16Bytes (0.00s)
--- PASS: TestRawKVRoundTrip (0.00s)
--- PASS: TestVerifyRealFiles (6.59s)
PASS
ok hf2gguf 7.602s
```
`TestVerifyRealFiles` re-runs the full 3.2 GB byte comparison (skipped on
`-short` or when the artifacts are absent).
---
## 4. Serve on one Arc (Vulkan2)
```
setsid --fork /root/projects/inference/build-vulkan/bin/llama-server \
-m /root/projects/gensyn/inference/models/open-1b-sft-f16-go.gguf \
--device Vulkan2 -ngl 99 -c 32768 -fa off --jinja \
--spec-type ngram-map-k,ngram-cache -t 16 --parallel 1 --alias open-1b \
--host 127.0.0.1 --port 8085 >/tmp/open1b.log 2>&1 </dev/null &
```
Startup log:
```
load_model: loading model '.../open-1b-sft-f16-go.gguf'
load: special_eos_id is not in special_eog_ids - the tokenizer config may be incorrect
llama_context: n_ctx_seq (32768) > n_ctx_train (4096) -- possible training context overflow
load_model: the slot context (32768) exceeds the training context of the model (4096) - capping
load_model: initializing, n_slots = 1, n_ctx_slot = 4096, kv_unified = 'false'
model loaded
listening on http://127.0.0.1:8085
```
Device confirmation: PID `1378706` maps `/dev/dri/renderD129` with large
`rw-s` regions; `renderD129 → /sys/devices/.../0000:07:00.0` = Arc #2 = Vulkan2.
`/props` reports `"speculative.types":"none,ngram-map-k,ngram-cache"` and
`"model_ftype":"F16"`.
### Performance (single Arc, 1 slot, `-fa off`, ngram spec)
| Metric | Value |
|---|---|
| Decode | **~111.7–117.2 tok/s** (e.g. 256 tok @ 111.65; 96 tok @ 117.20) |
| Prefill | **~892 tok/s** (961 prompt tok in 1077 ms) |
| VRAM on Vulkan2 | free 10971 → 7258 MiB ⇒ **≈ 3.6 GiB** used; fully released after `kill` |
### Sample outputs
```
$ curl ... /completion {"prompt":"The three primary colors are", greedy}
red, blue, and yellow.
$ curl ... /completion {"prompt":"Q: What is the capital of France?\nA:", greedy, 96}
Paris
$ curl ... /completion (seeded chat turn, see caveat) 113.9 tok/s
" Venus and Mars. Venus is the hottest planet in our Solar System, with
temperatures reaching 465°C (1,470°F). Mars, on the other hand, is a
terrestrial planet, meaning it has a solid surface and liquid water at its poles."
```
### Stopping
```
$ ss -ltnp | grep :8085 # -> pid=1378706
$ kill 1378706 # never pkill -f
```
Both the test server (:8085) and a diagnostic server (:8086) were stopped;
`--list-devices` returned to 10971 MiB free on Vulkan2/3.
---
## 5. Caveats
1. **Chat template newline quirk (model-side, not conversion-side).** With the
exact vendor template tail `<|end_header|>\n\n` for the assistant turn, this
checkpoint emits `<|eot|>` as the first sampled token → empty reply / instant
EOS (reproduced on both :8085 with ngram spec and a :8086 run with
`--spec-type none`, so it is **not** the speculative decoder). A single `\n`,
`\n\n\n`, or a seed token makes it answer coherently:
| tail after `assistant<|end_header|>` | result |
|---|---|
| `\n\n` (vendor template) | immediate EOS |
| `\n` | `\n1. **Earth**: …` (coherent) |
| `\n\n\n` | `\n1. **Earth**: …` (coherent) |
| ` ` | `\nSure, here are two names…` (coherent) |
Because our GGUF is byte-identical to the vendor reference, this is a
property of the released checkpoint/GGUF (likely SFT formatting), not of our
conversion. Workarounds for consumers: use `ignore_eos` + trim, seed the
assistant turn, or fix the template's trailing newline.
2. **Context capped to 4096.** `open1b.context_length = 4096`, so the requested
`-c 32768` is capped to `n_ctx_slot = 4096`. KV is f16 (`-fa off` mandatory on
Arc per `brainstation-gpu`).
3. **`special_eos_id is not in special_eog_ids`** warning on load — present for
the reference file too (byte-identical), so not introduced by us.
4. **1B greedy degeneration.** Long greedy generation without repetition penalty
loops on this small model; use sampling / `repeat_penalty` for long outputs.
5. **Process note (disclosure).** During an early diagnostic I piped one
`/completion` response through `python3 -c` to pretty-print JSON — a breach of
the "no Python" rule (the tool itself is 100% Go, and nothing in the pipeline
depends on Python). All subsequent parsing was done with Go/raw JSON. No `.py`
file or venv was created; the only Python files in the tree are the vendor's
read-only `models/open-1b-sft/*.py`.
---
## 6. Artifacts
| Path | What |
|---|---|
| `hf2gguf/` | Go module (`dump` / `convert` / `verify`) + tests |
| `hf2gguf/README.md` | tool usage |
| `models/open-1b-sft-f16-go.gguf` | our output (byte-identical to reference) |
| `models/open-1b-sft-f16.manifest.json` | dumped contract (39 KV + 220 tensors) |
| `models/open-1b-sft-f16.gguf` | reference (untouched) |
| `/tmp/open1b.log` | server log |
No changes were committed; everything lives under gitignored `inference/`.
+73 -41
View File
@@ -1,82 +1,114 @@
# hf2gguf
Pure-Go (Go 1.27) tool to convert a HuggingFace `open-1b` safetensors checkpoint
into GGUF **byte-identically** to a supplied reference GGUF, plus a byte-level
verifier. Built for epic `vado/1b#1` (Gensyn open-1b local bring-up).
A Go 1.27 tool that converts a Hugging Face open-1b checkpoint to GGUF and
verifies the result byte for byte. No Python, no torch, no pip. Library:
[go-gguf](https://github.com/cymertek/go-gguf).
Library: [`github.com/cymertek/go-gguf`](https://github.com/cymertek/go-gguf)
(reader + writer). No Python anywhere in the tool.
For open-1b it reproduces llama.cpp's `convert_hf_to_gguf.py` output exactly,
down to the same SHA-256. The output is deterministic and the check is built in,
so the conversion can be trusted without trusting a Python environment.
## Why Go, and why this shape
| | hf2gguf | llama.cpp `convert_hf_to_gguf.py` |
| --- | --- | --- |
| Runtime | one Go binary | Python 3 with torch, numpy, transformers, gguf-py |
| Setup | `go build` | pip or uv, wheels, pinned versions |
| Startup | immediate | imports torch and its chain |
| Memory | streams one tensor at a time, O(1) per tensor | loads tensors through torch |
| Output check | built-in byte-level `verify` | none |
| Output | byte-identical to a reference file | the reference |
| Scope | open-1b only | many architectures |
| Quantization | none, F16 only | many quant types |
| Dependencies | go-gguf | a large Python tree |
The advantages that matter here:
- No Python runtime. Nothing to install or pin, and it runs where Python is
unavailable or unwanted.
- Deterministic output, checked byte for byte against a reference.
- Small and readable. One binary, one small module, no large dependency tree.
- Streaming. Tensors are converted one at a time; memory does not grow with the
model size.
- Easy to build and ship. `go build` and a single file in CI images.
## Limits
- Narrow by design. It maps one Llama-derived architecture and does not claim to
convert arbitrary models.
- Metadata is copied from a reference GGUF, not derived from the config alone.
The config and tokenizer are parsed and validated, but the reference stays
authoritative.
- F16 only. There is no quantization to Q4, Q8, or other ggml types.
- It depends on a third-party GGUF writer for serialization.
## Build
```bash
go build ./...
go vet ./...
go test -race ./... # full test includes the 3.2 GB byte comparison
go test -race ./... # the full test includes the 3.2 GB byte comparison
```
## Subcommands
## Commands
### `dump` — the reproduction contract
### dump
Reads the reference GGUF and emits a JSON manifest of **all** metadata KV pairs
(key, type, value / array elem-type + count + `raw_sha256`) and every tensor
(name, shape, ggml type, nbytes).
Reads the reference GGUF and writes a JSON manifest of all metadata pairs (key,
type, value, or array element type, count, and `raw_sha256`) and every tensor
(name, shape, ggml type, byte count).
```bash
go run . dump -gguf models/open-1b-sft-f16.gguf -o models/open-1b-sft-f16.manifest.json
```
`go-gguf` exposes scalar/string metadata and tensors; its `MetadataEntry.Value()`
returns an empty value for `BTypeArray`, so `rawkv.go` walks the KV section
directly to preserve tokenizer arrays (`tokens`, `merges`, `token_type`) as raw
bytes. Writing still goes through `go-gguf`'s writer — nothing hand-rolls GGUF
serialization.
`go-gguf` returns an empty value for `BTypeArray`, so `rawkv.go` walks the KV
section directly to keep the tokenizer arrays as raw bytes. Writing still goes
through the `go-gguf` writer.
### `convert` — emit our GGUF
### convert
```bash
go run . convert \
-ref models/open-1b-sft-f16.gguf \
-safetensors models/open-1b-sft/model.safetensors \
-config models/open-1b-sft/config.json \
-ref models/open-1b-sft-f16.gguf \
-safetensors models/open-1b-sft/model.safetensors \
-config models/open-1b-sft/config.json \
-tokenizer-config models/open-1b-sft/tokenizer_config.json \
-tokenizer models/open-1b-sft/tokenizer.json \
-o models/open-1b-sft-f16-go.gguf
-tokenizer models/open-1b-sft/tokenizer.json \
-o models/open-1b-sft-f16-go.gguf
```
- Metadata is copied verbatim from the reference KV section.
- Tensors are mapped HF FQN → GGUF name (see the module docs in `convert.go`),
with `blocks.{i}.ffn.w_gate_up.weight` split into `ffn_gate`/`ffn_up`, and
`*.weight_scale` dropped (the reference has none).
- Payloads stream from safetensors and are converted F32→F16 with IEEE
round-to-nearest-even on the fly (O(1) memory per tensor).
- `-config` / `-tokenizer-config` / `-tokenizer` are optional; when given they
are parsed and **validated** against the reference (mismatch is fatal).
- Metadata is copied verbatim from the reference.
- Tensors are mapped by name, with `blocks.i.ffn.w_gate_up.weight` split into
`ffn_gate` and `ffn_up`, and `*.weight_scale` dropped.
- Payloads stream from safetensors and convert F32 to F16 on the fly, using
round-to-nearest, ties to even.
- The config and tokenizer flags are optional. When given, they are parsed and
validated against the reference; a mismatch is fatal.
### `verify` — byte-level equality
### verify
```bash
go run . verify -ref models/open-1b-sft-f16.gguf -got models/open-1b-sft-f16-go.gguf
```
Compares metadata on raw wire bytes and tensors by shape, type and full payload
equality (streaming, first-diff offset reported on failure).
Compares metadata on raw wire bytes and tensors by shape, type, and full payload.
Reports the first differing offset on failure.
```
metadata: 39 checked, 0 mismatches
tensors: 220 checked, 220 byte-identical, 0 mismatches
verification PASSED: 39 metadata keys and 220 tensors byte-identical
verification PASSED
```
## Layout
| File | Purpose |
|---|---|
| `main.go` | dispatch, manifest types, `dump` |
| `rawkv.go` | read-only GGUF KV walker (array support) |
| `safetensors.go` | safetensors header/byte access |
| `hfconfig.go` | HF config/tokenizer parsing + validation |
| `convert.go` | metadata reproduction, tensor mapping, F32→F16 |
| `verify.go` | `verify` command + comparison engine |
| --- | --- |
| `main.go` | dispatch, manifest types, dump |
| `rawkv.go` | read-only GGUF KV walker with array support |
| `safetensors.go` | safetensors header and byte access |
| `hfconfig.go` | HF config and tokenizer parsing, validation |
| `convert.go` | metadata reproduction, tensor mapping, F32 to F16 |
| `verify.go` | verify command and comparison engine |
| `convert_test.go` | F16 rounding, KV round-trip, full-file verification |
+2 -2
View File
@@ -1,6 +1,6 @@
package main
// convert.go — emit models/open-1b-sft-f16-go.gguf from the HF safetensors,
// convert.go converts the HF safetensors into models/open-1b-sft-f16-go.gguf,
// reproducing the reference GGUF's metadata (verbatim) and tensor set/order.
//
// Tensor mapping (HF FQN -> GGUF name), shapes are the reference's (ggml ne0 is
@@ -117,7 +117,7 @@ func cmdConvert(args []string) error {
defer st.Close()
// Validate HF config against the reference contract (no values are taken from
// it — the reference is authoritative — but mismatches are fatal).
// it (the reference is authoritative), but mismatches are fatal).
if *hfConfig != "" {
if err := validateConfig(*hfConfig, kvs); err != nil {
return fmt.Errorf("config validation: %w", err)
+2 -2
View File
@@ -26,8 +26,8 @@ func TestF32BitsToF16(t *testing.T) {
{"neginf", 0xff800000, 0xfc00},
{"qnan", 0x7fc00000, 0x7e00},
{"snan", 0x7f800001, 0x7e00},
{"min_normal", 0x38800000, 0x0400}, // 2^-14
{"min_subnormal", 0x33800000, 0x0001}, // 2^-24
{"min_normal", 0x38800000, 0x0400}, // 2^-14
{"min_subnormal", 0x33800000, 0x0001}, // 2^-24
{"tie_to_even_zero", 0x33000000, 0x0000}, // 2^-25 (half of min subnormal)
{"tie_to_even_two", 0x33c00000, 0x0002}, // 3 * 2^-25
{"one_tenth", 0x3dcccccd, 0x2e66},
+19 -19
View File
@@ -1,6 +1,6 @@
package main
// hfconfig.go — parse and validate the HF config/tokenizer against the
// hfconfig.go parses and validates the HF config/tokenizer against the
// reference GGUF metadata. The reference remains authoritative for values; this
// only proves the reference is consistent with the published HF files and that
// we parsed them (per the task), it does not source any GGUF value from them.
@@ -14,23 +14,23 @@ import (
)
type hfConfigFile struct {
ModelType string `json:"model_type"`
HiddenSize int64 `json:"hidden_size"`
NumHiddenLayers int64 `json:"num_hidden_layers"`
NumAttentionHeads int64 `json:"num_attention_heads"`
NumKeyValueHeads int64 `json:"num_key_value_heads"`
HeadDim int64 `json:"head_dim"`
IntermediateSize int64 `json:"intermediate_size"`
VocabSize int64 `json:"vocab_size"`
MaxPositionEmbeddings int64 `json:"max_position_embeddings"`
ModelType string `json:"model_type"`
HiddenSize int64 `json:"hidden_size"`
NumHiddenLayers int64 `json:"num_hidden_layers"`
NumAttentionHeads int64 `json:"num_attention_heads"`
NumKeyValueHeads int64 `json:"num_key_value_heads"`
HeadDim int64 `json:"head_dim"`
IntermediateSize int64 `json:"intermediate_size"`
VocabSize int64 `json:"vocab_size"`
MaxPositionEmbeddings int64 `json:"max_position_embeddings"`
RopeTheta float64 `json:"rope_theta"`
RMSNormEps float64 `json:"rms_norm_eps"`
TieWordEmbeddings bool `json:"tie_word_embeddings"`
QKNorm bool `json:"qk_norm"`
QKNormGain bool `json:"qk_norm_gain"`
EmbeddingNorm bool `json:"embedding_norm"`
SlidingWindow int64 `json:"sliding_window"`
SWAFullEvery int64 `json:"swa_full_every"`
TieWordEmbeddings bool `json:"tie_word_embeddings"`
QKNorm bool `json:"qk_norm"`
QKNormGain bool `json:"qk_norm_gain"`
EmbeddingNorm bool `json:"embedding_norm"`
SlidingWindow int64 `json:"sliding_window"`
SWAFullEvery int64 `json:"swa_full_every"`
}
func findKV(kvs []rawKV, key string) (rawKV, bool) {
@@ -100,9 +100,9 @@ func validateConfig(path string, kvs []rawKV) error {
}
type tokConfigFile struct {
EOSToken string `json:"eos_token"`
ModelMaxLen int64 `json:"model_max_length"`
ChatTemplate string `json:"chat_template"`
EOSToken string `json:"eos_token"`
ModelMaxLen int64 `json:"model_max_length"`
ChatTemplate string `json:"chat_template"`
AddedDecoders map[string]struct {
Content string `json:"content"`
} `json:"added_tokens_decoder"`
+1 -1
View File
@@ -35,7 +35,7 @@ func main() {
}
// ---------------------------------------------------------------------------
// Manifest types — the reproduction contract.
// Manifest types for the reproduction contract.
// ---------------------------------------------------------------------------
type manifest struct {
+3 -3
View File
@@ -90,9 +90,9 @@ func (t ggufBType) size() int {
type rawKV struct {
Key string
BType ggufBType
Int int64 // integer/bool scalars
Float float64 // float scalars
Str string // string scalars
Int int64 // integer/bool scalars
Float float64 // float scalars
Str string // string scalars
ElemType ggufBType // arrays: element type
Count uint64 // arrays: element count
Raw []byte // arrays: concatenated element bytes (excludes elem_type+count)
+1 -1
View File
@@ -1,6 +1,6 @@
package main
// safetensors.go — minimal read-only HuggingFace safetensors index.
// safetensors.go reads a minimal, read-only HuggingFace safetensors index.
//
// Format: [u64 LE header_len][header_len bytes of JSON][tensor data ...].
// The JSON header maps tensor name -> {dtype, shape, data_offsets:[start,end]}
+1 -1
View File
@@ -1,6 +1,6 @@
package main
// verify.go — prove the emitted GGUF equals the reference: identical metadata
// verify.go proves the emitted GGUF equals the reference: identical metadata
// (keys, types, values; verified on raw wire bytes) and identical tensor set,
// shapes, ggml types and per-tensor payload bytes.