Skip to content

Release v1.8.0 - #1217

Merged
JustVugg merged 152 commits into
mainfrom
dev
Aug 24, 2026
Merged

JustVugg merged 152 commits into
mainfrom
dev

Conversation

@JustVugg

@JustVugg JustVugg commented Aug 24, 2026

Copy link
Copy Markdown
Owner

60 pull requests since v1.7.0. The cycle's spine: every engine now finds a
resident expert in one probe instead of a scan, prefill stopped re-reading
bytes it already had, and the DeepSeek V4 CUDA floor dropped from Ampere to
cards from 2016. Everything risky is opt-in; every default path is covered by
token-exact oracles.

Expert lookups: O(1) on every family

DeepSeek V4: a wider GPU tier

Both DSV4_HYBRID and COLI_CUDA_MOE_DOUBLE are experimental: defaults are
byte-identical with the envs unset, and they are looking for community
numbers on real cards.

Kimi K3: checkpoints, a vendor oracle, tool calling

KV cache quantization (@ZacharyZcR)

coli tune: measured, for every engine

Fixes across the fleet

Tools, bench, CI, docs

The version bump to 1.8.0 is included (c/version.py).

monotophic and others added 30 commits August 18, 2026 17:19
…-ID intake

SUBMIT grows a key=value extension namespace past the 7th field
(logprobs=k opts a request into per-token numeric emission, capped at
run_ablate_score's top-32 ceiling; ids=1 marks the payload as ASCII
token ids parsed straight into the prompt buffer, bypassing tok_encode
with no detokenize/re-encode round trip). Old engines reject any
extended header: both legacy sscanf arms fail on the extra field and
answer ERROR 0 BAD_REQUEST. The rejection itself is safe, but the
reject path does not drain the rejected request's payload bytes, so
the engine re-reads them as protocol lines -- the pre-existing
behavior of every malformed-SUBMIT reject, not new exposure added by
the extension namespace.

Opted-in requests emit one ECHO frame per echoed prompt position
(log-softmax + unsorted top-k table, position 0 carries nan: nothing
to condition on) from a new live prefill read-out loop structurally
parallel to run_score's, and their DATA frames carry the same numeric
tail. spec_decode's emit callback now hands each token its scoring
logit row, so accepted draft tokens have no numeric gap. Requests that
do not opt in keep byte-identical frames and step()'s single
last-position lm_head cost.

The server dispatcher accepts ECHO and extended DATA without raising
(a new frame reaching an old dispatcher kills every in-flight request,
which is why this engine half lands first and stays inert until a
request opts in); the numeric fields are consumed by the server
feature half.
…e audit

golden_fixture_capture.py captures a battery of non-logprobs HTTP
responses (chat, tools, streaming, plain completions, the error cases
that must not move, /v1/models) against a running server, normalizes
ids/timestamps, and byte-diffs a pre-change capture against a
post-change one -- the mechanical form of the 'everything that worked
before still works' invariant, built before the opt-in channel lands
so it is the proof rather than reviewer inspection.

check_data_logprob_gaps.py audits a raw engine stdout transcript of an
opted-in run: every generated DATA frame must carry the numeric tail
(accepted draft tokens bypass the mux pick_tok sites, so a speculative
run is where a gap would hide), ECHO positions must be contiguous from
0, and position 0 must carry nan.
…n fields

The extended-header arm accepted "512logprobs=5" as gbytes=512 plus an
opt-in (%llu stops at the first letter and silently splits the field),
let a duplicated key win silently ("logprobs=3 logprobs=5" -> 5), and
pushed overflowing values through sscanf %llu, whose behavior on
out-of-range input is undefined (C11 7.21.6.2) -- a wrapping libc
folds 2^64+5 back to an in-range 5.

All three now reject through the existing BAD_REQUEST path: the
numeric gbytes field and the first key must be whitespace-separated,
each key may appear at most once, and the value is accumulated by hand
into a bounded int with an explicit range check, so no sscanf ever
touches it. One test case per defect added to the parse tests.
check_data_logprob_gaps.py audits that every generated DATA frame of
an opted-in request carries a numeric value; a "nan"/"inf" lp
(degenerate logits, e.g. an all -inf row after grammar masking) IS a
carried value, but the check treated it as a failure -- conflating
"the channel skipped this token" with "the value it carried is
non-finite". Non-finite values now count as PRESENT and are FLAGGED in
the report without failing the audit; whether the engine should
serialize such rows differently is a server-side (U7b) question,
deliberately not decided here. A true gap (a legacy 3-field DATA
frame) still fails exactly as before.
…ap coverage

weight_at (backend_cuda.cu) used to end in an unguarded int2 fall-through:
any format without a device decoder was silently read as 2-bit values and
returned plausible-looking numbers, while the CPU twins (qt_addrow,
qt_matvec_rows) refuse the same input loudly. Pin the supported-format
truth table in the host header as coli_cuda_weight_at_supported() so the
host gates and the device backstop agree by construction, and cover it
from both sides:

- tests/test_cuda_fmt_guard.c: host-only truth-table test, runs in the
  plain C suite with no CUDA toolchain (same arrangement as
  metal_fused_fmt_ok).
- tests/test_cuda_fmt_trap_cuda.cu: exercises the device-side __trap()
  refusal on real silicon under make cuda-test; re-execs itself per probe
  because the trap poisons the CUDA context.
repack_fp8_passthrough.py previously minted only routed experts and
o_proj; its output directory was not standalone-loadable. Extend it to
emit the full family:

- kv_b_proj, norms, and io tensors alongside experts and o_proj (the
  minted kv_b_proj shard loads today but must not be used for
  batched-path decode until fmt=8 absorb decode lands; the tool's
  docstring states this explicitly);
- aux files (config.json, tokenizer.json) copied into --outdir, closing
  the standalone-load gap;
- canonicalized __metadata__ key order, so identical inputs produce
  byte-identical shards (pre-existing safetensors nondeterminism, present
  since the tool's original fmt7 form).

Test coverage: test_fp8_repack.py grows the per-tensor and metadata
determinism cases, test_fp8_repack_full_family.py (new) covers the
full-family manifest and aux-file emission, and
test_fp8_e2e_repack_load.py runs the minted directory through the real C
loader end to end.
c/tools/README.md: describe the repack tool's full-family emission
(experts, o_proj, kv_b_proj, norms, io) and the aux files it copies into
--outdir. docs/FORMATS.md: document the fp8-e4m3 container-level
declaration, the stamped collision shape, and the decode constraints on
the minted kv_b_proj shard.
…falling through to int2

weight_at branched on fmt 0/1/2/4 and fell through to the int2 decode for
everything else, so any format without a device decoder -- fmt=5 (int3-g64),
fmt=6 (E8/IQ3), fmt=8 (fp8-e4m3), or whatever lands next -- was silently read
two bits at a time and returned plausible numbers, while the CPU twins
(qt_addrow, qt_matvec_rows) exit(1) loudly on the same input. fmt=3 is now an
explicit branch and the fall-through is __trap(): abort the kernel and poison
the context so the host's next cuda_ok() reports failure instead of the caller
consuming fabricated values.

absorb_fmt_ok now reads the shared coli_cuda_weight_at_supported predicate
(already in backend_cuda.h on this branch) instead of restating 'fmt <= 4',
which also admitted negative fmt values that would then have fallen through.

This is the product half of tests/test_cuda_fmt_trap_cuda.cu (device-side
trap coverage, already on this branch) and tests/test_cuda_fmt_guard.c (host
truth table). No fmt=8 decode support is added here: weight_at still refuses
fmt=8, exactly as the trap test asserts.
…nputs

Three audit findings on repack_fp8_passthrough.py, one fix round:

- RESUME: the progress manifest trusted os.path.exists, so a truncated or
  mutated "completed" output shard resumed as done, and an input shard
  mutated between runs went undetected. Manifest entries now record the
  emitted shard's size+sha256 (re-validated on resume; a mismatch re-emits
  to the recorded name, byte-identical for unchanged inputs, handing its
  stamp share back to the budget first) and the input shard's size+mtime_ns
  fingerprint (a mismatch aborts with InputChangedError: resuming would mix
  two sources into one container). A pre-fix string-schema manifest refuses
  loudly (ResumeManifestError) instead of being trusted unverifiable.
  Output shards get whole-file sha256 (the evidence class repack_rans.py's
  manifest already records); inputs get size+mtime_ns because every resume
  re-checks every input and hashing hundreds of GB of source per resume is
  the cost sha256 was rejected for on that side.

- MALFORMED REPACK-KIND TENSORS: a repack-kind tensor with the wrong dtype
  or a missing _scale_inv sidecar fell through both selection predicates
  and was silently DROPPED -- an exit-0 mint with a missing weight. Real
  mode and --dry-run both refuse loudly now (MalformedRepackTensorError),
  naming the tensor and the exact defect.

- CANONICALIZER SLOT-FIT: the header slot-fit guard was a bare assert,
  compiled out under python -O -- where an oversized rewrite (non-ASCII
  bytes json.dumps re-escapes longer than the raw UTF-8 the Rust writer
  stored) silently overwrote tensor payload bytes. Now a real ValueError,
  with a test that runs the corruption case under -O and asserts the file
  stays untouched.

test_fp8_repack.py: 11 new cases (fp8 modules 47 -> 58), each verified to
bite on the pre-fix tool.
…tation

The full-family docs commit (855c292) claimed these pieces for
c/tools/README.md and docs/FORMATS.md but did not include them. Now they
exist: the standalone-loadable output family (fp8-repacked residents and
routed experts, raw BF16/F32 pass-through for norms/router/embed/lm_head,
and the config.json/tokenizer.json/generation_config.json aux copies, with
the index.json non-copy rationale), and the constraint that a minted
kv_b_proj loads today but must not serve batched-path decode until fmt=8
absorb support (f8/absorb-fmt8) lands -- a loud crash, not a silent
misread, until then.
The trap test hardcoded per-fmt expectations ("fmt=8 must trap"), which
pins a moment instead of the invariant: the fmt=8 absorb-decode work
(f8/absorb-fmt8) adds a weight_at fmt=8 branch and flips
coli_cuda_weight_at_supported in the same commit, so on a tree where that
PR lands first, the frozen expectation fails on hardware with nothing
actually wrong.

Each probed fmt's expectation is now derived at run time --
coli_cuda_weight_at_supported(fmt) ? decoded : trapped -- so the test
asserts that weight_at's device dispatch AGREES with the predicate the
launch-site gates consult, in both directions: a predicate-false fmt that
decodes means the trap fall-through is gone; a predicate-true fmt that
traps means the promised decode branch is missing (or the harness cannot
tell the verdicts apart -- the failure message now names both readings).
The probe list is unchanged: both sides of today's truth table plus the
negative/out-of-range values the old `fmt <= 4` gate admitted, which
stay probed no matter what the predicate grows to support. fmts 0 and 3
double as the child-harness control, as before.
…build

backend_gpu_compat.h maps only the CUDA runtime names backend_cuda.cu
itself uses. The fmt trap test additionally uses cudaMemset (the backend
memsets asynchronously only) and cudaErrorUnknown (the backend never names
the generic error), so hipcc -- which compiles this file in the HIP
syntax-check lane via gpu-compile -- finds them undefined. Alias the two
locally per vendor in the test, the same arrangement
tests/test_weights_owned_cuda.cu uses for its own off-surface names,
rather than widening the product header for a test-only need.
weight_at's undecodable-format backstop calls __trap(), a CUDA intrinsic
with no HIP name-twin, so the HIP syntax-check lane fails compiling
backend_cuda.cu (rocm clang: use of undeclared identifier '__trap').

Map it in backend_gpu_compat.h's HIP arm -- the header's charter is that
every platform difference lives here and backend_cuda.cu stays untouched,
and it already maps a device intrinsic the same way (__syncwarp ->
__syncthreads). HIP device code aborts via abort(), the mapping hipify
itself applies. NVCC never sees the define, so the CUDA-side semantics the
fmt trap test pins (kernel aborts, context poisoned) are unchanged; under
HIP this makes the refusal compile, and its behavior on AMD silicon
remains unverified by this repo's tests, as the header now says.
Two fixes to the microbenchmark, both of which change what it measures.

1. Block size is parsed with atol(), so any fraction of a megabyte truncated
   to 0. GLM-5.2's experts are ~19 MB so this never bit, but a fine-grained
   MoE expert (3 * d_model * moe_intermediate_size * bits/8) lands between
   0.2 and 1 MB, and that whole range was inexpressible. Now atof(), rounded
   up to 4096 because O_DIRECT wants a sector-aligned length.

2. Every thread shared one fd. On Windows compat_open_direct() returns a
   SYNCHRONOUS handle (FILE_FLAG_NO_BUFFERING without FILE_FLAG_OVERLAPPED),
   and ReadFile on a synchronous handle serialises behind the file object
   lock, so [threads] had no effect at all and every Windows measurement was
   really queue-depth 1. compat_pread() is fine; the sharing was the problem.
   Each thread now opens its own fd.

Measured on a Phison 512 GB NVMe, Windows 11, 768 KiB blocks, O_DIRECT:

   before   1T 1.09   2T 1.07   4T 1.07   8T 1.02   16T 1.02  GB/s
   after    1T 1.02   2T 2.31   4T 2.91   8T 2.78   16T 2.58  GB/s

Cross-checked against an independent Python harness using CreateFileW with
FILE_FLAG_NO_BUFFERING and per-thread handles, which gives 2.85-2.95 GB/s at
4-8 threads on the same file: the two agree after the fix and disagree by
~3x before it.

Backward compatible: an integer argument behaves as before (19 -> "19 MB"),
the default is unchanged, and tools/datapoint.py's IOBENCH_RE only captures
the "-> N GB/s" field, which is untouched.

Linux and macOS are unaffected in behaviour -- positional pread already
concurs there -- but a private fd is correct on those platforms too.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
An expert's on-disk size is fixed at conversion time by the model's geometry,
and the engine reads one expert per request, so the architecture decides where
on the drive's block-size curve the streaming path operates. That curve is not
flat on consumer NVMe.

Measured on a Phison 512 GB NVMe (Windows 11, O_DIRECT, 128 reads, median of 3),
against a plateau of 3.02 GB/s at >= 1.5 MB:

   192 KB   -36%   queue-depth-1 penalty 4.33x
   384 KB   -17%                         2.98x
   768 KB   -10%                         2.15x
   1.5 MB    -3%                         1.86x
   12 MB     +1%                         1.20x

Both effects are graded and monotone in expert size: smaller experts lose
bandwidth outright, and lose proportionally more of it at low queue depth.
That is relevant to #441 (PILOT prefetch at queue depth 1), whose cost is
~4x for a fine-grained container and ~1.2x for GLM-5.2's ~19 MB experts.

One drive, one platform -- the knee position depends on the controller, NAND
and whether the drive has DRAM, so this needs replication before it means
anything general. The script exists to make that cheap: it needs no model
download and no engine build beyond iobench itself, and prints a ready-to-paste
markdown table. Stdlib only, like tools/datapoint.py.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
machine_info() hardcoded ram_gb=8.0 on anything that is not darwin or
linux, and evict_cache() sizes its eviction write from it: on a 128 GB
Windows box the tool wrote 9 GB, evicted nothing, and published the
warm decode as the cold figure — with nothing in the output to say so
(#1042).

Add the win32 branch from the report: GlobalMemoryStatusEx for the
physical RAM (checked, so a failed probe falls back to the old 8.0
instead of sizing the write from zeroed fields) and the registry
ProcessorNameString so the datapoint carries the marketing CPU name
rather than the family/model string. Stdlib only.

The portable fallback now also announces the RAM+1GB temp-file write
before starting it — correct behaviour, but surprising at 129 GB on a
big box, and --no-evict already exists for whoever wants out.

Probe fix suggested by @Unknown-Findout in #1042; tests mock the two
win32 probes so they run on any host.
The test carried two unrelated assertions, and ci.yml already said the
split was worth doing but not silently from a CI change: hit-rate
equality across two greedy replays (a real determinism check) and a
tok/s-within-25% bound over a ~15 ms tiny replay (a wall-clock check
that measured 38.8% between identical runs on a quiet box, and fails
4/5 runs on clean dev under WSL2 — one scheduler stall dominates the
whole run).

Now they are two tests. test_cpu_vs_cpu_determinism keeps the name,
the docstring's threat model (stray threading, uninitialized state)
and only the hit-rate assertion — verified stable 5/5 on the same
host that flaked before — and joins the CI list, which previously had
to exclude it wholesale to avoid the timing half.
test_cpu_vs_cpu_tok_s_stability holds the wall-clock bound, documents
that it is noisy on shared or virtualized hosts, and stays out of CI.
…ayers

The C engine (deepseek_v4.c) only requires compress_ratios to have at
least num_hidden_layers entries and ignores trailing extras, but the
python _dsv4_geometry check demanded an exact match. Models such as
ds-v4-flash ship 46 ratios for 43 layers, making every doctor/plan
run fail. Relax the check to >= and only iterate the first num_layers
entries to mirror engine behavior.
The recipe exists in deepseek-v4.md (rows16 vs reference accumulation
order, autopin history selecting between them) but #1136 shows a
careful reporter reconstructing the whole mechanism experimentally
because ENVIRONMENT.md — where the knobs are looked up — never points
at it. Add the pointer next to the engine's usage-save note, updated
for USAGE_SAVE now covering V4 too.
…ith the GPU tier

The #1097 default (9 lanes, 1.41x CPU decode) measured on the GPU tier's
reference box (RTX 5080, 2x NVMe, v1.7.0, 3324-token prompt, 48 greedy
tokens, two interleaved runs per config): decode 33.9/32.4 s vs 35.3/33.6 s
(~4 % — the VRAM mirrors and RAM cache absorb most decode misses), but
prefill 126.5/127.6 s vs 114.9/116.4 s (~10 % slower — the loader threads
contend with the MoE expert bank's parallel O_DIRECT refill for the same
NVMe queues). Record that next to the knob and in the canonical serve line.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
kvb_all materialises H*(qk_nope+v_head) floats for every context token
in one buffer — 30.1 GB at ctx 262144 — and cap_for_ram reserves that
permanently even though the buffer only exists during prefill. That
reservation is what drives the expert cache to its cap=1 floor at long
context (5.46 -> 0.86 tok/s in the issue's measurements): the process
pays 30 GB of resident budget for a transient.

Above KVB_FLASH_MB (default 2048), rebuild kv_b for KVB_TILE_MB worth
of tokens at a time and fold scores/values through an online
flash-style softmax. The rebuild total is unchanged (one matmul pass
over the context), scores and values keep their t-order; only the
softmax normalisation is incremental, so output can differ from the
one-shot path by rounding — the same kernel-family divergence class as
the CUDA/Metal attention arms (#510). Peak transient drops from
Tk*kvb_dim*4 to one tile plus 2 floats per (row, head), and the
cap_for_ram reserve mirrors the same trigger, freeing ~28 GB of budget
back to the expert cache at ctx 262144.

DSA rows keep the one-shot path: the top-keep list is scanned in its
two-band order and is not tileable by t-range. KVB_FLASH_MB=0 restores
the old behaviour everywhere; KVB_FLASH=0/1 is the A/B switch.

Verified: glm_tiny TF oracle 32/32 token-exact under the tiled path,
both single-tile and (with the row floor lowered locally) 8-tile with
causal clipping across every boundary; make test-c green.
Under DSA the score/value loops read only each row's top-keep list,
yet the one-shot matmul rebuilt all Tk rows — the same 30 GB paid for
a top-k read. Past the KVB_FLASH_MB ceiling, rebuild only the union of
selected rows (rows with ns==0 scan their full causal range and force
those rows in): per-row rebuild is independent and the read order
along each tlist is unchanged, so this path is bit-identical to
one-shot, unlike the dense tiling. Falls back to one-shot when the
union does not at least halve the buffer, or on allocation failure.

Verified: dense paths unchanged (tiny TF oracle 32/32 both modes),
zero-warning build, make test-c green. The gather branch itself needs
a DSA-active checkpoint to execute, which this box cannot run — logic
argued in the PR, verification requested from a 4xA6000 host.
docs(v4): V4_LOADER_LANES — keep the 9-lane default for the CPU path, set 3 with the GPU tier
docs: cross-reference the V4 reproducible-run recipe from ENVIRONMENT.md (#1136)
test: split the tok/s bound out of test_cpu_vs_cpu_determinism
fix(tools): probe real RAM on win32 in datapoint.py (#1042)
attn: tile the prefill k/v reconstruction past a ceiling (#768 cause 2)
JustVugg and others added 29 commits August 23, 2026 20:53
perf(dsv4): overlap hybrid fill DMA with host compute; pin slabs on Linux
The #1151 rebase gated the tiled (flash) and DSA-gather reconstruction
arms off under quantized KV, keeping #553's one-shot + staging design
untouched — correct for the merge, but it meant long context and KV
quantization could not compose: the 30 GB kvb transient is independent
of KV width, so a KV8 run at ctx 262144 still paid the one-shot buffer.

Both arms now dequantize the latent rows they need through the same
per-row codecs the one-shot path uses: the flash arm stages one tile's
worth of Lc rows (tile-sized f32 buffer, ~8 MB at the default tile)
before its kv_b matmul, the gather arm dequantizes exactly the union
rows it rebuilds, and the roped-key reads inline the one-shot path's
three-way branch so each representation keeps its exact accumulation
order. The f32 paths are byte-identical to before — the branches sit
on g_kv8/g_tq only.

Verified on glm_tiny TF oracle: f32 default 32/32 (unchanged); KV8 and
KV_TQ under KVB_FLASH=1 score 30/32 and 23/32 — identical to their
one-shot scores — in both single-tile and (row floor lowered locally)
8-tile runs; make test-c green, zero-warning build.
The cross-format alignment discussion in #594 asked for exactly this:
per-record {logical, encoded payload, framing} bytes plus physical
placement, so byte amplification can be verified on real data instead
of inferred from the achieved-ratio average.

repack_rans.py --census <dir> walks an already-minted output directory
read-only and writes rans-census.json: per record the byte-exact
logical length (ceil(n_symbols/2)), the summed stream payload before
padding, the framing around it (round16 header + payload pad), the
absolute shard offset and extent, and an explicit alignment_bytes=0 —
records pack back to back in the safetensors data section; O_DIRECT
alignment is the reader's page window, not a property of the stored
bytes, and a cross-format table wants that as a stated zero rather
than a missing column. Any record whose extent is not exactly
header + padded payload is flagged UNATTRIBUTED_BYTES and fails the
run.

Test: full attribution per record against the synthetic fixture
corpus (extent closes to header+payload+pad, offsets point at real
record starts, per-shard sums close, alignment stated as zero).
platform.release() says '10' on Windows 11 — Microsoft never moved the
internal version off 10.0, so the build number is the only tell: 22000
and up is Windows 11. Every Win11 datapoint in the tracker was being
filed as Windows 10 (#1042 follow-up, reported by the same tester who
confirmed the RAM probe on real hardware).

Tests cover both sides of the 22000 cutoff.
One amax per 512-dim latent row lets a single outlier dilate the
quantization grid for the whole row. FlashMLA's fp8 KV cache stores
one f32 scale per 128 latent elements for exactly this reason, and
the #1140 attribution experiment pinned KV8's tiny-oracle flips to
the latent value path — the component grouped scales tighten.

KV8_GS=<n> puts one scale per n latent elements (default 0 = per-row,
byte-identical to current behavior; the _gs entry points collapse to
the per-row functions and the test pins that equivalence). RoPE rows
keep per-row scale — qk_rope is 64 and was not implicated. Measured
on the tiny oracle (kv_lora=32, so the group count maps to the real
512-dim ladder):

    f32        32/32
    KV8        30/32   (per-row, as merged)
    KV8_GS=16  30/32   (2 groups)
    KV8_GS=8   29/32   (4 groups — near-tie flips are coin-flips
                        along the way, not monotone)
    KV8_GS=4   32/32   (8 groups — full recovery)

Byte cost at the real geometry: GS=128 (FlashMLA's choice, 4 groups)
adds 12 B per token-layer over per-row, ~0.6% of the fp8 row. No
.coli_kv format for grouped scales yet: KV8_GS forces KVSAVE=0 with a
notice, same interim as KV8_ROPE would have; the v2->v3 bump is the
follow-up once a granularity default is picked on a real checkpoint.

test_kv_fp8: grouped round-trip with an outlier confined to one group
(other groups' grids must stay tight, and grouped must beat per-row
off the outlier), nscale arithmetic, and gs=0 bit-equivalence.
The fill-once Vulkan routed-expert tier uploads from freshly-read RAM
slots INLINE on the decode thread, capped at a fixed K3_VK_UP=8 per
step. A fixed count is wrong in both directions: on a slow bus eight
uploads stall the step, on a fast one the tier fills needlessly slowly
and decode keeps paying CPU matmuls for experts that could already be
resident.

K3_VK_UP=auto (opt-in; a number or the default 8 behave exactly as
before) replaces the count with a measured budget:

    budget = fraction * step_seconds / upload_seconds

with both rates live EMAs (decode steps only), fraction from
K3_VK_FILL_FRAC (default 0.25), clamp [1, 64], and the legacy cap while
either rate is unmeasured -- enabling auto can never start worse than
the default. The policy is pure arithmetic in the new hybrid_split.h,
shared with the DSV4 hybrid split (deepseek_v4_hybrid.h stays as a
shim), and unit-tested GPU-less: legacy fallbacks, the budget formula,
both clamps, monotonicity in step time.

Visibility: the startup banner reports auto, and both the CLI decode
summary and the serve per-turn line print the current cap with the two
EMAs, so a single log shows what the policy decided and from what.

Validated end to end on Lavapipe (software Vulkan): default and auto
are token-identical over 24 tokens on the tiny fixture, VK matches the
CPU engine token for token, and the adaptive cap behaves as designed --
with memcpy-fast uploads (0.01 ms) under a 12 ms step it opens to the
ceiling and the tier fills within the first tokens instead of 8/step.
make check green; K3 tiny vendor oracle + checkpoint suite green.
fix(tools): label Windows 11 correctly in datapoint machine_info (#1042)
tools: per-record layout census for rANS containers (#594)
KV8: grouped latent scales (KV8_GS) — FlashMLA's fp8 cache geometry
perf(tune): persist rotating workloads and tune V4 loaders
attn: KV8/KV_TQ awareness for the flash and gather kvb arms (stacked on #1151)
perf(k3): measured adaptive expert-tier fill (K3_VK_UP=auto)
perf(tune): measure safe RAM and cache caps
feat(tools): full-family FP8 (e4m3) container mint with content-validated resume
Engine open used to construct the safetensors index SIX times: once
retained for the engine, once retained for the expert store, and four
throwaway builds inside the resource planner, the dense inventory and
the head-cache probe/load. Each build opens every shard, preads and
parses every header and hashes every tensor name -- on a 141-shard
checkpoint that is ~100k names five more times than needed, plus three
parses of config.json and three full per-layer plan+validate walks.

- The planner, inventory and head-cache paths now take the engine's
  retained target_index (and its parsed config); the two retained
  builds remain, the four throwaways are gone. One layer walk yields
  both the per-layer maximum and the dense total, replacing the
  separate inventory pass entirely.
- A new stderr line, `v4_open index=%.2fs plan=%.2fs`, makes the open
  phases measurable at last: datapoints report "engine load" from
  time_to_first_token, which folds in session create and the whole
  first prefill, so open-phase wins were invisible before.
- v4_fp8_pack_rows8_inplace -- a full second pass over every dense FP8
  byte at load -- swaps its scalar transpose (eight cache-line reads
  per 8 bytes written) for an 8x16 unpack-chain transpose with 16-byte
  loads and stores. Byte-identical order by construction, and the
  token-exact tiny oracle gates it end to end.
- The expert manifest's records array drops a redundant calloc
  (build_record zeroes each record before filling it).

Not included, on purpose: the pin-after-populate and mint-layout items
sketched for this slot were checked against the code first -- colibri's
startup pin path already populates before wiring (explicit comments at
colibri.c:2738 and 9271), and the safetensors writer does not guarantee
payload ordering, so a layer-contiguous mint flag would promise what it
cannot deliver. What the code disproved was dropped.

make check green; V4 tiny oracle token-exact; every amalgam unit
compiles under -Werror=implicit-function-declaration with and without
COLI_V4_GPU_TIER.
perf(dsv4): build the shard index once per engine open
…I_CUDA_MOE_DOUBLE=1)

Opt-in second full-layer expert bank: while the compute stream chews layer L
from the active bank, a worker thread uploads layer L+1's complete expert set
into the spare bank over the device's aux stream, starting before L+1's
routing is known. On the layer switch the banks swap and the per-expert valid
map travels with the swap, so partial prefetches are topped up by the existing
route-aware refill instead of thrown away.

- deepseek_v4_bank_pair.h: pure swap/legacy + prefetch-target decisions,
  unit-tested GPU-less (tests/test_v4_bank_pair: bootstrap, segment restart,
  worker failure, last layer, 61-layer sweep)
- backend_cuda_dsv4.cu: bank upload refactored onto a caller-chosen stream
  (compute or aux), DeepGEMM scale packing and pointer-table writes ride the
  same stream, single drain before return; dsv4_cuda_expert_bank_upload_aux
  added
- backend_loader_dsv4.c + dsv4.def: aux upload exported, resolved as OPTIONAL
  so older DLLs keep the tier; the engine detects the missing export on the
  first failed upload and permanently falls back to single-bank
- deepseek_v4.c: worker holds at most one store lease at a time (bounded pin
  slots), L+1 lookups live in L+1's own slot partition so the computing layer
  is never evicted; every failure path (second-bank allocation, aux upload
  unavailable, fully failed layer) degrades loudly to today's single-bank
  behaviour; swap/prefetch counters printed at release

Default (env unset) is byte-identical to the current single-bank path.
perf(dsv4): double-buffered expert bank prefetch for GPU prefill (opt-in)
perf(cache): index GLM, OLMoE, and Qwen expert slots
The README had drifted behind the engine in ways that were factually wrong,
not just stale:

- The DeepSeek V4 requirements row said the CUDA tier needs sm_80+; since the
  portable-pre-ampere work it runs on Pascal and Turing (GTX 10 / RTX 20),
  built with CUDA_ARCH=portable-pre-ampere NO_TC=1. Fixed in the table and in
  the V4 section.
- The roadmap still listed Kimi K2 and Qwen3 MoE as future work while Kimi K3
  and Qwen3.6 are documented as running two sections earlier. Rewritten around
  the six families that run today.
- Version strings in the sample banner and the release-archive example were
  several releases old.

New since the last README pass, now documented:

- Kimi K3 recurrent-state checkpoints (COLI_K3_CKPT, COLI_K3_CKPT_DIR) and the
  measured Vulkan tier fill (K3_VK_UP=auto), plus the token-exact KDA/MLA
  oracle that gates them in CI.
- The two opt-in DeepSeek V4 GPU levers (DSV4_HYBRID, COLI_CUDA_MOE_DOUBLE),
  labelled as experimental and looking for community numbers; defaults stay
  byte-identical.

Translations (it, zh-CN, zh-TW) updated for the factual points they carry:
version strings, the roadmap paragraph, and zh-CN's DeepSeek V4 section that
still described the engine as an experimental CPU-only path.
docs(readme): catch the README up with what already runs
…ows DLL

Since the V4 CUDA tier landed, dsv4_cuda_available() answered False on every
non-Windows platform: it only looked for coli_cuda_dsv4.dll next to the
engine. On Linux the tier is linked straight into the binary by nvcc
(-lcudart), so a perfectly good `make deepseek-v4 CUDA=1` build was refused
by --gpu/--vram with a hint about building a DLL that does not exist on
Linux (#1219). v1.6.1 predated the gate, which is why the report says the
regression starts at 1.7.0.

Detection now mirrors what cuda_binary() already does for GLM three lines
below: on Linux, ldd on the V4 engine binary must list libcudart (or
libamdhip64) and not as "not found"; on Windows the DLL probe is unchanged.
The error hint names the right build per platform: make deepseek-v4 CUDA=1
on Linux, the DLL target on Windows.

Unit tests cover both platforms: CUDA and HIP links accepted, CPU build and
broken links rejected, missing engine rejected, DLL present/absent, and the
per-platform hint text.
fix(coli): detect the Linux DeepSeek V4 CUDA build, not just the Windows DLL
Require the CUDA runtime used by the in-tree backend, avoid masking unexpected detector bugs, and report unsupported host platforms accurately.
fix(coli): harden DeepSeek V4 CUDA detection
@JustVugg
JustVugg merged commit dd7df2c into main Aug 24, 2026
46 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.