Commit Graph

311 Commits

Author SHA1 Message Date
Vincenzo Fornaro af23f314fd Merge pull request #360 from woolcoxm/test/efficiency-suite
tests: efficiency suite — tiny-model regression gates + full-model optimization dossier (#359)
2026-07-20 17:09:21 +02:00
Vincenzo Fornaro 1765ed80ac Merge pull request #453 from ZacharyZcR/feat/ablation-iq3-scheme
tools: IQ3_XXS-codebook scheme in the quant ablation — settles #452's codebook decision
2026-07-20 17:09:06 +02:00
Vincenzo Fornaro bb2bae8904 Merge pull request #456 from monotophic/convert/resume-guard
convert: mirror the --indir resume params guard onto the --repo download loops
2026-07-20 17:08:50 +02:00
Vincenzo Fornaro 3ddbc655ef Merge pull request #437 from 4ny3l/fix-tool-calling-401
fix(serve): lift non-EOS stop tokens in C and fix redundant tools parsing in Python (#401)
2026-07-20 17:07:34 +02:00
JustVugg 3455eeea0a Merge remote-tracking branch 'origin/dev' into pr437
# Conflicts:
#	c/colibri.c
2026-07-20 17:04:18 +02:00
woolcoxm cf126cbcf9 fix(tests): skip efficiency tests when glm_tiny fixture is absent (CI #360)
CI runs 'make check' = dependency-free tests, no model downloads (by design,
#140). glm_tiny/ is a gitignored generated fixture, so test_inefficiency.py
hard-failed on the Windows/macOS/Linux runners with 'config.json: No such file
or directory' instead of skipping.

_engine_present() now requires BOTH glm.exe AND glm_tiny/config.json, and
_skip_reason() names exactly which prerequisite is missing so the skip is
actionable. Verified: 8 skipped (0 failed) with the fixture absent; 5 pass +
3 CUDA-skip with it present.
2026-07-20 10:49:07 -04:00
woolcoxm d43b54534f tests: efficiency suite — tiny-model regression gates + full-model optimization dossier
Two layers of efficiency coverage for the engine, both parsing the telemetry
glm.c already emits (REPLAY/PROFILE/[PROF]/CUDA-tier) but nothing previously
asserted on:

1. test_inefficiency.py — tiny-model asserted regression tests (8 tests, run
   in make test via test-python). Gate on: throughput floor, PROFILE phase
   accounting sanity, disk-wait not dominant on a resident model, CPU greedy
   determinism, and (when a CUDA build is present) CUDA init, dense VRAM
   upload, and CPU-vs-CUDA argmax agreement >= 70%. CUDA tests auto-skip with
   a clear build hint on CPU-only binaries.

2. test_efficiency_report.py — opt-in optimization dossier for a real model.
   Turns on every instrumentation flag (PROF, COLI_CUDA_PROFILE, CACHE_ROUTE,
   DISK_SPLIT, LOOKA) and prints 9 sections (provenance, throughput + tail
   latency, where-time-goes, attention breakdown, expert cache, disk I/O +
   phase split, routing quality + predictability, speculation, GPU tiers),
   each flagging inefficiency with the concrete knob to move tok/s. Never
   fails CI.

tools/efficiency.py is the shared harness: parse_run() captures every signal,
run_engine() wraps the subprocess. Reuses PROFILE_RE/SPEED_RE from
tools/benchmark_cuda_fixture.py and extends the tok/s regex to also catch the
run_text (parenthesized) format the full-model PROMPT path uses.

Makefile adds: efficiency / efficiency-cuda / efficiency-report targets.

Verified end-to-end on the full glm52_i4_g64 model (CPU + CUDA).
2026-07-20 10:49:07 -04:00
monotophic 42a5417c13 convert: mirror the --indir resume params guard onto --repo download loops
Upstream courtesy fix, found while auditing the conversion recipe for this
branch -- pre-existing in dev, not introduced by int3-g64, but directly
relevant to anyone converting for real (a #383-class gap: same failure
family as the resume/manifest work already done for --indir, and the
silent-mixing mechanism issue #355 fixed for a narrower case).

The --indir path already refuses to resume with different conversion
parameters on the same --outdir (a manifest records ebits/xbits/io_bits/
group_size/n_layers/bits_map and compares on every resume). The --repo
streaming download loops (main model, --mtp, --indexer) never got the
same guard: each shard's resume check is just `if os.path.exists(outp):
continue` -- true whether or not THIS run's flags match the flags that
produced that shard. A --repo conversion resumed with changed bits
(--xbits 3 -> 4 mid-run, say, after an interruption) would silently mix
bit-widths across shards in the same container, with no error and no log
line distinguishing it from a normal resume.

Fix: check_or_record_params(), a small shared helper mirroring the
--indir manifest's refuse-on-mismatch logic but without needing its
per-shard bookkeeping (the --repo loops already track shard completion
correctly via out-NNNNN.safetensors existence, since shard index maps
directly to output filename there -- only whether the params used SO FAR
still match needed adding). Applied to all three --repo loops with
per-mode sidecar files (.out-mtp-params.json / .out-idx-params.json /
.out-params.json) so a --mtp and a main-model conversion into the same
--outdir don't cross-check each other's parameters.

Also includes PROJ_BITS (the per-projection expert bit overrides) in the
tracked params dict on BOTH paths -- it was missing from --indir's
existing manifest too, so a resume with a changed --up-bits/--gate-bits/
--down-bits would have passed the existing guard silently.

Verified directly (no real HF downloads; --repo network paths can't be
exercised under this task's constraints): unit-tested
check_or_record_params() standalone -- fresh outdir accepts and records,
a same-params resume accepts, a changed --xbits is refused, and a
proj_bits-only change (nothing else different) is refused. Re-ran the
existing --indir dry-run end to end (convert, resume, resume-with-changed-
xbits) to confirm the manifest-based path still works correctly with
proj_bits added to its params dict.

Gates: make test-c (20/20) and make test-python (85/85) both pass.
2026-07-20 09:53:19 -04:00
ZacharyZcR ae4e31a15a tools: IQ3_XXS-codebook scheme in the quant ablation (#452 step 1)
Adds `-iq3` to the ablation harness: a faithful torch model of llama.cpp's
deployed 3.06-bpw IQ3_XXS format — 256-entry 4-dim magnitude grid
(extracted from ggml-common.h, MIT), signs factored per 8 weights with
the odd-parity constraint priced in (a violating block flips its
smallest-magnitude sign), fp16 super-scale per 256 + 4-bit sub-scale per
32 searched over all 16 codes. Nearest-grid search runs as chunked
matmul-argmin (|g|^2 - 2 q.g) — a full cdist materializes tens of GB on
a 100M-param tensor and OOMed the first run.

Measured (OLMoE-1B-7B, n=200 x hellaswag/arc/mmlu; the first four rows
reproduce the published ablations exactly):

  fp16                58.0%
  int4 per-row        48.7%   (-9.3pp, the shipped container's scheme)
  int3-g64            50.5%   (-7.5pp)
  int3-g64-e8-rot     51.5%   (-6.5pp, simulated rate-scaled ball)
  int3-iq3            49.3%   (-8.7pp)
  int3-iq3-rot        51.5%   (-6.5pp)

The deployable IQ3 codebook plus rotation exactly ties the simulated E8
ball — that settles #452's codebook decision toward the IQ3-style block
structure, with rotation mandatory (worth 2.2pp on this codebook).
2026-07-20 12:55:44 +08:00
Vincenzo Fornaro e9b36141a4 Merge pull request #449 from cdhdt/fix/rss-guard-uaf
rss_guard: free the evicted expert slab under g_pilot_mx (fixes a use-after-free with the pilot prefetcher)
2026-07-20 02:29:06 +02:00
cdhdt 9c5ab39b62 rss_guard: free the evicted expert slab under g_pilot_mx (use-after-free)
rss_guard marked the victim slot eid=-1 under the lock, unlocked, and only then
freed s->slab. In that window the slot reads as {eid=-1, slab still valid} --
exactly the state pilot_realload's victim scan reuses first -- so a pilot worker
could claim it and pread into the slab while it was being freed: use-after-free,
or a double-free if the loader took its own realloc path.

Keep the free and the pointer/capacity NULLing inside the critical section, so
'slab valid' and 'slot reusable' are never simultaneously observable. The lock is
held across a free() (microseconds); workers only take it briefly for scan+reserve.

Reproduces on dev with a SINGLE pilot worker (rss_guard runs on the main thread
while the pilot worker runs in the background), whenever the RAM guard is active
(RSS_GUARD_GB, or any resolved g_ram_budget_gb).

make check 83/83, native + portable builds 0 warnings.
2026-07-20 01:27:28 +02:00
Vincenzo Fornaro 4b704823db Merge pull request #445 from ZacharyZcR/fix/pin-budget-release-host
pin: with CUDA_RELEASE_HOST the VRAM prefix must not consume the RAM pin budget — +72% on 6×5090
2026-07-20 00:03:13 +02:00
ZacharyZcR 453d1401ba pin: with CUDA_RELEASE_HOST the VRAM prefix must not consume the RAM pin budget
pin_load computed npin from the RAM budget FIRST, then carved the
VRAM-ranked prefix out of those npin slots. With CUDA_RELEASE_HOST the
prefix's host slabs are freed right after upload — so on a multi-GPU
host the top-ranked experts consumed the RAM budget without occupying
RAM, and the CPU tier pinned only the leftovers. Measured on 6x RTX 5090
(251 GB): 9,280 VRAM + only 1,721 RAM pins (32.5 GB warm) on a box whose
RAM fits ~10k more — the cold tail then paid disk on every token and the
hit rate ceilinged at 99.0% forever.

Move the VRAM budget estimate above the npin finalization and make the
release-destined prefix ADDITIVE to the RAM-derived count. Same box,
same env plus the fix:

    [PIN] placement: 9,280 VRAM + 10,176 RAM (192.5 GB warm)
    expert hit rate 100.0% (pin 100.0% + lru 0.0%)  — disk 0

    1,024-token greedy decode: 3.62 -> 6.21 tok/s  (+72%)
    warm late segment (t=768-1024): 5.11 -> 5.73 tok/s  (+12%)
    prefill: 10.4 -> 8.8 s

The whole-run gain is the LRU warmup penalty disappearing (full
residency from the first token); the late-segment gain is the steady
~0.9%-miss disk residue recovered. Non-release configs are untouched:
prefix_est stays 0 and the arithmetic reduces to today's exactly.
2026-07-20 05:15:45 +08:00
Vincenzo Fornaro 17fbdfa8f8 Merge pull request #393 from ZacharyZcR/feat/auto-tune
plan: auto-tune heuristics — MTP/PIPE/NUMA decisions from bottleneck classification
2026-07-19 22:40:53 +02:00
Vincenzo Fornaro a7ef058fc0 Merge pull request #438 from monotophic/e7/disk-class-instr
PROF: DISK-CLASS — per-load cold/warm disk instrumentation
2026-07-19 22:40:22 +02:00
monotophic 1f00142b25 PROF: DISK-CLASS — per-load cold/warm disk instrumentation (re-derived onto #391 split)
Re-derivation of e7/disk-class-instr @ de6dd6d (base caa49f7) onto
origin/dev @ 61004dc, after PR #391 split c/glm.c into c/colibri.c plus
quant.h/sample.h/kv_persist.h/telemetry.h/grammar.h. Semantic equivalence,
not a copy: same events, same accounting, re-sited onto the new tree.

Placement: colibri.c, unchanged from before the split. telemetry.h (#391)
is the dashboard/stats module (HWINFO/TIERS/EMAP/HITS protocol lines +
usage persistence) — prof_report(), expert_load_impl(), moe(), g_prof_io
and g_edisk_ns all stayed in colibri.c, so DISK-CLASS's aggregation and
printing follow them there. No relocation needed, no DEVIATION.

Read path: #362 (prefetcher-v3, 22509fc) turned out to be testbed-only
scope per its own merge message ("glm.c untouched") — confirmed zero
c/glm.c changes in that merge's diff. The seven expert_load() call sites
this patch touches (pipe_worker, expert_host_ensure, moe()'s OMP miss
loop, pilot_realload, repin_pass_limit, pin_load x2) are structurally
identical to the pre-split tree; the demand flag re-attaches at the same
sites with the same semantics (1 only at moe()'s own PIPE/OMP miss path,
0 everywhere else), so DISK-CLASS still counts demand loads only.

One real drift, unrelated to #362: #417 (cfcc742) fixed the exact "Metal
pre-routed FASE A never bumps the real elast/eaccess_clock" defect this
feature's comments described as a documented, deliberately-unfixed
upstream issue — the real clock now ticks in FASE A too. The private
elast_dc/eaccess_clock_dc clock is kept anyway: its job was never only
to route around that freeze, it also snapshots pre-bump state so a
call's own routing bump can't contaminate its own classification, and
keeping DISK-CLASS's bookkeeping fully separate from stock elast state
is what makes "byte-identical with PROF=0" provable by construction
instead of by argument. Code comments referencing the old defect are
updated to reflect the fix (historical note + #417/cfcc742 pointer)
rather than describing a bug that no longer exists.

Gates: make glm METAL=0 and METAL=1 both clean, zero warnings (matches
stock 61004dc, also built clean with zero warnings for comparison).
make test-c: 0 failures. make test-python: 77 tests, OK.

Authored by Fable 5 in Claude Code, analysis in partnership with
@Monotophic.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-19 15:42:57 -04:00
Colibri Developer 26bd8b403a fix(engine): filter non-EOS stop tokens in serve mode to prevent premature halt on tool calls
GLM-5.2's config defines three stop tokens: <|endoftext|>, <|user|>, and
<|observation|>. In serve mode, when the model generates <tool_call> blocks,
int4-quantized logit noise can cause argmax to pick a <|user|> or
<|observation|> token ID, immediately stopping generation.

The <|user|> and <|observation|> tokens are role markers handled by the
Python API server, not the C engine. Filter them out in stops_arm() when
SERVE=1, keeping only <|endoftext|> as the stop token.

- Add SERVE-mode guard in stops_arm() that retains only the EOS token
- Log the number of filtered tokens for diagnostics
2026-07-19 13:39:11 -06:00
Colibri Developer 70fb5b00f3 fix(api): pass tools and tool_choice as parameters to generation() to prevent duplicate extraction
The generation() method was re-extracting tools from the raw request body,
bypassing the filtering that render_chat() applies when tool_choice is a
function object (forced function call). This caused parse_tool_calls() to
receive the unfiltered tool list, producing incorrect or missing tool_calls
in the response.

- Add tools and tool_choice parameters to generation() signature
- Pass them from chat_completion() after render_chat() processing
- Add early structural validation of tools in generation_options()
  for clear HTTP 400 errors on malformed input
2026-07-19 13:38:57 -06:00
Nicholas Beerbower 4586d33c60 fix: MTP-head probe uses the last expert by index, not a hardcoded 255
The has_mtp completeness probe checked for `mlp.experts.255.down_proj.weight`,
which only exists when n_routed_experts == 256. REAP-pruned checkpoints (and any
MoE with a different expert count) have fewer experts, so the probe spuriously
reported has_mtp=0 and disabled MTP speculative decode even though the head was
present and complete. Probe `mlp.experts.<n_experts-1>` instead.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-19 10:54:01 -04:00
ZacharyZcR 8a9a0fca4d plan: auto-tune heuristics — bottleneck classification + parameter decisions
Extend resource_plan to classify the hardware into bottleneck regimes
(disk / memory / mixed / compute) and derive tuning knobs automatically:

  MTP:   off when compute-bound (42% loss at full residency, #389)
         or disk-bound with <90% hit (union growth adds reads)
  PIPE:  COLI_CUDA_PIPE=1 single-GPU, =2 multi-GPU, PIPE=1 CPU disk
  NUMA:  selective interleave for GPU hosts, blanket hint for CPU-only
  PIN:   PIN_GB=all when fully resident + no GPU
  OMP:   COLI_NO_OMP_TUNE=1 for Metal (spin steals GPU power)

`coli plan` now shows an auto-tune section with each knob and its
reason. `environment_for_plan()` applies them via setdefault so
explicit user settings always win.

plan version stays at 2 (additive fields: bottleneck_class,
projected_hit_rate, tune). 7 new tests covering all regimes.
2026-07-19 21:20:00 +08:00
ZacharyZcR 083fda5b0a fix: update test_logit_nan to include colibri.c instead of glm.c 2026-07-19 21:18:33 +08:00
ZacharyZcR bc69a9a6d0 fix: remove duplicate argmax_v — use NaN-safe version from sample.h 2026-07-19 21:15:54 +08:00
ZacharyZcR f853ea8a0b refactor: split glm.c into colibri.c + 4 header modules
Rename glm.c → colibri.c and extract four self-contained modules
into header-only files (same pattern as st.h/tier.h/grammar.h):

  quant.h      (672 lines) — SIMD matmul kernels, quantization
  sample.h     (143 lines) — RNG, top-p sampling, stop-set
  kv_persist.h (121 lines) — .coli_kv disk persistence
  telemetry.h  (189 lines) — dashboard protocol, stats, usage

Main engine file shrinks from 6588 to 5396 lines (−18%).

Build system: primary target is now colibri$(EXE); `make glm`
remains as a phony alias for backward compat. CI, setup.sh,
coli CLI, and all 10 test files that include the engine are
updated. make check passes (C + Python, 73 tests, zero warnings).
2026-07-19 21:12:04 +08:00
JustVugg cfcc742591 metal: advance the LFRU recency clock on the GPU-prerouted decode path (#417)
On Metal, when routing is precomputed on the GPU (g_pre_idx), the moe fast
path bumps eusage/ehit/eheat for the selected experts but skips the one thing
the full CPU router does at the equivalent site: elast[layer][e] =
++eaccess_clock. So the session-local recency clock advances during prefill
(full router) but freezes the moment GPU-prerouted decode starts, and REPIN's
tier_pick_lfru() tie-breaker then runs on stale recency for the rest of the
run. Mirror the exact update the non-Metal path already does. Inside
#ifdef COLI_METAL, so CPU/CUDA are untouched; elast only feeds the LFRU
eviction heuristic, so this cannot affect output, only which experts REPIN
keeps warm.

Found and reported by @monotophic with a source-level trace repro
(ELAST_TRACE). Fix is inspection-verified against line ~3055; needs a
Metal build to exercise end-to-end.

Closes #417

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-19 14:58:32 +02:00
JustVugg 72e36772f5 security: reject malformed model tensors at the untrusted-mirror boundary
Colibri loads model directories and safetensors from mirrors it does not
control, so the file's declared shapes and byte spans are attacker-influenced
input. Three memory-safety holes on that boundary, independently confirmed
(incl. a from-scratch adversarial audit that re-derived the same two) and
present in the shipped v1.0.0:

- st.h st_read_f32: numel came from the shape, nbytes from the offsets, with
  no cross-check. A crafted tensor whose shape inflates numel past nbytes made
  the BF16/F16 loop read past the malloc'd raw buffer and the F32 memcpy write
  past the caller's config-sized destination (heap OOB read + write). Now
  enforce numel*esz == nbytes before any copy.
- st.h header parse: the shape product could overflow int64 to a small/negative
  numel that would then pass the cross-check. Guard each multiply.
- glm.c qt_resolve_fmt (new, replaces the three duplicated "?1:?2:3" fmt sites
  in qt_from_disk and both expert_load arms): the old inference SILENTLY fell
  to int2 for any unrecognized weight byte count, so a too-short weight became
  a valid int2 whose matmul read O*I nibbles past the buffer; and an oversized
  scale array overflowed the per-row t->s. Now the weight bytes must match a
  known int8/int4/int2 layout and the scale array must match the expected
  per-row (O) or grouped (O*ng) cardinality, else refuse.
- glm.c config/generation_config slurp: unbounded ftell -> malloc(n+1) gave a
  hostile file a load-time OOM, and on malloc failure b[got]=0 was a NULL
  deref. Cap at 256 MB and NULL-check.

Verified: TF token-exactness unchanged on every quant format (full-precision
32/32, int4 11/32, int2 1/32, mix 5/32 -- byte-identical to the pre-change
binary); fmt=4 grouped path preserved (the scale check is by construction the
same condition detect_group_size already imposed); a hand-crafted hostile
safetensors is refused cleanly; ASan+UBSan clean on legit and hostile loads
(only the pre-existing intentional startup leaks remain).

These are the C trust-boundary items of #368, landed as a minimal standalone
fix; the server-side and build items of that PR follow via its rebase.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-19 13:06:35 +02:00
JustVugg 22509fccde Merge pull request #362 from EgonRuiter/prefetcher-v3
feat(prefetch): async expert prefetcher v3.2 for the OLMoE testbed

Testbed-only scope: olmoe.c + tools/oracle files; glm.c untouched (the
production engine already ships the equivalent techniques: coalesced slab
preads, PILOT lookahead, persistent PIN). Trivial .gitignore conflict
resolved keeping both sides.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-19 12:40:43 +02:00
Vincenzo Fornaro e25aeecc4c Merge pull request #410 from JustVugg/p403-rss-guard
glm: measured-RSS guard — the RAM budget enforces itself at the safe point (#403)
2026-07-19 11:29:11 +02:00
JustVugg c90e2cc438 glm: measured-RSS guard — the RAM budget enforces itself at the safe point (#403)
cap_for_ram's projection is an estimate: on the GB10 (#403) long generations
overshot it by ~40 GB (projected 74.4, real 115.6) and the kernel killed the
engine three times. Run D of the issue proves a low cap CONTAINS the growth;
this guard does that automatically, keyed on MEASURED RSS instead of the
projection.

At the repin safe point (no moe in flight), every ~16 emitted tokens: if RSS
exceeds the resolved budget (RAM_GB/auto, or an explicit RSS_GUARD_GB
ceiling), free the least-used LRU expert slabs in place and lower ecap so the
cache cannot regrow. Slabs are >128 KB so glibc returns the pages to the
kernel immediately -- RSS actually drops.

Eviction never compacts the array: with PILOT_REAL the pilot worker holds
pointers into ecache[] across its preads, so the slot stays in place with
eid=-1/used=0 (first candidate for reuse); reserved slots (eid<0) are never
touched and victim selection happens under g_pilot_mx. resident_bytes is left
alone: LRU slots are never accounted there (only pin + dense).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-19 11:02:31 +02:00
JustVugg 0f606bc446 test: skip the tools e2e suite on Windows (shebang mock engine)
CreateProcess cannot exec a shebang script, so the gateway exits during
setUpClass on the windows CI job. The gateway logic under test is
platform-independent and stays covered by the POSIX jobs.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-19 11:01:26 +02:00
JustVugg 2122c004d9 serve: end-to-end tool-calling regression test + unparsed-marker diagnosis (#401)
The gateway's tool-calling path had unit coverage (parse_tool_calls,
render_chat) but nothing exercised the real subprocess wire protocol or the
HTTP surface a coding client actually hits. #401 reports plain-text replies
where tool_calls were expected; every documented path checks out, so pin the
whole path down with a mock engine speaking SUBMIT/DATA/DONE and assert:

- non-stream: tool_calls populated, finish_reason tool_calls, no raw markers
- stream: markers suppressed across 20-way chunk splits, tool_calls delta
- tool-result round trip: <|observation|><tool_response> rendering, text reply
- no tools: plain text untouched

Also emit a stderr diagnosis when tools are declared and tool-call markers
are present in the reply but the strict parse matches nothing (typically
quantization-mangled output) pointing at COLI_TOOL_SALVAGE=1 -- the likely
field condition behind #401.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-19 10:56:00 +02:00
JustVugg d94c68149d Merge pull request #352 from woolcoxm/fix/test-stops-windows
fix(test): make test_stops build on Windows (mkdtemp compat shim)

Conflict with #366's getenv_utf8 in compat.h resolved by keeping both blocks.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-19 10:42:41 +02:00
Vincenzo Fornaro 66b5e57bc4 Merge pull request #397 from NeuralNotwerk/fix/mtp-int8-repair
tools: repair int4-converted MTP heads in place; warn on --mtp --ebits <8
2026-07-19 10:40:07 +02:00
Vincenzo Fornaro 3f99d2bbb7 Merge pull request #395 from Stonki13/fix/windows-cuda-detection
Fix CUDA detection on Windows (coli/doctor.py always report CPU-only)
2026-07-19 10:40:01 +02:00
Vincenzo Fornaro 19bab420a0 Merge pull request #404 from anrasi/fix/indir-meta-resume
convert(fp8->int4): --indir copies the full metadata set and resumes interrupted passes (#383)
2026-07-19 10:39:56 +02:00
Vincenzo Fornaro 36d389bf50 Merge pull request #402 from anrasi/fix/coli-run-think
coli: honor THINK=1 in run mode
2026-07-19 10:39:50 +02:00
JustVugg 6690c4f58e Merge branch 'p394' into trialsafe 2026-07-19 01:28:16 +02:00
JustVugg e5b1e19022 Merge branch 'p366' into trialsafe
# Conflicts:
#	.github/workflows/ci.yml
2026-07-19 01:26:51 +02:00
JustVugg 4d780e2b46 Merge branch 'p378' into trialsafe 2026-07-19 01:25:40 +02:00
Stonki13 d2d3a7b559 Fix CUDA detection on Windows: cuda_binary()/cuda_linkage() always returned False
Both coli's cuda_binary() and doctor.py's cuda_linkage() detect CUDA support
by running `ldd` on the engine binary and looking for a linked libcudart —
but that's Linux-only (cuda_binary() checks sys.platform != "linux", and
cuda_linkage() checks os.name != "posix", both short-circuiting to False on
win32). Windows CUDA_DLL=1 builds never link libcudart at all: glm.exe loads
coli_cuda.dll dynamically via LoadLibrary at startup (backend_loader.c), so
there's no import-table entry for ldd/dumpbin to find in the first place.

The practical effect: `coli doctor` always reported "NVIDIA GPU detected but
the engine is CPU-only" on Windows, and `coli run/chat/serve --gpu ...`
always hard-exited with "--gpu needs the CUDA build" — even on a correctly
built CUDA_DLL=1 binary with coli_cuda.dll sitting right next to glm.exe.

Fix: on win32, detect a COLI_CUDA build by scanning glm.exe for the marker
string "[CUDA] mode: routed experts", which only exists in code compiled
under #ifdef COLI_CUDA (see glm.c's cuda init block), then confirm
coli_cuda.dll actually sits next to the binary — mirroring the Linux
"linked but missing" distinction. Linux/macOS detection is unchanged.

Verified on Windows 11 with mingw-w64 GCC 16.1.0 + CUDA 13.2 + RTX 5080:
`coli doctor` now reports "CUDA engine and devices are available", and
`coli run --gpu 0 --vram 8` populates VRAM (confirmed via the engine's own
"[CUDA] resident set: N tensors, X GB VRAM" runtime log) instead of exiting.
2026-07-18 23:18:16 +02:00
ZacharyZcR 05bba7994c release: version infrastructure + GitHub Release workflow
- c/version.py: single source of truth (__version__ = "1.0.0")
- coli: reads version.py, banner shows dynamic version, --version flag
- .github/workflows/release.yml: tag push triggers cross-platform build
  (Linux x86_64, macOS ARM64, Windows x86_64) and creates a GitHub
  Release with packaged binaries + changelog notes
- CHANGELOG.md: v1.0.0 baseline documenting all shipped features

To cut a release:
  1. bump c/version.py
  2. add a CHANGELOG section
  3. git tag v1.0.0 && git push --tags
2026-07-19 05:01:41 +08:00
recviking d6676c10e9 tools: repair_mtp_int8.py — fix int4-converted MTP heads in place; warn on --mtp --ebits <8
Per-row int4 quantization of the MTP head's eh_proj [D,2D] zeroes its ENTIRE
embedding half: the tensor's two column halves differ in scale by ~20-30x per
row (embedding-half absmax ~0.05, hidden-half ~1.5 on GLM-5.2), so a single
per-row scale (absmax/7) puts every embedding-half weight below half a
quantization step and np.rint rounds it to exact zero (packed bytes 0x88).
The draft head then cannot see the input token and MTP acceptance collapses
to ~0% — the mechanism behind issue #8's measurement (int4: 0-4%; int8:
39-59%), and the reason --mtp already defaults to --ebits 8.

This showed up in the wild: a published pre-converted container had its MTP
shards made with an explicit --ebits 4 and drafts were pure garbage on every
backend (deterministic, data-side; verified byte-for-byte by reproducing the
published bytes with quant_int4 on the official BF16 rows).

- tools/repair_mtp_int8.py: repairs such a container IN PLACE — finds the MTP
  layer's per-row-int4 dense tensors (eh_proj, q/kv/o projections, shared
  experts; routed experts untouched), re-downloads only those from the FP8
  source repo (~355 MB of HTTP range reads, no torch, no token), requantizes
  at int8 with quant_int8's exact math, and rewrites the shards atomically
  with *.bak-int4 backups. --dry-run to inspect. The engine picks up int8
  automatically (qt_from_disk detects format by blob size).
  Validated on GLM-5.2 744B: MTP acceptance 0% -> 100% (greedy), 1.11 -> 3.20
  tokens/forward, decode 0.18 -> 0.30 tok/s (Metal, 4-bit KV).

- convert_fp8_to_int4.py: print a warning when --mtp is combined with
  --ebits <8 and per-row scales (the exact footgun above); --group-size 128
  remains a valid int4 alternative since group scales give the embedding
  half its own scale.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-18 16:27:29 -04:00
andres-gb10 308e269f46 convert(fp8->int4): --indir copies the full metadata set and resumes interrupted passes (#383)
Two gaps in the local path, both raised in #383:

- The main --indir pass copied only config.json while the download path
  copies four files; without tokenizer.json the converted container can't
  run chat/serve. Copy the same four, print what was copied, and warn
  when the source is missing any (tokenizer.json called out explicitly).

- Local passes restarted from shard 0 on every rerun. out-NNNNN names
  count EMITTED shards, not input indexes (shards with no relevant
  tensors emit nothing), so the download path's exists-check can't be
  mirrored directly: a sidecar manifest per prefix records
  input -> output (or empty) plus the conversion parameters, written
  atomically after each shard. Rerun skips what matches and refuses a
  parameter mismatch on the same outdir instead of mixing containers
  (the #355 failure mode).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-18 21:11:40 +01:00
andres-gb10 799fe41f88 coli: honor THINK=1 in run mode
coli run hardcoded the nothink template (<|assistant|><think></think>),
so THINK=1 had no effect in one-shot runs while serve mode honors it
(glm.c serve template picks <think> vs <think></think> from the same
env var). Pick the template the same way here.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-18 16:45:19 +01:00
bokiko 6a2ab47f3b convert: resolve --mtp/--indexer shards from the local index instead of scanning all of them (#383)
When --indir contains model.safetensors.index.json, the head/indexer
passes convert only the shards that actually hold the requested tensors
(3 instead of scanning 141 for --mtp — every empty scan still opens a
5 GB shard). Prints the selection in the [PLAN] block. Without the index
the full scan is unchanged.

Output is byte-identical to the unfiltered path (sha256-verified on the
GLM-5.2 FP8 head shards, with a decoy shard proving the filter excludes
rather than reorders).

Co-Authored-By: Claude <noreply@anthropic.com>
2026-07-18 15:15:26 +03:00
Vincenzo Fornaro caa49f7fa0 Merge pull request #384 from ZacharyZcR/perf/auto-numa
plan NUMA interleave on multi-socket Linux
2026-07-18 12:52:08 +02:00
Vincenzo Fornaro 3fed654473 Merge pull request #372 from ZacharyZcR/feat/paged-ragged-kv
cuda: keep paged ragged KV resident across decode steps
2026-07-18 12:48:07 +02:00
JustVugg 4fcbea9056 Merge branch 'p370' into t370
# Conflicts:
#	c/Makefile
#	c/glm.c
2026-07-18 12:41:07 +02:00
ZacharyZcR 9c17dc6d29 plan NUMA interleave on multi-socket Linux 2026-07-18 18:38:40 +08:00
ZacharyZcR 6d3a6168e4 cuda: keep ragged KV resident across decode steps 2026-07-18 18:34:46 +08:00
JustVugg f8e4dc95b5 serve: honor --ngen when the client omits max_tokens; convert: print the resolved plan (#382, #383)
Two operator-surprise fixes:

openai_server.py (#382, LordMZTE): a request without max_tokens defaulted to
min(256, limit) — so `coli serve --ngen 32768` still cut every answer at 256
tokens for clients that omit the field. The operator's configured budget IS the
default now; generation still ends at EOS, so it's a cap, not a target.

convert_fp8_to_int4.py (#383, bokiko): the resolved conversion plan (mode,
source, ebits/io/x bits, grouped-vs-per-row) prints as a [PLAN] line before any
work. The two traps in #383 — --mtp defaulting ebits to 8 with the grouped
branch silently gated off, and --mtp appearing to ignore --indir — are already
defused by the #355 fix (the --mtp pass emits ONLY head tensors on the local
path), but a 3.5-hour job must show its plan at second 1, not in a post-hoc
size sanity check. bokiko's exact invocation now prints:
  [PLAN] mode: MTP head only | source: local ./fp8 | experts 8-bit, ... |
         PER-ROW (grouped branch needs bits<=4; ebits=8 disables it)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-18 12:28:27 +02:00