Skip to content

release: merge dev → main (v1.3.0 — Kimi K3, Inkling on small RAM) - #703

Merged
JustVugg merged 54 commits into
mainfrom
dev
Jul 29, 2026
Merged

release: merge dev → main (v1.3.0 — Kimi K3, Inkling on small RAM)#703
JustVugg merged 54 commits into
mainfrom
dev

Conversation

@JustVugg

Copy link
Copy Markdown
Owner

Promotes 51 commits from dev. The headline: colibrì goes from one model family to three MoE families, 744B to 2.8T parameters — and a 975B answers on a 25 GB box.

New model families

Kimi K3 (Moonshot, 2.8T total / 104B active, 93 layers) — #676, by @steve-m, with the chat/API/Web integration by @ZacharyZcR. A sibling engine (c/kimi_k3.c) that streams the QAT-trained MXFP4 routed experts straight from the original Hugging Face shards — never re-encoded — and quantizes the bf16 dense set at load time. Includes the KDA + gated-NoPE-MLA + AttnRes + LatentMoE architecture, an MXFP4 matmul kernel (scalar + AVX2 + int8-activation), chunked prefill (bit-identical, 2.6×), parallel O_DIRECT expert reads, and --chat for K3's XTML format.
Status: engine and tokenizer validated — the multi-turn wire matches Moonshot's official encoding_k3.py 77/77 token ids — and the C/Python regression suite passes. Full-model generation is still gated on a host with ~1.6 TB of storage for the checkpoint; nobody has run it end to end yet. Said plainly so the people with that hardware know it's theirs to try.

Inkling (Thinking Machines, 975B / 41B active) now runs on a small host — #701. Two problems were in the way:

  • The pre-converted container ships int4 experts but bf16 dense weights: 49.4 GB resident, expanded to f32 while loading (~99 GB peak). A new one-pass tool quantizes just the dense set to int4-gs64 (15.3 GB); the engine auto-detects it, and without it the bf16 path is untouched.
  • The expert cache silently computed with the wrong experts. moe() acquired a slot for every (token, expert) pair up front and kept the pointers until compute, but a full cache evicts slots already handed out in the same call. Correctness therefore required a cache big enough for every distinct expert in the batch — up to 108 slots per layer for an 18-token prefill at topk=6. Below that: incoherent output, no error. Now the experts are processed in rounds of cap and accumulated, so nothing is evicted while in use, and the cache can go down to one slot per layer.

Result: My name is Inkling. — a 975B model, correct answer, clean EOS, 15.1 GB RSS on a 25 GB machine. Verified against the transformers oracle at 36/36 teacher-forced and 24/24 tokens, at both cap=4 and cap=1 (where the old code corrupted).

Correctness fixes

Performance

Thanks

This release is mostly other people's work, and the hard parts were found by people with hardware we don't have:

Version bumped to 1.3.0 (c/version.py). Full suite green: 28 C tests + 233 Python tests. Tag v1.3.0 after merge to publish the release artifacts.

tasoo-oos and others added 30 commits July 27, 2026 18:11
Dependency-free C99 single header (same shape as st.h/quant.h: all-static,
tests include it directly), three layers:

- scalar stream codec: ryg_rans-style 32-bit-state byte-renormalized
  encode/decode over the 16-nibble alphabet, ported unchanged from the
  measured prototype round, plus a checked decode that enforces the
  invariants every genuine encoder output satisfies (initial state in
  [L,256L), exact byte consumption, final state == L) so validators can
  refuse corruption without ground-truth bytes;

- chunk record: n_symbols/packed_bytes/stream_offsets[257] header, derived
  round16 padding, 256 round-robin-interleaved streams per tensor.
  TRUST-VERIFY-REFUSE reader: every malformation class gets its own named
  error (truncation, count mismatch, offset malformations, short streams,
  nonzero padding, trailing bytes), no partial acceptance;

- batched decode arms: scalar (branchy, mirrors the reference), scalar_bf
  (closed-form renorm -- the SIMD algebra kept testable on any host), NEON
  (16 streams/group, early per-lane loads, vqtbl freq/start lookups, fused
  vuzp repack; ~1.15x scalar_bf single-thread on an M-series host, and the
  G=8 variant that measured 0.88x is why the width is 16), AVX-512 F+BW
  (16 lanes, one vpermd per table lookup, vpmovqb packed store; ported from
  the prototype kernel measured at 8.7 GB/s batched on Zen 5).

Vector arms are byte-identical to scalar by contract -- this is a codec,
there is no tolerance -- and each carries the quant.h-style envelope:
compile-time ISA gate, first-use round-trip selftest that disables a
failing arm loudly, env kill-switch (RANS_NEON=0 / RANS_AVX512=0), and
RANS_PATH=<arm> forcing that refuses rather than silently downgrades.
The selftest table mixes freq=1 and freq=3 rare symbols deliberately:
power-of-two frequencies keep floor(log2 x) on a rigid lattice that never
visits some multi-byte renorm bands (measured directly -- a sabotaged
renorm threshold passed every comfortable-table test), so the off-lattice
f=3 group is what makes the selftest actually bite.
repack_rans.py mints the entropy tier from per-row int4 expert shards:
corpus-wide header index (a tensor and its .qs sidecar can live in
different shards), ONE global table from the pooled nibble histogram
stamped byte-identical into every output shard (decode stays shard-local),
every record round-trip-verified byte-exact BEFORE its shard is written,
and deterministic output (sorted orders, sort_keys stamps, no timestamps
-- two runs diff clean). Writer-side geometry guard mirrors the fp8 tool's
discipline: a tensor at the per-group-64 byte signature (weight/scale
ratio exactly 32) or without a confirmable per-row .qs is refused by name,
never silently coded under this format's identity.

rans_verify.py is the TRUST-VERIFY-REFUSE validator: stamp layer (the
colibri.fmt stamp is MANDATORY here -- entropy-coded sizes are
data-dependent, no byte-arithmetic inference exists, so the stamp is the
only dispatch signal, not a cross-check), table layer (JSON shape, freq
sum, prefix start, slot banding, crc32), record framing, per-stream
decode invariants, and a deterministic re-encode pin. One named refusal
class per corruption; exit 0 only on full trust.

rans_format.py is the shared reference module: table construction
(largest-remainder quantization, unchanged from the measured prototype),
record build/parse, minimal deterministic safetensors i/o, and a
pure-Python codec fallback.  builds tools/librans_c.* (a thin
exported-symbol bridge over rans.h, same optional-accelerator shape as
tools/libiq3): with it the tools use the C codec, without it they emit
identical bytes far more slowly -- the e2e test pins the two
implementations byte-equal.
test_rans.c (wired into TEST_BINS): stream round-trip property suite over
random and adversarial inputs (all-zero, all-max, single-symbol-freq=M,
15-symbol boundary, empty/odd/edge lengths, low-resolution and rare-symbol
tables), checked-decode refusal semantics including an enumerated
single-bit-corruption sweep (a corruption that silently PASSes the checked
decode is a verifier hole, so it is asserted impossible, not sampled),
record framing round trip at the format's real N=256 plus small-N shapes,
the full named-refusal battery, and the batched-arm identity sweep: every
compiled arm (scalar, scalar_bf, NEON here; AVX-512 on x86 builds) must be
BYTE-identical to the packed reference on the whole suite. The rare-symbol
case exists because a deliberately sabotaged renorm threshold passed every
other case: power-of-two-frequency tables keep the decoder's state off the
multi-byte-refill bands entirely (measured), so the f=3 off-lattice group
is what makes this suite able to bite. Envelope semantics are asserted
too: kill-switches drop the vector arms from selection, and a forced
unavailable arm is an error, never a silent downgrade.

test_rans_repack.py (unittest, skips without numpy): synthetic per-row
int4 fixtures with a cross-shard .qs sidecar; end-to-end byte-exact round
trip through real shard files; .qs raw pass-through; MANDATORY-stamp and
per-shard-table metadata shape; writer determinism (two runs diff clean,
AND a forced pure-Python run emits bytes identical to the C-codec run);
the verifier's corruption battery (payload flip, count mismatch, offset
break, truncation, dirty padding, dropped stamp, dropped table, wrong
table crc, freq-sum break, dangling stamp -- each refused by name); and
the writer-side geometry refusals (g64 signature, missing scales, empty
selection).
…t claimed)

The complete wire contract a third-party consumer needs: safetensors
container shape (original tensor names, U8 record blobs, raw .qs
sidecars), the chunk record byte layout with derived round16 padding, the
256-stream round-robin interleave and why it makes wide decode
output-coalesced, the per-stream codec including the verifiable
encoder-output invariants, and the per-shard table-in-metadata blob with
its crc field and generation id.

The load-bearing section is the MANDATORY stamp: unlike every existing
format, entropy-coded byte counts are data-dependent, so no
byte-arithmetic inference can identify this format -- the colibri.fmt
stamp is the only dispatch signal, and a future engine read path must gate
on it BEFORE the existing size-formula inference, which stays untouched.
Scope is per-row int4 v1 (the per-group-64 class is excluded with its
rationale), and the registry row proposes the NAME only: the numeric fmt
ordinal is deliberately left for the maintainer to assign.
…er bound, never-crash validator

Review findings (one blocker, four MAJOR): the codec core survived a
4,907-mutant ASan/UBSan fuzz with zero memory errors and zero arm
divergence, but the untrusted-input edges around it did not hold. All
fixes, each with its regression pinned:

- parse hardening: the packed_bytes check is now non-wrapping
  (n/2 + (n&1) — the additive ceil form accepted n_symbols=UINT64_MAX with
  packed_bytes=0), and an amplification bound refuses any header claiming
  more symbols than payload_len*8*M_max could encode under ANY admissible
  table (E_OVERSIZE) — the decompression-bomb surface E2's loader would
  have inherited, refused before anything sizes buffers from n_symbols.
  C and Python refuse identical bytes with identical classes, and that
  parity is now a TESTED property (same-bytes battery through both
  implementations), not a documented intention.

- encoder bound: the old per-stream scratch rule (n + n/2 + 64) was
  arithmetically false for freq=1 symbols at scale_bits >= 13 (a symbol
  costs up to scale_bits bits), so a legitimate rare-symbol-heavy tensor
  refused as out-of-memory. Correct bound (ceil(n*scale_bits/8)+4+slack),
  and bound exhaustion is now E_SCRATCH — a logic failure, never conflated
  with the allocator. Bulk-freq=1 input round-trips at scale_bits 13/14/15.

- named encode refusals: input containing a value > 15 or a symbol with
  table frequency zero refuses as E_SYMBOL_UNCODABLE via a pre-scan,
  instead of burning the scratch buffer into a misdiagnosed E_NOMEM; the
  stream primitive masks to the nibble alphabet so no input bytes can index
  past freq[16] (ASan-verified).

- rans_verify.py's "never crashes" is now enforced, not promised: the
  shard header shape is validated by name (object-ness, entry shape,
  data_offsets bounds against the file, str->str metadata), record-size
  allocations refuse as E_NOMEM instead of raising MemoryError, and
  per-tensor/per-shard catch-alls convert anything unexpected into a named
  E_INTERNAL refusal. The review round's six traceback classes all print
  REFUSE lines now.

- test vacuum: make test-python builds the ctypes bridge first, the e2e
  C-vs-Python byte-identity pin skips LOUDLY if the bridge is somehow
  absent, and a bridge-presence assertion pins that the default run
  actually used the C codec.

- geometry guard exactness: 2-D weights get positive per-row verification
  from the shape fields; flattened tensors at ratio 64 — exactly the
  per-group-128 signature, byte-indistinguishable from per-row I=128 —
  refuse as E_GEOMETRY_AMBIGUOUS instead of being silently stamped with
  geometry the ratio cannot prove.

- n_streams is validated in every public entry (E_NSTREAMS; the ctypes
  bridge pins the format's 256) — no division by zero, and the generic
  decode path can no longer return RANS_OK with untouched output. Record
  buffers must be 4-byte aligned (E_UNALIGNED) and the offsets table is
  accessed via memcpy on the parse/write side; the little-endian
  precondition is a compile-time #error and documented.

- quantize_freq: stable argsorts (exact fractional ties now break by
  symbol index on every host — contract 4's cross-host clause) and a
  provable-termination precondition (m >= present symbols, else ValueError).

- multi-shard repack is crash-evident: a non-empty output directory
  refuses without --force, and repack-manifest.json is written LAST as the
  completion marker — an interrupted set can no longer masquerade as done.

- the selftest's renorm-band coverage is machine-checked (the fixture
  fails the arm if multi-byte refills stop occurring) and the suite gained
  the same assertion — the one load-bearing claim that was previously an
  author assertion. The review fuzzer is adopted as tests/fuzz_rans.c
  (make fuzz-rans, ASan+UBSan, seeded/bounded; not a test gate).

- docs: the encoder renorm emission bound (x_max formula) makes a
  third-party WRITER doc-buildable; the >=4-bytes-per-stream validity rule,
  the amplification bound, and header-integer endianness are stated.

Sabotage meta-check re-run on the final tree: a t15 off-by-one in the NEON
arm still fails both the identity suite and the first-use selftest.
Engine object still byte-identical to base.
…heck

Per-record validation proves a record is A well-formed artifact of this
format; it cannot prove it is THE record a given mint run produced — a
well-formed record swapped in from another checkpoint passes every
per-record check (the #671 review point). Close that at the build layer:

- repack_rans.py records, per output shard, a `records` map — original
  tensor name -> sha256 of the emitted record bytes exactly as written —
  hashed inline while the bytes are already in hand (no second read). The
  record wire format is untouched: the manifest is a sidecar file, still
  deterministic (sorted keys, no timestamps), still the completion marker.

- `--manifest-only <dir>` retro-generates the manifest for already-minted
  directories by hashing shards in place, no re-encoding. It preserves
  every field of an existing manifest and only recomputes the digest maps,
  so retro output is byte-identical to mint output for the same shards
  (tested byte-for-byte). It lives in the WRITER deliberately: the
  manifest is the writer's completion/provenance artifact, and
  rans_verify.py stays side-effect-free — a tool pointed at a stranger's
  download must never write into it.

- rans_verify.py checks each record's bytes against its digest whenever a
  manifest with digests sits next to the shards, BEFORE record decode
  (the swap case is exactly what the later checks are blind to). Named
  refusals: E_DIGEST_MISMATCH (bytes differ from the mint run),
  E_DIGEST_MISSING (digests exist but not for this shard/tensor — a
  record the mint never produced), E_MANIFEST_MALFORMED (a manifest that
  exists but cannot be parsed is evidence of tampering, refused rather
  than treated as absent). No manifest, or a digest-less manifest from an
  older mint, is note-and-proceed — never an error. Never-crash holds.

- tests: digest round-trip green with "manifest digests checked" pinned;
  tamper bite (one flipped payload byte in the shard file, manifest
  untouched -> E_DIGEST_MISMATCH); retro equivalence (byte-identical
  manifest over a minted dir; from-scratch retro reproduces the digest
  map); backward compat (stripped digests -> note + green); corrupt
  manifest -> E_MANIFEST_MALFORMED without traceback; foreign shard ->
  E_DIGEST_MISSING. The per-record corruption battery now drops the
  manifest first — that layer must hold even when an attacker removes the
  whole-artifact evidence. Mint determinism (manifest included) already
  pinned by the existing two-run byte compare.

Build integrity (these bytes came from that mint run) vs container
identity (the stamp/registry lineage) is documented in
docs/int4-rans256-g0.md; the engine-side load check ships with the
consumer PR.

Whole-file layer (micro-addendum, same round): each manifest shard entry
also carries `sha256` — the complete .safetensors file bytes, hashed while
streaming the write before the atomic rename (retro mode hashes files in
place; retro-over-minted stays byte-identical to mint). The file hash
subsumes what the record map cannot see — .qs sidecars, headers, padding —
closing the scale-swap gap; the record map stays for granular diagnosis.
Verify checks the file hash FIRST (cheapest whole-artifact gate,
E_SHARD_DIGEST_MISMATCH), then the per-record layer, continuing past a
file-level mismatch so record digests localize in-record damage. Tests:
.qs-region tamper bites as E_SHARD_DIGEST_MISMATCH with NO record-level
refusal (the exact gap, demonstrated), record tamper trips both layers,
retro identity and mint determinism re-proven, degraded manifests
(no digests at all / file-hash-only) note-and-proceed.

Validator delta-3 fix (same commit): manifest entry shapes are now
type-validated unconditionally BEFORE the digest-evidence scan — an entry
whose only digest fields are malformed-typed (sha256 as a number, records
as a list, an entry that is not an object) previously zeroed the evidence
scan and was silently classed as a digest-less older mint instead of
refusing. Wrong-typed digest fields are malformation regardless of whether
valid evidence remains elsewhere; both validator corpses are in the shipped
battery (E_MANIFEST_MALFORMED, no TRUST line, zero tracebacks). Retro
--manifest-only also warns on stderr when `source` provenance is
unknowable, so the loss is visible in output, not just by field absence.
fix(win): test_st_mirror stale dirs + two -Wformat-truncation warnings
serve: wire Host allowlist through coli CLI
…ngine

docs: define colibri as an experimental inference engine
entropy: in-tree rANS codec, int4-rans256-g0 container format, offline tools
…ode (#678)

With KV_SLOTS=1 (the default) each serve turn runs through ONE spec_decode
call, and run_serve_mux's stdin poll only runs between turns. When the
gateway's stop filter matched (e.g. a role marker) it sent STOP, but the
engine sat inside the turn generating suppressed tokens until max_tokens --
minutes of "active query, no progress" on CPU-only hosts (#675 follow-up).

Fix: mux_ctl_poll, a non-blocking stdin poll called from the mux spec emit
callback once per emitted token. It consumes pending STOP/CANCEL lines and
raises g_mux_stop/g_mux_cancel, which spec_decode checks next to g_intr, so
a server stop now takes effect within ~1 token. STOP exits into the normal
mux_done (DONE frame); CANCEL mirrors mux_submit's cancel epilogue (ERROR
CANCELLED, consistent KV append). The framed protocol stays in sync: a
SUBMIT that arrives while the only slot is busy has its payload drained and
is refused with SLOT_BUSY, exactly as mux_submit would. On stdin types the
platform cannot poll, behaviour falls back to the pre-fix end-of-turn
handling. The flags are only ever raised during a mux spec turn, so chat,
run and oracle spec_decode callers are unaffected.

Tested: CPU build clean; -DCOLI_CUDA syntax clean; tests/test_stops 4/4;
tests/test_openai_server.py 100/100; live mux protocol test against the
glm_tiny fixture (READY, STOP honored in 0.27s on the batched path, CANCEL
-> CANCELLED, unknown id -> NOT_FOUND, follow-up requests complete). The
spec path itself needs an MTP-bearing model; verification on real hardware
requested from the #678 reporter.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
fix(serve): honor STOP/CANCEL mid-turn in single-slot speculative decode (#678)
rocWMMA's headers static_assert on archs without MFMA/WMMA units
(ROCm/TheRock#1944), so any HIP build for MI50/Vega/RDNA1/RDNA2 died at
compile. Make rocWMMA optional: the Makefile derives -DCOLI_HIP_NO_WMMA
from HIP_ARCH (normalized: comma lists accepted, native resolved via
rocm_agent_enumerator so the arch filter sees real gfx names) and the
compat header then compiles the WMMA kernels out, taking the same
host-dispatch fallbacks as pre-Volta CUDA. The define must reach both
hipcc passes: an arch-macro check in the header would leave host
dispatch enabled for kernels whose device bodies were compiled out.

Also gate COLI_CUDA_TC_INT4 on COLI_GPU_HAS_WMMA like TC_W4A16 already
is: grouped_s4_wmma's body is __CUDA_ARCH__>=750-guarded, and launching
it empty silently leaves the output buffer unwritten.

Missing rocwmma-dev on a WMMA-capable arch stays a hard #error; mixed
capable/incapable HIP_ARCH lists warn that WMMA is disabled for all
targets (shared host dispatch requires one consistent setting).

Verified on gfx906 (MI50) + gfx1030 fat binary: builds clean, engine
serves GLM-5.2 with the VRAM expert tier active. make check passes
(229 tests).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The engine takes the string "auto" (sized to free VRAM at init), but the
launcher did a strict float() on the env var, so coli doctor/chat/serve
crashed with "could not convert string to float: 'auto'". Non-numeric
values now mean 0 for the planner (planner sizes the budget itself) while
the engine keeps reading the original string. Same guard for RAM_GB, where
the engine's atof() already tolerated it but the launcher didn't.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
fix(hip): support GPUs without matrix cores (gfx9xx, gfx101x/103x)
ngram_draft() already proposes a continuation by matching the tail of the
current sequence against earlier positions in that same sequence. This adds
the same lookup over a persistent file of previously generated token ids:
at each decode step the engine proposes what followed the longest suffix of
the live context found in the corpus (suffix 8..3, most recent occurrence
first). Proposals enter the existing batch-union verify forward, so the
target model's argmax still decides what is accepted.

Unlike MTP (one token per forward) a corpus hit proposes a whole span. On
GLM-5.2 int4, 96-token greedy, replaying a prompt whose generation is in the
corpus: 1.78 -> 6.00 tok/forward and 3.69 -> 4.52 tok/s on a resident H200
(90% acceptance), 1.76 -> 7.92 tok/forward and 0.82 -> 1.00 tok/s on CPU
(100% acceptance). Note the gap: 3-4x fewer forwards buys ~22% of wall
clock, because in an MoE every verify row activates its own experts, so
speculation amortizes the dense path and attention but never the expert
work. docs/corpus-draft.md documents that ceiling rather than leading with
the forward multiplier.

Rejected drafts cost time, so the source pauses below COLI_CORPUS_MINACC
acceptance (default 50%, the measured break-even: 90% gave +22%, 19% gave
-25%). A prompt unrelated to the corpus costs nothing -- the suffix match
finds nothing and the source stays inert (measured: same forward count as
no corpus at all).

Off by default: without COLI_DRAFT_CORPUS the corpus pointer is NULL and
behaviour is unchanged. Build a corpus from any run with TOKENS=1, which
already dumps the generated ids.

Validation on this branch: make check 229 tests OK, clean build 0 warnings,
oracle 32/32 TF + 20/20 greedy, and 20/20 greedy again with the source
armed (a new draft source must be token-exact when ON, not only when OFF).
tests/test_corpus_draft.c covers the lookup contract: longest suffix wins,
recency wins ties, the -1 span separator terminates a proposal, caps hold,
degenerate inputs propose nothing.

One caveat, not specific to this source and filed separately: on CUDA a run
whose drafts are accepted in long streaks can diverge from the unbatched
path by a single near-tie token (1 in 96 measured), and the same corpus
accepts at 90% on GPU vs 100% on CPU. The CPU path is byte-exact.
…IN_GB hosts (#686)

CUDA_RELEASE_HOST defaulted to ndev>1, chosen when the host copy was the
multi-GPU re-upload path. On a single GPU that is also asked to fill RAM
(PIN_GB=all, or a PIN_GB large enough that the two tiers compete for the
same memory), that default keeps a full host copy of every VRAM-tier
expert and the RAM tier starves.

Measured on 1x H200 141 GB + 235 GB RAM, GLM-5.2 int4, 96-token greedy,
3 runs, median:

  default (RELEASE_HOST off)  9,297 resident experts   86.2% hit  1.10 tok/s
  CUDA_RELEASE_HOST=1        14,951 resident experts   99.4% hit  4.75 tok/s

[PROF] for the default config showed 166.9 GB fetched with 87.7 s of read
service inside an 87.4 s decode: the whole token budget went to misses the
duplicated RAM could have absorbed.

The host copy is provably redundant in that configuration — releasing it
still reloads from disk if CUDA later fails. So keep ndev>1 exactly as
before and add the single-GPU + large-PIN_GB case:

  - explicit CUDA_RELEASE_HOST always wins (0 disables, as before)
  - ndev>1 unchanged, so multi-GPU behaviour is byte-identical
  - single GPU only opts in when an expert tier exists AND PIN_GB is
    "all" or >= the tier size (under CUDA_EXPERT_GB=auto the tier is sized
    to the whole card, so any explicit PIN_GB qualifies)
  - one stderr line when the new default engages, naming the knob to undo it

Also extends the existing "[RAM_GB auto] WARNING ... PIN_GB is inflating the
resident set" path: on a single GPU still holding host copies, what inflates
the resident set is usually those copies rather than PIN_GB itself, so the
warning now names CUDA_RELEASE_HOST. That line already fired in exactly this
situation without telling the user which knob to reach for.

Validation: make check OK, clean build 0 warnings (CUDA=1 and portable),
oracle 32/32 TF + 20/20 greedy.
fix(cuda): release host copies of the VRAM tier on single-GPU large-PIN_GB hosts (#686)
fix(cuda): apply grouped scales in ragged attention
JustVugg and others added 24 commits July 29, 2026 19:41
feat(spec): frozen-corpus draft source (COLI_DRAFT_CORPUS)
The measured numbers come from replaying a prompt whose generation is already
in the corpus — a best case, not a general speed-up. On novel text the corpus
has nothing to propose and the gain approaches zero, which is the case a chat
assistant is normally in. Says so explicitly so the table isn't read as a
blanket +22%.

Also cross-links #689: deep drafts from this source are the easiest way to
reach the open CUDA verify-batch divergence at S>=8.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…PU (#687)

A resident tensor whose upload fails is disabled permanently and silently
falls back to CPU. Today that produces one line per tensor:

  [CUDA] tensor [2048,6144] on device 0 disabled after an error; falling back to CPU

Sixty of those scroll past in a second, none of them says what to do, and
the end state reads as healthy: the GPU is fully allocated, nvidia-smi
looks busy, and the run is at CPU speed. In #687 that cost a debugging
session to work out that CUDA_EXPERT_GB=auto had claimed the card to
within 4 MiB, leaving nothing for the lazily-uploaded dense tensors.

This keeps the per-tensor detail for the first few, then prints one
prominent block naming the knob that fixes it, and repeats the count in
the end-of-run CUDA stats — by then the original lines are thousands of
log lines back, and the count is what explains a CPU-speed "GPU" run.

Diagnosability only: no behaviour change, no new env var, and nothing
prints on a healthy run.

Validation: make check OK, clean build 0 warnings (CUDA=1 and portable),
oracle 32/32 TF + 20/20 greedy.
…alar + AVX2

The native layout of compressed-tensors 'mxfp4-pack-quantized' checkpoints:
packed [O,I/2] u8 (low nibble = even column, bit3 = sign) + scales [O,I/32]
ue8m0, w = v * 2^(s-127). Computes on the vendor bytes directly - QAT weights
(Kimi K3 routed experts) are exactly the trained values, so they are streamed
and multiplied as-is, never re-encoded. AVX2 decodes a 16-byte group with one
pshufb over the doubled-e2m1 int8 LUT; the 0.5 un-doubling rides the group
scale. Scalar and SIMD paths use the same bit-trick exponent decode so both
agree on the s=0/255 edge cases. Verified against an independent numpy
implementation of the spec (rel err 1.2e-7 = f32 noise) and byte-conventions
against compressed-tensors' pack_fp4_to_uint8/unpack_fp4_from_uint8.
… vocabs

Two additions for Kimi K3's tokenizer (tiktoken vocab, fancy-regex pattern):

- kimi Split family (sniffed via \p{Han} in the pre_tokenizer regex): the
  o200k case-aware rules plus a leading [\p{Han}]+ rule, Han masked out of
  the letter classes (fancy-regex &&[^\p{Han}] intersections), and no '/'
  tail in the punctuation rule. A Han codepoint can only match the Han rule,
  so Han-free text tokenizes exactly like o200k minus the '/' tail.

- rank-BPE: when model.merges is empty, bpe_piece merges the adjacent pair
  whose CONCATENATION has the lowest vocab id - tiktoken's own algorithm,
  exact by construction. Merge-list recovery from ranks is provably lossy
  (the standard max-rank-minimizing reconstruction mis-merges 'newlines'
  as new+l+ines), which is why K3's generated tokenizer.json ships no merges.

tests/test_tok_kimi.c + tools/k3_tokenizer.py --ctest cross-check the C
encoder against tiktoken: exact on all 18 corpus cases (Chinese, kanji vs
kana, Korean, CRLF, contractions, emoji, mixed-script boundaries). Existing
families are untouched: merges-bearing tokenizers keep the merges path.
New sibling engine for Kimi K3 (2.8T / 104B active, 93 layers), following the
one-engine-per-family pattern (colibri.c untouched). Shares st.h / json.h /
tok.h / quant.h. make kimi_k3; ./kimi_k3 <model_dir> "prompt" --ngen N.

- 69 KDA layers: SiLU(causal-conv4) projections, L2-normed q/k, per-channel
  bounded decay gk = -5*sigmoid(exp(A_log[h])*(f_b(f_a x)+dt_bias)), delta
  rule S=(I-beta k k^T)Diag(e^gk)S + beta k v^T, per-head RMSNorm x sigmoid
  full-rank output gate. Checkpoint A_log [128] = per-head [96] zero-padded.
- 24 gated MLA layers, absorb form, NoPE (nothing is rotated; the 64 extra
  dims are cached raw), sigmoid output gate before o_proj.
- AttnRes: the residual stream is replaced by softmax mixes over block
  snapshots (layers 0,12,...,84) + the running prefix sum - twice per layer
  and once before the head, scored against res_norm.w*res_proj.w, raw-entry
  mixing in fp32.
- Stable LatentMoE: sigmoid router + score-correction bias, top-16,
  raw-score renormalization; latent 7168<->3584 projections around the
  experts; RMSNorm on the aggregate; 2 fused shared experts; SiTU-GLU
  (4 tanh(g/4) sigmoid(g) * 25 tanh(u/25)).
- Routed experts stream from the safetensors shards as native MXFP4 through
  a per-layer LRU (K3_EXPERT_GB): one pread per expert (the six tensors are
  stored back-to-back - measured), loads issued in disk-offset order
  (experts are NOT id-ordered in the shards - measured), multi-drive shard
  split via K3_DIRS (st_init_multi).
- Non-expert BF16 is quantized at load (K3_BITS int4-g64 / int8 / f32,
  K3_MLA_BITS, K3_HEAD_BITS) or read pre-quantized from a repacked
  container (U8 + .qs sidecars, tools/k3_repack.py) - container start
  measured 30.4s -> 0.6s on two layers.

Validation (docs/kimi_k3.md): with injected inputs and f32 weights the
engine matches an independent numpy reference (tools/k3_ref.py) on real
checkpoint weights across all four layer types to rel-L2 <= 2.2e-6;
container-vs-load-time quantization is bit-identical (layer-0 trace equal);
tokenizer exact vs tiktoken. Known limits: single-token stepping (sequential
prefill), CPU only, no chat template.
- k3_tokenizer.py: tiktoken.model -> tokenizer.json (byte-level vocab, EMPTY
  merges on purpose -> tok.h rank-BPE, kimi Split regex, 256 added tokens
  from tokenizer_config). --check replays rank-BPE in python vs tiktoken;
  --ctest drives tests/test_tok_kimi against tiktoken. 18/18 exact.

- k3_ref.py: independent numpy implementation of the K3 stack (KDA
  recurrence, gated NoPE MLA, AttnRes, LatentMoE with MXFP4 dequant) reading
  tensors straight from the HF shards; gen/run/cmp subcommands produce and
  compare the engine's K3_TRACE hidden-state dumps. This is the reference
  the engine is validated against (rel-L2 <= 2.2e-6 at f32 on real weights).

- k3_repack.py: one streaming read of the HF snapshot -> ideal ingestion
  container, so a disk-to-disk copy IS the conversion. Experts byte-identical
  in id order (six tensors back-to-back = the engine's slot layout, one pread
  per expert), big BF16 mats quantized with the ENGINE-EXACT algorithm
  (int8/row or int4-g64, --bits/--mla-bits/--head-bits) into U8 + .qs f32
  sidecars, sensitive smalls passed through, vision dropped, spec-valid
  safetensors (no holes) + regenerated index, per-shard resume, --verify-full
  re-reads every expert byte. Kills the engine's 10-15 min load-time
  quantization; quantized bytes verified bit-identical to the load-time path.
  ~1.56 TB source -> ~1.50 TB (int8) / ~1.48 TB (int4).
Full-model bring-up on a 62 GiB box exposed three streaming-path issues:

1. Expert loads were sequential buffered preads; with ~55 GiB of resident
   weights the page cache has no headroom and reclaim throttled reads to
   ~1.8 GB/s (drive: 7.1 GB/s O_DIRECT, 2.9 buffered — measured). A token's
   misses now load in parallel into working-set slots and read O_DIRECT
   through st.h's twin fds (aligned-window + tiny buffered tail; K3_DIRECT=0
   reverts to buffered + WILLNEED). Expert reads: 6.3 GB/s.
   Decode: 26-28 -> 9.2-10.7 s/token on the 93-layer model.
2. The LRU cap floor was topk, silently committing topk*92 slots (~26 GiB)
   regardless of K3_EXPERT_GB; experts are consumed one at a time, so the
   floor is now 1 and the budget is honored.
3. Explicit K3_BITS=4 on an int8 container downcasts to int4-g64 at load:
   ~35 vs ~57 GiB resident (fits next to a desktop session), same decode
   speed once I/O is fixed; unset K3_BITS keeps container bits (default
   unchanged).

93-layer sanity (greedy, repacked int8 container): 'The theory of general
relativity was developed by' -> int8: 'Einstein when he felt that the theory
of special relativity was insufficient...', int4: 'Einstein in 1915, and it'.
Init 42 s (int8) / 131 s (downcast pass).
Teacher-forced bit-width comparisons (with --ngen 0): run the same --ids
twice under different K3_BITS and diff the logit streams offline. Used for
the int8-vs-int4 dense-side quality evaluation on real text.
The float kernel converts every decoded weight nibble to float before the
FMA; at 25.8 GB of packed expert bytes per K3 token that path measured
~4.4 GB/s effective. The doubled e2m1 values are exact int8 (same LUT), so
a 32-group reduces to maddubs(|w|, sign(x,w)) + madd — the dot_i4i8 idiom —
with the group scale folded once per group:
    y = sum_g idot_g * 2^(e8-127)*0.5 * xscale_g
Activations quantize per 32-group to int8 (~0.4% group noise, aligned with
the weight groups so scale folding is exact). Unit test vs the float kernel:
rel-L2 3.5e-3 on random data. On the full model: teacher-forced real-text
ppl 2.684 vs 2.694 (float), argmax agreement 98.4%, KL mean 0.003 — free at
text level, 3x faster (expert compute 5.8 -> 1.9 s/token). Scalar fallback
included; I%32!=0 falls back to the float kernel.
Decode 9.4 -> 5.2 s/token (and 3.0 opt-in), on top of the O_DIRECT work:

- K3_PIPE (default 1): a small loader-thread pool (K3_LOAD_THREADS, 4) runs
  the expert preads while the compute loop chews already-loaded experts —
  expert j's matmuls overlap expert j+1's read. t_eload now reports the
  UN-hidden wait (2.5 s/token; pure I/O would be 3.8).
- K3_IDOT (default 1): expert matmuls via quant.h's new matmul_mxfp4_i8.
  Real-text gate: ppl 2.684 vs 2.694 float, agreement 98.4% — free.
- K3_TOPP (default 0 = off): drop the low-weight tail of the top-16 to
  cumulative weight p, renormalized (GLM TOPP semantics) — the only lever
  cutting I/O and compute together. At p=0.7 it keeps ~8.1/16 experts:
  3.0 s/token, but ppl 2.811 vs 2.694 (dNLL +0.043) with accuracy unchanged
  at 73.4% — measurable, so it stays OPT-IN; gate any setting with a
  K3_LOGITS A/B first.

Full-model numbers, greedy sanity text unchanged. make check green.
Prefill now runs layer-major over K3_CHUNK-token chunks (default 32):

- every dense matmul batches over the chunk, so each weight matrix streams
  from RAM once per chunk instead of once per token;
- the MoE builds the chunk's expert union per layer and loads each unique
  expert ONCE (position lists via counting sort), feeding the same pipelined
  block loader as decode. Measured on real text at C=32: 2.7x dedup —
  neighbouring tokens share experts far more than QB-flat routing predicts
  — 9.6 instead of 25.8 GB streamed per token;
- the lm_head runs only on the chunk's last token (K3_LOGITS still dumps
  every position);
- sequential state (KDA recurrence, MLA cache append, AttnRes bookkeeping)
  advances per token inside each layer in the original order, so chunked
  results are BIT-IDENTICAL to token-at-a-time: the 125-position
  teacher-forced logit streams at C=32 vs C=1 match exactly (max abs diff
  0.0). K3_TRACE forces C=1 (trace rows are token-major by contract).

The KDA delta-rule state sweeps get explicit AVX2 (mul+fmadd over the
128-wide state rows, scalar fallback when head_dim%8!=0): attention
0.9 -> ~0.5 s/token; vs the pre-SIMD logits: 100% argmax agreement,
ppl 2.6841 unchanged at 4 decimals.

Full-model prefill: ~5.3 -> 2.0 s/token (125-token prompt, C=32, int8
dense, K3_LOGITS on — pure generation prefill is slightly better still).
Decode unchanged (same C=1 path). make check green.
…hannel

K3 ships its chat renderer as encoding_k3.py: an XTML structure where only
<|open|>/<|close|>/<|sep|>/<|end_of_msg|> are special tokens and tag names /
attributes are ordinary text, encoded as standalone segments. A turn is

  <|open|>message role="user"<|sep|>TEXT<|close|>message<|sep|><|end_of_msg|>

and the generation prompt opens the assistant message plus its STRUCTURAL
thinking channel (<|open|>think<|sep|>; K3_THINK=0 opens <response> directly
— in thinking mode the channel exists in every assistant message, which is
the preserved-thinking property).

The C builder mirrors the reference segment-for-segment (segment boundaries
are token boundaries, so per-segment BPE matters) and is verified TOKEN-EXACT
against encoding_k3.py + tiktoken for system/no-system and thinking/
non-thinking prompts. The CLI hides the structure at decode and prints
[think]/[response] banners; <|end_of_msg|> is already the configured eos.

Full-model greedy check ('how many r in strawberry'): the model reasons in
the think channel (spells the word, finds positions 3/8/9), closes it, opens
response, answers '3 times' — native use of the format end to end.

Single system+user turn only for now (no multi-turn CLI, no tool-call
rendering); make check green.
Kimi K3 engine: native-MXFP4 expert streaming, KDA + gated NoPE MLA + AttnRes + LatentMoE (#658)
olmoe: decode goes disk-bound whenever the expert LRU misses
feat(cuda): say it once, loudly, when resident tensors fall back to CPU (#687)
…iner; --chat

Three things, all needed to run Inkling on a host that cannot hold its bf16
dense set.

1. CORRECTNESS — the expert cache silently computed with the wrong weights.
   moe() acquired a slot for every (token, expert) pair up front and kept the
   pointers until the compute pass. slot_acquire evicts the LRU when the cache
   is full — including slots already handed out in the same call: the pointer
   stays valid but the slot now holds a DIFFERENT expert. No crash, no warning,
   just incoherent output. Correct behaviour therefore required a cache big
   enough for every distinct expert in the batch: a prefill of 18 tokens at
   topk=6 needs up to 108 slots per layer.
   Now the routed experts are processed in ROUNDS of `cap` pairs — acquire,
   fill, compute, accumulate — so nothing is evicted while in use. The MoE
   output is a weighted sum, so accumulating in rounds is identical to one
   pass, and the cache can go down to one slot per layer (more disk reads,
   memory proportional to `cap` instead of to the batch).
   Verified on tools/make_tiny_inkling.py's transformers oracle: 36/36
   teacher-forced and 24/24 tokens, at cap=4 AND at cap=1 (where the old code
   used one slot for both experts of a topk=2 model).

2. MEMORY — optional int4-gs64 dense container.
   The pre-converted container has int4 experts but bf16 dense weights: 49.4 GB
   measured, resident, and load_w expands bf16 to f32 while loading (~99 GB
   peak), so hosts below ~64 GB die before generating. c/tools/
   convert_inkling_dense_int4.py quantizes just the dense set into a separate
   container (15.3 GB); the engine auto-detects <snap>/dense-int4g64/ and
   INK_DENSE_Q4=0 ignores it. Without the container nothing changes: the bf16
   path is untouched. Adds matmul_i4g (group-scaled int4) and matmul_i8r
   (per-row int8) plus a bounds guard in matmul_w, since the container is a
   file, not an invariant.
   Measured error vs the real bf16 weights: ~11% rel-L2 on attention/shared/
   dense-MLP at int4-gs64 (which is what 4 bits costs on this distribution),
   ~0.9% on embed/lm_head at int8. Enough for coherent output: the 975B answers
   "My name is Inkling." on a 25 GB box, 15.1 GB RSS, clean EOS.

3. USABILITY — `--chat` renders Inkling's chat template in the engine, so the
   direct CLI stops feeding an instruct model raw out-of-distribution text.
   coli chat/serve/web already render it through the gateway.

Speed is unchanged and still disk-bound: ~8 GB of expert cache against a 464 GB
expert bank is ~1.7% residency, tens of seconds per token. This makes the model
runnable and testable on a small host, not fast — docs/inkling.md says so
plainly rather than quoting the best case.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…he-fix

fix(inkling): expert cache served the wrong experts; int4 dense container; --chat
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…t it trims

`colibri.c` has had `TOPP` and `kimi_k3.c` has `K3_TOPP`; `inkling.c` was the
only engine without it, and it is the engine where it matters most. Inkling is
disk-bound on a small host: topk=6 experts x 28 MB x 66 layers is ~11 GB read
per token, and 92% of decode time is expert I/O. Every expert TOPP drops is an
expert not read.

Same semantics as the other two engines: sort the routed (weight, id) pairs
descending, keep them until the cumulative weight crosses `p`, drop the tail,
and do NOT renormalise — the dropped weight simply does not contribute.
si[] is ordered by sigmoid(logit)+bias while the weight is sigmoid(logit)
without the bias, so the weights are not guaranteed descending and the pair is
re-sorted before the cut, exactly as colibri.c does.

This is a QUALITY lever, not a free speed-up: it changes the computation. So it
is opt-in, off by default, announced at startup, and — new here — it reports
its own effect at the end of the run:

    [topp] 0.30: 36/72 routed used (50.0% trimmed, 1.00 experts/token avg)

Measured on the tiny fixture at TOPP=0.30: 50% of routed experts trimmed and
expert loads down from 47 to 27 (-43%), which is the mechanism the disk-bound
host needs. A number you can A/B instead of a claim you have to believe.

TOPP=0 (default) leaves the computation bit-identical: the transformers oracle
stays at 36/36 teacher-forced and 24/24 tokens, at cap=4 and cap=1. Values
outside (0,1) are refused with a message rather than silently clamped.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
feat(inkling): TOPP — adaptive routed-expert trimming, and report what it trims
The models table said which engines exist but the Run-it section only ever
showed GLM, so a reader had no way to know that coli picks the engine from the
model's config.json and renders that family's chat template — i.e. that the
command line does not change between models.

Adds the build targets, the identical chat/web/serve invocations for GLM,
Inkling and Kimi K3, the note that non-GLM chat routes through the local
gateway (so TUI, API and dashboard share one arch-aware template), and the two
per-model caveats that actually bite: Inkling needs --cap on a RAM-tight host,
Kimi needs ~1.6 TB for the snapshot.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@JustVugg

Copy link
Copy Markdown
Owner Author

Two additions since the description above — both already on dev, so this PR carries them:

TOPP for Inkling (#704)

colibri.c had TOPP and kimi_k3.c had K3_TOPP; inkling.c was the only engine without it, and it is the one where it matters most — Inkling on a small host spends 92% of decode in expert I/O, so every expert the router's tail contributes is ~28 MB read for little gain. Same semantics as the other two engines (sort routed pairs descending, cut at cumulative p, no renormalisation), opt-in, off by default.

New here and not in the other engines: it reports what it trimmed, so the trade is measurable rather than assumed.

[topp] 0.30: 36/72 routed used (50.0% trimmed, 1.00 experts/token avg)

Measured on the tiny fixture: 50% of routed experts trimmed, expert loads 47 → 27 (−43%). TOPP=0 (the default) leaves the computation bit-identical — oracle still 36/36 and 24/24, at cap=4 and cap=1.

Honest limit: no quality A/B on the real 975B yet. The fixture is random-init so its argmax happens not to flip when the tail is dropped — that proves the plumbing, not the quality. Documented as a quality lever accordingly.

README: one command line for all three models (3937c00)

The models table listed the engines but the Run it section only ever showed GLM, so nothing told a reader that coli picks the engine from the model's config.json and renders that family's chat template — i.e. that the command line does not change between models:

COLI_MODEL=/nvme/glm52_i4   ./coli chat
COLI_MODEL=/nvme/inkling_i4 ./coli chat
COLI_MODEL=/nvme/kimi_k3    ./coli chat
./coli web --model /nvme/inkling_i4

Plus the two caveats that actually bite: Inkling needs --cap 2 on a RAM-tight host, Kimi needs ~1.6 TB for the snapshot.

@JustVugg
JustVugg merged commit 01abef3 into main Jul 29, 2026
20 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.

[Bug]: 1.2.0 gibberish responses with Cuda A100

10 participants