简体中文 | English
Audience: people who want to help keep running the RNS-CKKS fully-encrypted inference chain of Qwen3-VL-2B. This document only covers "how to keep computing", not the theory (the theory document lives in the main project at
资料/FHE同态加密推理技术介绍.mdand is not shipped with this repository). For the tool inventory and per-parameter usage see05_Tool_Reference_EN.md. Version: 2026-09-14 · Author's self-tested environment: AMD Ryzen 7 9800X3D (8C/16T) · Windows 11 24H2 · MinGW-W64 gcc 16.1.0
| Item | Content |
|---|---|
| Goal | On a single machine, CPU-only (no GPU), pure C11, zero third-party dependencies: run all 28 Transformer layers of Qwen3-VL-2B entirely on ciphertexts and output logits |
| Where we are now | Layers 0–4 (5 layers) are done: each layer's lay hop is RESULT=PASS, each layer's boot refresh hop is BOOT=PASS (this round was a rerun from the seed, see §8) |
| What is still missing | Layers 5–25 on the chain (the relay chain stops at u4r112). The tail 26–27 + fin has already been run through separately, as another branch (plaintext seed into L26, bypassing layers 5–25), see §8.1 |
| Per-layer cost | lay ≈ 33 min + boot ≈ 72–81 min ≈ 1.8 h/layer (measured on 9800X3D with 4 threads) |
| Why a relay is needed | The whole chain on one machine is ≈50 h of CPU-only compute; splitting it into "a few layers per person" gets it done within days |
| Per-layer hand-off artifact | The layer's output is 8 ciphertext files u{L}r112_{t}_{h}.ct (≈28 MB) + sk.bin; whoever receives them can continue from L+1 |
| Judging criteria | For lay, look for RESULT=PASS at the end of the log; for boot, look for BOOT=PASS. Stage-level FAIL does not count, see §7 |
We claim exactly one thing below; nothing else is treated as an established conclusion:
| Claim | Status |
|---|---|
✅ The first 5 layers (layers 0–4) are reproducible and independently verifiable on CPU only: layer by layer the lay criterion is RESULT=PASS and the boot refresh criterion is BOOT=PASS (each refresh returns to the full chain, out np=2083); the same source code is bit-identical coefficient by coefficient on x86-64 and aarch64 |
This repository is responsible for it |
✅ The tail (layers 26–27 + fin) also computes correctly: encrypted directly from a plaintext seed into L26 (bypassing layers 5–25); all five steps PASS, and fin's logits top1 matches the reference 4/4 |
This repository is responsible for it (see §8.1) |
| ❌ "The full 28-layer chain runs" | Not claimed — that is the community relay goal, not an existing result |
| ❌ Cryptographic security / performance superiority | Not claimed (see §9) |
So the remit of this guide is: to explain the "the first 5 layers are reproducible and verifiable" claim thoroughly and make it solid, and to hand out layers 5–27 as an open relay item. Any results from layer 6 onward that someone produces after taking over are their contribution and must not be backfilled into this project's existing conclusions.
- x86-64 or ARM64 (RK3588 has been verified to be bit-identical coefficient by coefficient with x86, difference 0 / ~1e-16);
- ≥4 logical cores recommended (default
T23_NT=4; 8 threads give higher absolute throughput but also higher power draw); - ≥8 GB of available memory (
laystage measured working set ≈1.0 GB; thebootstage's modulus chainnp=2100is larger, so leave more headroom); - Disk: data package ≈7 GB + model 4.26 GB (only if you generate the data yourself) + ≈28 MB of artifacts per layer; leave ≥20 GB free.
- A
gccthat can compile C11 + OpenMP (MinGW-W64 or Linux gcc both work); the whole chain only depends onlibc + libm; - No cryptographic library needs to be installed.
The source code does not include the weights or the reference data. Two routes, pick either:
| Route | How | Cost |
|---|---|---|
| A. Generate it yourself (fully reproducible) | Download Modl/Qwen3-VL-2B-Instruct/ (model.safetensors 4.26 GB + vocab.json) and install numpy + safetensors, then run, from the repository root, the steps in the main repo's tools/preproc/README.md §3: _export_weights.py → _embed4.py → _silu_mode.py → _full_layers.py → _tail_ref.py → _sin_fit.py / _recip_fit.py → _adap_scale.py |
one Python preprocessing pass |
| B. Ask the author for a ready-made package | leave a note in an Issue (we reply with a download link, never as an attachment — 7 GB is far past any attachment cap) | transferring 7 GB |
⚠️ The order is not optional — two files fail silently when missing:_embed4.py(lay0's plaintext input) and_silu_mode.py(the per-layer SiLU path switch) produce files the driver needs but does not complain about — without them the driver still printsRESULT=PASSwhile the numbers are wrong. Measured: layer 0 withsilu0.binmissing givesmax|err|= 4.2e-2 – 8.5e-2 (above the 3e-2 tolerance); with it, back to 1.27e-3. So a criteria log is not a substitute for numeric re-checking: after every layer, re-checkmax|err|independently withtools/relay/verify_layer.ps1(orverify_layer.exe) — see §5.6 and §7.
The one former gap —
l0/embed4.bin— is now closed. It is lay0's plaintext input (the embedding lookup for the 4 tokens) and used to have no generator, so it could only be shipped with the data package. It is now produced bytools/preproc/_embed4.py, which defaults to--ids 100,101,102,103(exactly the four ids the published data uses); the output is 32768 B and byte-identical to theembed4.binin the data package (SHA256 prefixc945c8fe92cc0595, verified by re-running it).
sk.bincan also be generated by the driver itself: create.tmp_tok/chain/first, then runT23_PHASE=l0 T23_KEYONLY=1 .tmp_tok/t23lay.exeand look forKEYONLY: sk.bin saved. That directory step is not optional — the driver uses a barefopenand never creates directories; without it you get[FAIL] sk save(and the same applies when running layers:[FAIL] ct save).
.tmp_tok/
├── chain/sk.bin # secret key (2 KB) — unique to the whole chain, must come from the same source as the weights/reference
├── chain/u0r112_{t}_{h}.ct # starting ciphertext seed (8 files), or your u{L-1}r112 when you take over
├── tail/w0 .. w27/ # per-layer weights (the .bin files read by gcc, 9 per layer)
├── tail/l{L}_*.bin # per-layer plaintext reference (for verification)
├── tail/scale{L}.bin # per-layer adaptive scale (residual folding divisor)
├── tail/silu{L}.bin # SiLU approximation mode switch
├── tail/ln{L}_e1.bin / ln{L}_p1.bin / m2c{L}_e.bin / m2c{L}_p.bin
├── l0/ l1w/ l1r/ # weights and references specific to layer0 / layer1
└── ...
Data self-check (confirm before running):
.tmp_tok/chain/sk.bin, the input ciphertexts for your leg,tail/w{L}/(9 files) andtail/scale{L}.binmust all exist. If one is missing, the run will error out and exit straight away during the load stage.Also check these two separately — they do not error out when missing, but the numbers come out wrong:
tail/silu{L}.bin(one per layer, 8 B; produced by_silu_mode.pyin §1.3) andl0/embed4.bin(used bylay0only; produced by_embed4.py). Do not start after checking only the four items above.
(The tree above is the data package's layout relative to the repository root; the engine hard-codes the .tmp_tok/ prefix.)
This repository's Releases carry prebuilt Windows x86-64 binaries (t23lay.exe / t23boot.exe /
verify_layer.exe, statically linked, no MinGW needed). Unzip, put them in place, and you can
skip the build in §2:
cd <repo root>
New-Item -ItemType Directory -Force .tmp_tok, .tmp_tok\chain | Out-Null
Copy-Item <extracted dir>\tools-win-x64\*.exe .tmp_tok\Once the data is in place, smoke-test with it (seconds, no layer run needed):
.tmp_tok\verify_layer.exe -L 4 -Ct u4 # expect RESULT: VERIFY_PASS (8/8) max_err=1.3823e-02Details in data/README.en.md §3 ⓪. On non-Windows platforms
(Linux / macOS / aarch64) build it yourself per §2.
Source locations: src/core/{vllm_ntt.c, vllm_ckks.c, vllm_tp.c} + the drivers tools/drivers/{t23_m3p.c, t23_chain.c} (both ship with the repository; .tmp_tok/ holds only data and artifacts).
Create the working directory first:
mkdir -p .tmp_tok .tmp_tok/chain .tmp_tok/relay(PowerShell:New-Item -ItemType Directory -Force .tmp_tok, .tmp_tok\chain, .tmp_tok\relay | Out-Null)..tmp_tok/chain/is where ciphertexts are written and the drivers never create it (without it you get[FAIL] ct save/[FAIL] sk save);.tmp_tok/relay/holds the logs of the manual runs in §4.2/§4.3 and redirection does not create directories either (see the measured note in §4.2); both-otargets also write into.tmp_tok/, and if the directory is missing the linker fails immediately withcannot open output file .tmp_tok/t23lay.exe. Naming the outputt23layis enough — MinGW gcc appends.exeitself. (If you use the one-key script in §4.4 you can skip this — the script prepares the directories it needs itself.)
# lay hop (intra-layer forward): modulus chain of 112 primes
gcc -O2 -fopenmp -Wno-implicit-function-declaration \
-I include -I include/core -I include/common \
-DCKKS_N=2048 -DCKKS_NPRIMES=112 -DBB=32 -DGG=32 \
src/core/vllm_ntt.c src/core/vllm_ckks.c src/core/vllm_tp.c tools/drivers/t23_m3p.c \
-o .tmp_tok/t23lay -lm
# boot hop (noise refresh): modulus chain of 2100 primes
gcc -O2 -fopenmp -Wno-implicit-function-declaration '-Wl,--stack,33554432' \
-I include -I include/core -I include/common \
-DCKKS_N=2048 -DCKKS_NPRIMES=2100 -DBB=32 -DGG=32 \
src/core/vllm_ntt.c src/core/vllm_ckks.c src/core/vllm_tp.c tools/drivers/t23_chain.c \
-o .tmp_tok/t23boot -lmCompile-time traps (we hit every one of them)
-Imust be given forinclude,include/coreandinclude/commonat the same time (vllm_tp.creferencesvllm_platform.hincommon/); passing onlyinclude/coregivesvllm_platform.h: No such file.- On Windows, the comma inside
-Wl,--stack,...is treated by PowerShell as an argument separator → the whole thing must be quoted as'-Wl,--stack,33554432', or run it throughcmd /c. bootusesnp=2100, and the MinGW default stack overflows (0xC00000FD) → add-Wl,--stack,33554432.
Every layer L requires two hops, both landing in .tmp_tok/chain/; each hop produces 8 .ct files (4 tokens × 2 ciphertext components):
seed : chain/u0r112_{t}_{h}.ct (starting seed, produced by encrypting the plaintext starting point and refreshing it)
lay{L} (forward) : u{L-1}r112_{t}_{h}.ct ──▶ u{L}_{t}_{h}.ct criterion RESULT=PASS
boot{L} (refresh) : u{L}_{t}_{h}.ct ──▶ u{L}r112_{t}_{h}.ct criterion BOOT=PASS
laycryptographically finishes this layer's RMSNorm → QKV → Attention → O → SwiGLU MLP → residual, and outputs the ciphertextu{L}whose "noise is nearly used up";bootperforms one full bootstrapping (ModRaise → CoeffToSlot → EvalMod/sin folding → SlotToCoeff), refreshing the noise budget back to the full chain, and outputsu{L}r112, which the next layer can keep consuming on the deep chain.
Layer numbers start at 0: the records currently held in this repository are lay0..lay4 (layers 0..4), and the hand-off point is
u4r112. The person taking over starts at lay5.
| Variable | Role | Example value |
|---|---|---|
T23_PHASE |
Phase | lay / boot / fin |
T23_LAY |
Intermediate layer number (lay only) |
5 |
T23_BI / T23_BO |
boot input/output prefixes (boot only) |
u5 / u5r |
T23_E2EMODE |
End-to-end acceptance mode (ignores stage-level FAILs caused by reference deviation) | 1 |
T23_NT |
Thread count | 4 |
cd <repo root>
# the log directory must exist first: cmd's > redirection, like the drivers, never creates directories
New-Item -ItemType Directory -Force .tmp_tok\relay | Out-Null
# ---- layer 5's lay hop ----
$env:T23_PHASE="lay"; $env:T23_LAY="5"; $env:T23_E2EMODE="1"; $env:T23_NT="4"
# use cmd for redirection: guarantees the log lands on disk byte for byte, not transcoded by PowerShell
cmd /c "`.tmp_tok\t23lay.exe` > `.tmp_tok\relay\lay5.out` 2> `.tmp_tok\relay\lay5.err`"
Select-String -Path .tmp_tok\relay\lay5.out -Pattern 'RESULT=PASS' # a match = pass
# ---- layer 5's boot hop (refresh) ----
$env:T23_PHASE="boot"; $env:T23_BI="u5"; $env:T23_BO="u5r"
cmd /c "`.tmp_tok\t23boot.exe` > `.tmp_tok\relay\boot5.out` 2> `.tmp_tok\relay\boot5.err`"
Select-String -Path .tmp_tok\relay\boot5.out -Pattern 'BOOT=PASS' # a match = pass
⚠️ If.tmp_tok\relaydoes not exist the redirection above fails silently (measured, not theoretical): cmd only printsThe system cannot find the path specified.to stderr, no log file is created at all, and%ERRORLEVEL%still stays 0; only the followingSelect-Stringthen reports a missing path — which is easily misread as "this layer crashed". Likewise, if.tmp_tok/chain/is missing the driver reports[FAIL] ct save(already noted in §2). Create both directories up front.
cd <repo root> # must be the repo root: driver paths are hard-coded to .tmp_tok/...
mkdir -p .tmp_tok/chain .tmp_tok/relay # the log directory is not created for you either
T23_PHASE=lay T23_LAY=5 T23_E2EMODE=1 T23_NT=4 .tmp_tok/t23lay > .tmp_tok/relay/lay5.out 2> .tmp_tok/relay/lay5.err
T23_PHASE=boot T23_BI=u5 T23_BO=u5r T23_NT=4 .tmp_tok/t23boot > .tmp_tok/relay/boot5.out 2> .tmp_tok/relay/boot5.err
grep -q RESULT=PASS .tmp_tok/relay/lay5.out && echo "lay5 PASS"
grep -q BOOT=PASS .tmp_tok/relay/boot5.out && echo "boot5 PASS"The binaries live under
.tmp_tok/(the-otargets of §2), not at./t23layin the repo root; and do notcd .tmp_tokbefore running — the driver writes its data paths as.tmp_tok/..., so a different working directory makes it look for.tmp_tok/.tmp_tok/...and every load fails.
The repository ships tools/relay/rerun5_lay_boot.ps1, which can take a layer range, build automatically, resume from where it stopped, and write timing.tsv:
# run layers 5..9 (lay+boot for each); layers whose artifacts already exist are skipped automatically
powershell -NoProfile -ExecutionPolicy Bypass -File tools/relay\rerun5_lay_boot.ps1 -From 5 -To 9The logs and summary land in .tmp_tok/relay_rerun/: master.log (timeline), timing.tsv (per-layer wall clock/status), lay{L}.out/boot{L}.out (raw logs), meta.txt (machine metadata).
Log-directory convention: the one-shot script writes
.tmp_tok/relay_rerun/, while the manual runs in §4.2/§4.3 write.tmp_tok/relay/— the two are not interchangeable, so do not look/grepin the wrong one.
The script follows three engineering conventions that you must observe too, and that you should copy if you write your own script: ① if a
.ps1contains Chinese comments, it must be saved as UTF-8 with BOM (otherwise PowerShell 5.1 reads it as ANSI and reports "unexpected token }"); ② redirect subprocess output withcmd /c "... > out 2> err", never with a PowerShell pipe (which turns UTF-8 Chinese into mojibake); ③ the criterion must be taken from the layer output, see §7.
Once these 8 files are all present under .tmp_tok/chain/, the layer's refresh is considered complete:
u{L}r112_0_0.ct u{L}r112_0_1.ct
u{L}r112_1_0.ct u{L}r112_1_1.ct
u{L}r112_2_0.ct u{L}r112_2_1.ct
u{L}r112_3_0.ct u{L}r112_3_1.ct
Pack the things below into one zip, host it on your own Release or cloud drive, and reply in the claim issue with the download link plus that zip's SHA256:
| File | Description | Size |
|---|---|---|
u{L}r112_{t}_{h}.ct ×8 |
Core: the input ciphertexts for the next leg | ≈28 MB |
sk.bin |
Secret key (identical across the whole chain; omit it if the other side already has it) | 2 KB |
lay{L}.out boot{L}.out |
Criterion evidence (containing RESULT=PASS / BOOT=PASS); manual runs keep them in .tmp_tok/relay/, the one-shot script in .tmp_tok/relay_rerun/ |
a few hundred KB |
timing.tsv meta.txt |
Wall clock and machine metadata | KB-scale |
Naming suggestion: relay_L{L}_{yourname}_{date}.zip (in practice just generate it in one command with pack_relay.ps1, see §5.5).
⚠️ Do not attach the zip itself to an issue or discussion. Two hard reasons:
- It will not fit: GitHub caps attachments in issues / PRs / discussions at 10 MB for images and GIFs and 25 MB for anything else, while a one-layer package is ≈28 MB;
- Even if it fit, it should not be trusted: every piece of evidence in this chain rests on byte-level hashes (
manifest.sha256,verify_relay.ps1,verify_layer -BitA/-BitB), and intermediaries such as email re-encode or normalise bytes — after which everymanifest.sha256entry mismatches and you cannot tell "the package was tampered with" from "the relay changed one byte".A link is byte-faithful: the package that
verify_relay.ps1downloads must match the SHA256 posted in the claim issue, which also means the contributor cannot swap it afterwards.
The next person drops
u{L}r112_*.ctback into.tmp_tok/chain/and continues with-From $($L+1).
| Leg | Layer range | Estimated single-machine time |
|---|---|---|
| ✅ Done | 0 – 4 | ≈9 h (measured 8.94 h in this round's rerun, see §8) |
| Leg 2 | 5 – 7 | ≈5.5 h |
| Leg 3 | 8 – 10 | ≈5.5 h |
| … | … | … |
| Last leg | 25 – 27 + fin |
≈6 h |
You can also claim just 1 layer (≈1.8 h); the hand-off bundle is just as small, and any amount of compute helps.
5.4 How others independently verify these 5 layers (the concrete practice of "technically verifiable")
You do not need to trust our verbal conclusion; run the three levels of checks below — failing any single one of them means something is wrong:
Check ①: criterion self-check (seconds)
# the lay / boot criterion must hit for every layer; any layer without a match is a real failure
foreach ($L in 0..4) {
"lay$L : " + ((Select-String .tmp_tok\relay_rerun\lay$L.out -Pattern 'RESULT=PASS') -ne $null)
"boot$L : " + ((Select-String .tmp_tok\relay_rerun\boot$L.out -Pattern 'BOOT=PASS') -ne $null)
}
# whether the refresh really returns to the full chain (should be 2083 for every token/component of every layer)
Select-String .tmp_tok\relay_rerun\boot*.out -Pattern 'out np=2083' | Measure-ObjectCheck ②: hand-off bundle integrity and criteria (pack + verify loop, see §5.5)
# pack your layer range into an uploadable bundle (auto-generates the in-bundle SHA256 manifest and README)
powershell -NoProfile -ExecutionPolicy Bypass -File tools/relay\pack_relay.ps1 -From 0 -To 4 -Id <yourname>
# after anyone receives the zip, one command checks integrity + criteria + structure
powershell -NoProfile -ExecutionPolicy Bypass -File tools/relay\verify_relay.ps1 -Zip <package path>Hand-off artifacts are deterministic products: same source data + same source binaries + same layer order → byte-identical. A matching manifest = what you got is the same artifact, not replaced or truncated.
Check ③: cross-architecture bit-level consistency (optional, strongest)
On another machine with a different ISA (e.g. ARM64/RK3588), rerun lay0,
decode u0_{t}_{h}.ct with the same sk.bin, and the decrypted values should be coefficient-for-coefficient identical to the x86 result (measured difference 0 / ~1e-16).
If this check passes, it shows the conclusion is independent of floating-point rounding order and of the hardware.
All three pass ⇒ "the first 5 layers are reproducible and technically verifiable" holds; checks ① and ② can be done by anyone on their own machine.
Relay participant: pack (the bundle automatically contains manifest.sha256 and README.txt)
# after finishing L5..7, fill in your own layer range and ID
powershell -NoProfile -ExecutionPolicy Bypass -File tools/relay\pack_relay.ps1 -From 5 -To 7 -Id yourname
# output: .tmp_tok\relay_out\relay_L5-7_yourname_<timestamp>.zipBundle structure:
relay_L5-7_yourname_<ts>.zip
└── relay_L5-7_yourname_<ts>/
├── chain/ u{L}r112_{t}_{h}.ct ×8 × per layer ← core hand-off artifact
├── logs/ lay{L}.out/.err boot{L}.out/.err ← criterion evidence
├── timing.tsv meta.txt master.log
├── sk.bin ← secret key shared by the whole chain
├── manifest.sha256 ← SHA256 of every file in the bundle
└── README.txt ← layer range / claim boundaries / verification and continuation instructions
Size reference: 1 layer ≈28 MB, 4 layers ≈111 MB (measured).
sk.binis the single secret key of the whole chain;-NoKeyexcludes it (the receiver must then already hold the samesk.bin).
Any third party: verify (no need to trust the packer)
powershell -NoProfile -ExecutionPolicy Bypass -File tools/relay\verify_relay.ps1 -Zip <package path>
# exit code 0 = everything passed; 1 = at least one check failedThe script performs three kinds of check:
| # | Check | Description |
|---|---|---|
| ① | Integrity | Every file in the bundle matches manifest.sha256 byte for byte; and it also checks in reverse "whether any smuggled file is not covered by the manifest" |
| ② | Criteria | logs/lay*.out contains RESULT=PASS; logs/boot*.out contains BOOT=PASS and out np=2083 exactly 8 times per layer (4 tokens × 2 components) |
| ③ | Structure | 8 u{L}r112_*.ct per layer, each file ≈3.5 MB (np=112) |
Self-test sample from this repository (packing the real layers 0–3 data of _bak_20260914 and then verifying it):
[1] integrity [PASS] all files match manifest -- checked=51 mismatched=0
[PASS] no unlisted files (no smuggling) -- 0 extra
[2] criteria [PASS] lay criterion lay0..lay3
[PASS] boot criterion boot0..boot3 -- BOOT=PASS=True refreshes=8
[3] structure [PASS] ciphertexts present -- count=32 [PASS] each ct ~3.5MB
RESULT: ALL_CHECKS_PASS
Trust model (must be stated clearly; do not overclaim)
- ✅ The SHA256 manifest can prove: the bundle was not tampered with, was not truncated, and carries no extra smuggled files; the criterion logs and the ciphertexts are artifacts from the same batch.
⚠️ It cannot prove "the computation itself is correct" — the criterion logs could in theory be forged.- 🔒 The only way to prevent forgery is cross-recomputation: because the pipeline is deterministic, if two people independently rerun the same layer, the SHA256 of the produced
u{L}r112_*.ctmust be byte-identical. So the real verification stance is:- run ①②③ first (seconds; filters out corrupt/wrong bundles);
- for key layers (e.g. L10, L27) find a second person to recompute independently, and compare the SHA256 of the hand-off artifacts;
- if the two agree ⇒ that layer's result is trustworthy, and it does not depend on anyone's "statement".
This is the extra value of a "relay" over "running it all on one machine": every layer can naturally be reproduced independently and falsified by cross-checking.
§5.5 only answered "was the bundle modified"; it did not answer "is the computation right". verify_layer fills that gap.
Principle: decrypt the layer's ciphertext with sk.bin → decode into real slots → compare element by element against the reference tail/l{L}_u2_ref.bin produced by the float32 plaintext forward pass, and compute max|err|.
This is a code path independent of the driver's PASS logic (it implements decryption/decoding/comparison itself), so it can serve as third-party review.
Usage
# ① accuracy: verify the lay output u{L} (default tolerance 3e-2)
powershell -NoProfile -ExecutionPolicy Bypass -File tools/relay\verify_layer.ps1 -From 0 -To 4
# ② accuracy: verify the boot refresh output u{L}r112
powershell -NoProfile -ExecutionPolicy Bypass -File tools/relay\verify_layer.ps1 -From 0 -To 3 -Mode boot
# ③ bit-level cross-recomputation: two people run the same layer independently and compare byte for byte (anti-forgery)
.tmp_tok\verify_layer.exe -BitA u5 -BitB u5_other -L 5
# the underlying program can also be invoked for a single item
.tmp_tok\verify_layer.exe -L 4 -Dir .tmp_tok/chain -Sk .tmp_tok/chain/sk.bin -Tol 3e-2Dependency: the plaintext reference tail/l{L}_u2_ref.bin from the data package (8192 float32 = 4 tokens × 2048 dimensions).
Calibration of the tool itself (validated against the previous round's real data; matches layer by layer — the difference against this round's rerun in §8 is in the 4th–5th significant digit, which is normal across rounds)
| Layer | verify_layer measured max|err| |
Corresponding value in the driver log .tmp_tok/relay/lay{L}.out |
Match |
|---|---|---|---|
| 0 | 1.2712e-03 | 1.271e-03 | ✅ |
| 1 | 3.1467e-03 | 3.147e-03 | ✅ |
| 2 | 2.8366e-02 | 2.837e-02 | ✅ |
| 3 | 1.2284e-02 | 1.228e-02 | ✅ |
| 4 | 1.3820e-02 | 1.382e-02 | ✅ |
All 5 layers agree (4–5 significant digits), showing the tool is equivalent to the driver; conclusion: the first 5 layers are
ALL_LAYERS_VERIFY_PASS.
- Layer 2's
t1 h1component hasmax|err| = 2.837e-02against a tolerance of 3e-2 — only 5.8% margin left; - Layer 2 also has two further points of the same order of magnitude, 1.93e-02 / 1.52e-02;
- Pattern: the error is systematically concentrated in the
h=1component (the second half of the dimensions, 1024–2047), whileh=0is usually an order of magnitude smaller. - Implication: the first 5 layers pass, but the error budget is not generous; if deeper layers keep accumulating, the tolerance may be breached — this is exactly the metric to watch closely once the relay reaches layer 6 and beyond (record the
max|err|trend ofh=1for every layer).
.tmp_tok/
├── chain/
│ ├── sk.bin [input] secret key
│ ├── u{L-1}r112_{t}_{h}.ct [input] the input ciphertexts for your leg
│ ├── u{L}_{t}_{h}.ct [output] lay output
│ └── u{L}r112_{t}_{h}.ct [output] boot output (= what you hand to the next leg)
├── tail/w{L}/ [input] layer L weights
├── tail/l{L}_*.bin [input] layer L plaintext reference
├── relay_rerun/ [output] logs / timing.tsv / meta.txt
└── t23lay(.exe) t23boot(.exe) [output] binaries
Once it is running, the log will show PASS together with a large number of FAILs, for example:
[A:xnorm t0] max|err|=2.404e-01 FAIL
[C:attn t0] max|err|=4.941e-01 FAIL
[U2 t0 h0] max|err|=3.980e-03 PASS [E2E:ref-deviate-ok]
RESULT=PASS (0)
This is not a contradiction, it is a matter of convention — the reasoning and the quantified basis are in "Why a stage can FAIL while the final verdict is still PASS" at the end of this section. The project's convention is:
- The layer-output criterion is the criterion: whether a layer passes is decided only by the
RESULT=PASS(lay) orBOOT=PASS(boot) at the end of the log:grep RESULT=PASS .tmp_tok/relay/lay{L}.outhas a match (with the §4.4 one-shot script the directory is.tmp_tok/relay_rerun/);grep BOOT=PASS .tmp_tok/relay/boot{L}.outhas a match (and the log showsout np=2083, proving it really did refresh back to the full chain).
- There is also a
foldcount that changes with the layer number (lay0/1 = 16, lay2 = 128, lay3/4 = 256); it moves in step with the number of stage-level FAIL lines — this is part of the reference construction, not an error. - But
RESULT=PASSonly means "the flow completed with no hard error" — it does not mean the numbers passed. The relay script setsT23_E2EMODE=1, which downgrades intermediate deviations to "reported only, not counted infails" (tagged[E2E:ref-deviate-ok]in the log) — so a shallow layer running the wrong SiLU path still prints PASS (measured:[U2 t3 h0] max|err|=4.460e-02 FAIL [E2E:ref-deviate-ok]followed byRESULT=PASS (0)). Every layer must be independently re-checked withverify_layer(§5.6) formax|err|before it counts.
In one sentence: only the
RESULT=PASS/BOOT=PASSat the end of a line counts; ignore the stage-level FAILs. Conversely, if the layer-output criterion fails, that is a real problem — please post the full log back to the Issue.
Why a stage can FAIL while the final verdict is still PASS — the two are not measuring the same thing:
| Step | Explanation |
|---|---|
| Why the stage FAILs | L26/27 use a real C-fold (g_causal + real C folding); the intermediate C/D/U2 values deviating from the true values is expected, not a defect |
| Magnitude of the expected deviation | Pre-quantified by the planning script _cfold_plan.py: |Δao| ~ 1.4 / 0.8 — the same order as the observed [C:attn] 8.5~12 and [D:o] 14~29 |
| Why the final verdict is still PASS | The same plan defines the normalized criterion: E2E logits drift 0.14 < margin 2.03 — the internal deviation shrinks to 0.14 at the logits, while the top1 decision margin is 2.03 |
| Where acceptance moves to | In this mode C/D/U2 are report-only (not counted in fails); acceptance is transferred to the E2E logits gate |
One line: a stage FAIL is a difference in an intermediate quantity's convention (expected and pre-quantified); the final PASS is the end-to-end logits-gate verdict — they are not the same ruler, and reading them together creates an illusion of self-contradiction.
Source of authority: the E2E-mode comment in the driver source tools/drivers/t23_m3p.c (around lines 1678-1680).
- Machine: AMD Ryzen 7 9800X3D (8C/16T) · Windows 11 24H2 · MinGW-W64 gcc 16.1.0 ·
T23_NT=4 - Reference (previous round,
relay/master.log) hop-by-hop wall clock:
| Hop | Wall clock | Hop | Wall clock |
|---|---|---|---|
| lay0 | 1992 s (33.2 min) | boot0 | 3873 s (64.6 min) |
| lay1 | 1926 s (32.1 min) | boot1 | 3890 s (64.8 min) |
| lay2 | 2039 s (34.0 min) | boot2 | 3978 s (66.3 min) |
| lay3 | 2025 s (33.8 min) | boot3 | 4337 s (72.3 min) |
| lay4 | 1857 s (31.0 min) | — | — |
- This round's rerun log:
.tmp_tok/relay_gh/master.logandtiming.tsv(complete, all 10 hops PASS).
| Layer | lay | boot | Hand-off artifact |
|---|---|---|---|
| 0 | 1962 s (32.7 min) | 4377 s (73.0 min) | u0r112 |
| 1 | 2008 s (33.5 min) | 4891 s (81.5 min) | u1r112 |
| 2 | 1942 s (32.4 min) | 4350 s (72.5 min) | u2r112 |
| 3 | 1949 s (32.5 min) | 4339 s (72.3 min) | u3r112 |
| 4 | 1972 s (32.9 min) | 4392 s (73.2 min) | u4r112 ← the next leg starts here |
| Total | 9833 s | 22349 s | 10 hops 32182 s ≈ 8.94 h |
The table is generated directly from
relay_rerun/timing.tsv; for the full hop-by-hop record (including thebootstage profile and the per-layermax|err|) see03_Measured_Data_Appendix_EN.md§2–§4.
This is not a continuation of the chain — it encrypts the plaintext seed tail/l26_u1.bin directly into
L26, bypassing layers 5–25, in order to validate "the last two layers + the output head" on their own.
| Step | Wall | Result |
|---|---|---|
t26 |
2079 s | RESULT=PASS, U2 8/8 PASS (4.471e-03 ~ 2.299e-02) |
boot26 |
4469 s | BOOT=PASS, 8× out np=2083 |
t27 |
2068 s | RESULT=PASS, U2 2/8 over threshold (3.822e-02 / 7.969e-02) |
boot27 |
4489 s | BOOT=PASS, 8× out np=2083 |
fin |
3178 s | RESULT=PASS (0; ref-deviate 8), top1 4/4 correct (all 120555) |
| Total | 16284 s ≈ 4.52 h | — |
Archiving and independent re-check: see 03_Measured_Data_Appendix_EN.md §9.2 and its "Tail measurement" section.
These are facts that were written into the paper draft from the very beginning and that cannot be concealed:
- No claim of cryptographic security strength: the current parameters (
n=2048, an extremely long modulus chain) are at the mechanism/correctness verification level, far below 128-bit; upgrading to 128-bit would requiren ≥ 16384or equivalent parameters. What the relay reproduces is "can it compute correctly", not "can it resist attack". - The randomness is a prototype RNG; the interface already reserves a place for injecting a CSPRNG, but none is wired in at present.
- The weights are plaintext constants (HE plaintexts preprocessed offline); what this chain protects is the inference input and intermediate activations, not the model weights. Understand "fully encrypted" as "input and activations fully encrypted".
- Performance is not a selling point: single-token latency is hours, which suits offline batch processing / auditable private inference, not interactive generation. The cost of FHE is overhead on the order of 10^5–10^7.
- The full 28-layer chain has not yet been run to completion: every mention of "28 layers" in this document is an extrapolated ETA (≈50 h), not a measured value.
- Claim a layer range (e.g.
5–7) in the Issue, to avoid collisions; - when done, host the hand-off zip on your own Release or cloud drive and reply in the Issue with the
download link plus the zip's SHA256, attaching
timing.tsvand the criterion logs (logs are only KB-scale, so pasting them inline is fine; do not attach the zip — see §5.2); - if the layer-output criterion FAILs, do not delete the logs — post
lay{L}.out/lay{L}.errin full (keep the original bytes; do not re-save through Notepad, which re-encodes them); - once the hand-off bundle is confirmed received by the next leg, you can free your own
.ctreservation.
Verification and registry (maintainer side, publicly reproducible): anyone downloads the package from the link and runs one command
powershell -NoProfile -ExecutionPolicy Bypass -File tools/relay\verify_relay.ps1 -Zip <downloaded package>(exit code 0 = integrity + criteria + structure all pass), then verify_layer.ps1 -From L -To L for
max|err|. Results are recorded in an index table in the first post of the claim issue, one row per layer:
| layer | zip SHA256 | contributor | verifier | date | criterion | verify_layer max|err| |
notes |
|---|---|---|---|---|---|---|---|
| 5 | <64 hex> |
<ID> |
<ID> |
<date> |
RESULT=PASS / BOOT=PASS |
<value> |
— |
The registry is an endorsement, not a gate: verifiability always stays on the public side — anyone can download the same package, run the same command, and reach the same conclusion without going through us. That is exactly what §5.5 means by "depending on no one's word".
Q: lay finished without RESULT=PASS, but with CHAIN: dumped uL_*?
A: That means the dump succeeded but verification did not pass; it is a real FAIL, so post the log back to the Issue.
Q: Can I run only lay without boot?
A: No. The noise budget of lay's output u{L} is nearly exhausted, and it must be refreshed back to the full chain by boot (out np=2083) before it can be fed to the next layer; otherwise the next layer suffers noise breakdown.
Q: boot's in np= differs per layer (e.g. 15/16/20) — is that normal?
A: Yes, it is normal. That is the remainder of the modulus chain consumed by the previous lay hop, and it depends on that layer's operator depth. As long as out np=2083, the refresh succeeded.
Q: The Chinese text in the log turns into mojibake?
A: Redirect with cmd /c "... > file" (do not go through a PowerShell pipe); save .ps1 files as UTF-8 with BOM.
Q: Can it run on ARM64/RK3588? A: Yes. It has been verified to be bit-identical coefficient by coefficient with x86, so a cross-architecture relay is possible (but RK3588 is slower, about 40 min for the t0 stage; evaluate that for yourself).
Q: If I continue on a different machine, will the precision change? A: No. Bit-level consistency means the result is decoupled from the hardware rounding order and is reproducible across machines.
Every hand-off bundle is generated by pack_relay.ps1 and carries its own manifest.sha256; the credential published externally is the SHA256 of the zip itself.
After downloading, collaborators first check the zip's SHA256, then run verify_relay.ps1 for the byte-for-byte check inside the bundle.
| Hand-off bundle | Layer range | zip SHA256 |
|---|---|---|
kestrel-fhe-relay_L0-4_20260915.zip (this repo's Releases) |
0 – 4 | b3a6c7af273c664854c2bdf8af4a045776cc5c9f204db90c9b2981de719bcbf7 |
kestrel-fhe-relay_L26-27-fin_20260915.zip (this repo's Releases, tail branch) |
26 – 27 + fin |
26d8a9f09741c507e8bd0e35810e18062775071dded5bdc0653aca2122c58402 |
After finishing your leg, generate your own bundle with
tools/relay/pack_relay.ps1 -From <a> -To <b> -Id <ID>; the packing log printssha256 : ...directly — copy that value into this table (or just post it in the Issue). Full verification of the two published assets is in../data/README.en.md.
This guide is updated on a rolling basis as the chain progresses; for the latest progress see .tmp_tok/relay_rerun/master.log.