docs: rewrite in plain style (no em dashes, no emoji), tighten reports

This commit is contained in:
vad
2026-09-29 07:39:12 +00:00
parent 4fc62ef8d1
commit 6d1f79359a
11 changed files with 392 additions and 654 deletions
+118 -273
View File
@@ -1,332 +1,177 @@
# Gensyn open-1b (SFT) — local bring-up & proof of run
# Local run: open-1b on one Arc B580
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).
- Workdir: `inference/`
- Engine: the shared llama.cpp Vulkan build, with the open1b patch
- Language: Go 1.27
## TL;DR
## Summary
- 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/`).
- 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.
Reference SHA-256 (and our output — identical):
`a18530417c732bb115a3501a7e1388ea7a54bd2a96c70be571d1249aa210fbae`
---
## 1. Inputs & topology
## Inputs
| 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; produced by llama.cpp `convert_hf_to_gguf.py` + our local Open1B patch; **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 |
| --- | --- | --- |
| 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 |
GPU devices as reported by the engine (`--list-devices`):
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(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)
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` was confirmed to map to PCI `0000:07:00.0` (Arc #2) via
Vulkan2 maps to PCI `0000:07:00.0`, confirmed through
`/sys/class/drm/renderD129/device`.
---
## 2. Tool: `hf2gguf` (Go 1.27, `github.com/cymertek/go-gguf`)
## Tool
```
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
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
```
### Why a raw KV walker
`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.
`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
Commands:
```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
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
```
`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
### Tensor mapping (reference names are the contract)
The 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.
| 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 |
| 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. 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.
- 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:
## Validation
```
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
```
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.
Both files hash to
`a18530417c732bb115a3501a7e1388ea7a54bd2a96c70be571d1249aa210fbae`.
### Metadata contract (all 39 keys, all byte-identical)
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.
| 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) |
`go build`, `go vet`, and `go test -race` are clean.
Array `raw_sha256` values are in `models/open-1b-sft-f16.manifest.json`.
## Serve
### 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 \
```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 \
--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` |
| --- | --- |
| Decode | 111.7 to 117.2 tok/s |
| Prefill | about 892 tok/s |
| VRAM | about 3.6 GiB, released after stop |
### Sample outputs
Sample outputs, greedy:
```
$ curl ... /completion {"prompt":"The three primary colors are", greedy}
red, blue, and yellow.
prompt: The three primary colors are
out: 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."
prompt: Q: What is the capital of France? A:
out: Paris
```
### Stopping
Stopped with `kill` after reading the pid from `ss -ltnp`.
```
$ ss -ltnp | grep :8085 # -> pid=1378706
$ kill 1378706 # never pkill -f
```
## Caveats
Both the test server (:8085) and a diagnostic server (:8086) were stopped;
`--list-devices` returned to 10971 MiB free on Vulkan2/3.
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.
---
## 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 reference GGUF, 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
## Files
| 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) |
| --- | --- |
| `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 |
No changes were committed; everything lives under gitignored `inference/`.