Commit Graph
206 Commits
Author SHA1 Message Date
Constantin cb87a62e82 ple: read the n-gram table at full BF16 precision (#586)
The full checkpoint stores the table as BF16 (320-byte rows, 102.4 GB), the source of record; the engine gets it as
IQ4_NL (about 8% off per row) or FP8. A bfloat16 is the top half of a float32, so widening is a shift: exact for
every value, no rounding and no scale. One entry in the PLE format list (type BF16, self-describing: no scale, no
metadata) and bf16_dequant_row; the direct reader takes the 320-byte row like any other width, and
PLE_ROW_BYTES_MAX is 320.

ple_reader_test checks the widening is exact over the patterns a lossy path breaks on (subnormals, +-inf, both NaNs);
the generic every-format test covers a synthetic BF16 GGUF through both readers.

Not in this commit: the PR's rename of tools/ple_fp8_pack.py (nothing needs it) and any setup option. Whether a
table format other than IQ4_NL is worth offering is a quality question (see the measurement in the 0.1.40 notes).

Ported by the maintainer from constantindjonkam's PR #586.
2026-10-05 12:18:48 +02:00
saikiran-rs 16bdc7073d native experts: Q4_0 and Q4_1 GPU kernels, Q4_0 PLE table
A plain llama-quantize Q4_0 file of this model (bartowski's Qwen3.8-Flash-Next-Q4_0 and
Swift-1.5 Q4_0: gate/up Q4_0, down Q4_0 on 42 layers and Q4_1 on 6, a Q4_0 PLE table and
token embedding) was refused at start: no GPU kernels for Q4_0/Q4_1 experts, and the PLE
reader took only IQ4_NL, Q5_0 and FP8. Follows #222 (Q4 to Q6 quantizations).

- Q4_0: Fmt<2> with the codes centred before dp4a (q - 8 as signed bytes), the exact
  integer form ggml-cpu's q4_0 x q8_0 dot uses, rather than llama.cpp's CUDA chain
  (uncentred codes minus 8 x the q8_1 float sum): on a real Q4_0 expert the CUDA chain
  measured 1.4e-2 relative error against the float product, the centred form 5.3e-3.
- Q4_1: Fmt<3>, the Q5_1 chain without the fifth bit and the same min-term choice.
- dq_q4_0 / dq_q4_1 for the prompt path's dequant fallback; row bytes, is_iq and the
  GU/D/MMVQ format lists.
- PLE: Q4_0 rows (90 B, as IQ4_NL) through the same reader: one entry in the PLE format list (ple_format_test and
  ple_reader_test cover it).
- docs/UNSLOTH_Q4.md: a Q4_0 section with the measured run.

native_expert_parity --synthetic (now in the CMake matrix): q4_0/q4_0 CPU 1.29e-2, GPU
1.29e-2, CPU-GPU 1.1e-7; q4_0/q4_1 CPU-GPU 3.2e-4; GPU dequant bit-exact against
to_float. The existing pairs are unchanged. On bartowski's file (revision 928589f), layers
0 and 5 (Q4_1 down) and 20 and 47: CPU and GPU 1.1-1.3e-2, CPU-GPU 1.1e-7 (Q4_0 down) and
5e-4 to 1.1e-3 (Q4_1 down). Served on one RTX 3060 (all experts in RAM, 131K context):
24,461-token prompt 787 tok/s read, 24.0 tok/s out; 67,934 tokens 737 / 26.4, the hidden
fact found.

Ported onto the format table by the maintainer (the PLE part of the PR is the Q4_0 table entry).
2026-10-05 12:18:48 +02:00
Anonandksanislo 74945954fe ple: read Q8_0 and Q5_1 n-gram tables (#651)
Ordinary GGUFs ship per_layer_token_embd.weight in other formats than IQ4_NL: Unsloth's UD-Q6_K_XL and
Swift-1.5 Q4_K_L as Q8_0 (170-byte rows, ~54 GB), a Q5_K_M finetune as Q5_1 (120-byte rows). The engine refused
both at start.

- dequantize_q5_1 in strata/artifact/dequant.hpp (ggml's dequantize_row_q5_1: unsigned 5-bit codes times d plus the
  block minimum m); Q8_0 rows use the existing dequantize_q8_0
- two entries in the PLE format list (the table-driven commit before this one): Q5_1 and Q8_0, on both readers;
  PLE_ROW_BYTES_MAX grows to 170
- tests: dequant_q5_1_test (synthetic blocks against ggml, no model, no GPU), dequant_bf16_test knows Q5_1,
  ple_q5_parity takes Q5_0, Q5_1 or Q8_0 tables and checks both the mapped and the direct reader

Ported onto the format table by the maintainer from anon761's PR #651 (the --ple-io ram fault-in is the next
commit). Quality of the Q8_0 table: ksanislo measured 0.53% error against the BF16 table, FP8 2.66%, IQ4_NL 7.59%.

Co-authored-by: ksanislo <[email protected]>
2026-10-05 12:18:47 +02:00
Niko1221 ce62f5058f ple: a table-driven PLE format (enum, row bytes and a dequant per type)
PleTable had one bool per format (q5_0, fp8) and an if-chain in decode(), open() and format(). Now one list,
ple_formats(), holds one entry per table type: its GGUF type name, the bytes of a 160-value row, whether it
carries a scale and its dequantizer. open(), format(), the row size, the error string and the row buffers all
read it, so the next formats (Q8_0, Q5_1, Q4_0, BF16) are one entry each.

No behaviour change: IQ4_NL, Q5_0 and FP8 decode with the same functions, and the "not IQ4_NL, Q5_0 or FP8 (I8)"
error text is built from the list and reads the same.

ple_reader_test --selftest now writes a PLE-only GGUF for every format in the list and checks both readers
(direct and mapped) against the format's own dequantizer, for single rows and a 16-row token gather.
2026-10-05 12:18:47 +02:00
Niko1221 6f7f680529 verify: the all-resident graph runs only while every expert is in VRAM; a -1 raises a plan error flag (#871)
A prompt loan, a VRAM shrink or an adaptive swap marks experts -1 in the residency table.  The zero-doorbell graph
planned from the device table and returned silently on a -1, so the window ran a stale plan (!!!! at 100% residency).
refresh_ar() checks the host table before each window and runs the doorbell graph (its own captured variant) while any
expert is out; resident_plan also writes a mapped error flag the host checks after the window.
2026-10-05 11:03:50 +02:00
Strata Contributor 1d78e57bd0 cache: preserve checkpoints when split allocation fails 2026-10-05 10:00:33 +02:00
Niko1221 f5410ae2be Merge branch 'f-139b-par' into f-139-rel
# Conflicts:
#	src/core/verify.cpp
#	src/program/generate.cpp
2026-10-04 10:14:03 +02:00
Niko1221 4f5e6c841d Merge branch 'f-139b-spd' into f-139-rel
# Conflicts:
#	src/core/expert_source.cpp
#	src/kernels/cuda/iq_kernels.cu
2026-10-04 10:12:55 +02:00
Niko1221 bd34d0d87e Merge branch 'f-139b-old' into f-139-rel
# Conflicts:
#	setup.py
2026-10-04 10:12:13 +02:00
Niko1221 56501a38db #583: the byte-budget ring only where it gains; short prompts keep 0.1.39's ring
- a prompt that fits 0.1.39's auto chunk keeps 0.1.39's ring (Prefill::set_ring_budget's small_max): 4K prompts
  were 1-17% slower with the smaller ring (Coder -15%, IQ3_S -5%, IQ3_XXS -2%; 5 interleaved pairs each);
- ring slots are given up for a bigger chunk only where 0.1.39's rule held the chunk under 6144 (ring_cap_for);
  from 6144 on the ring keeps its 0.1.39 size (the Coder 6144/384 -> 7936/199 was -12% at 32K);
- the unpinned arm's cap stays 96 (the byte budget made it ~49 on IQ3_S);
- the scan sets its own ring budget instead of set_ring_override, so a layer split's 96-slot override (#340) is no
  longer clobbered by the scan.
RTX 5070 vs STRATA_RING_BYTES=0: 4K -0.7..+1.1% (bit-identical); 32K IQ3_XXS +18.5% (cache 1500) / +9.4%, Coder
+3.1%, IQ3_S +0.2%.
2026-10-04 10:10:54 +02:00
Niko1221 bcd3603e3c #583 default on (owner): STRATA_RING_BYTES=0 opts out; DETAILS.md with the measured quality
Changes default output on long prompts of native packs against 0.1.39 (expected; gate notes in the S40 report).
2026-10-04 07:58:15 +02:00
Niko1221 2bb11b52a6 hip_compat: cudaFuncAttributePreferredSharedMemoryCarveout (#646's fused_gr carveout on HIP) 2026-10-04 04:53:45 +02:00
Gopi 8fef4f1ff2 serve (auto): the chunk and the ring are one budget, and the ring is the better buy
`--prefill auto` walked a fixed list of chunk sizes - 32768, 16384, 8192,
6144, ... - and took the first that fitted, with the ring a fixed 384 slots
taken off the top before the chunk was ever considered.  Both halves of
that were wrong on a rig like this one.

The list is coarse exactly where it matters.  `bytes_needed` is a sum of
(T x positive constant) terms plus a max of such sums, so it rises
monotonically with T, and so does every test the scan applies - which makes
the largest chunk that fits a bisection on the 256-token grid the prompt
path already works on.  Seven probes against the list's ten, at one
`bytes_needed` each.  It matters because this rig affords ~8,700 tokens and
was handed 8192; 8704 is not on the list.

The order was wrong because chunk and ring spend the same borrowed VRAM,
and a ring slot is worth far more than a chunk token.  Measured here (2x
RTX 3060 + 2x RTX 5060, CUDA3 lending at its 90% cap, 120K prompt): 8960
tokens with the 17-slot ring that leaves reads at 963 tok/s, 8192/130 at
1,008, 7168 with the ring full at its 199-slot byte budget at 1,000, 5632/199
at 915.  A ring slot is worth ~0.53 tok/s and a chunk token ~0.05, so the 69
slots between a full ring and 8192's 130 are worth more than the 512 chunk
tokens they cost - and once the ring IS full, a smaller chunk buys nothing.
So: the largest chunk that still leaves the ring full, and only a rig where
no chunk can afford one falls back to the old rule with a kRingMin floor.

The room is counted in BYTES, not slots.  A ring slot is max_blob, but a
cache slot holds its own layer's blob - 2.15 MiB against a 2.54 MiB max_blob
here.  In slots the ring looked 18% cheaper than it is, a chunk whose ring
fitted only after that discount was refused outright, and the scan stopped a
step short.  `bytes_from_slots` is the inverse of the existing
`slots_from_bytes`, and `cache_slots_for` factors the latter out for the
serve path's per-participant version.  Both inverses clamp at k <= 0: the
lend budget is `min(slots - 128, pct * slots / 100)` and goes negative on a
cache under 128 slots, where `slots - k` is off_[slots] - the end of the
prefix sum, so that cache lends 0 bytes rather than reading past its offsets.

Upstream's per-participant `fits`/`fits_one` and the #448 diagnostic are kept:
the ring room is taken over the smallest participant, `alone` is the chunk
CUDA0 alone would read in, and the small-card warning is unchanged.

Verified on the rig: the scan picks 7680/199 against the old rule's 8960/17,
1018.8 and 1021.1 tok/s on two cold 120K prompts against 963.1 - +5.9%.
Q8_0 picks 5632/101, its 506 MiB budget over a 5,222,400 B blob.

0.1.39b (Niko1221): behind STRATA_RING_BYTES=1 with the byte-budget ring; the default keeps 0.1.39's chunk list
(plan_lend and the serve scan), its log line gains the ring. The INFO fields (the Monitor commit) are not taken.
2026-10-04 04:07:03 +02:00
Gopi 04b8d346a1 prefill: the streamed ring is a byte budget, so a native pack sets its own slot count
The ring was 384 slots - a number measured on Q2_0, where a slot is one
expert blob of 1,382,400 B, so 506 MiB.  A slot is a whole blob, so on a
pack with bigger blobs the same 384 slots are a different amount of
memory: 1,912 MiB on Q8_0, more than half of an 8 GB card's expert cache,
and what kept that card at a 1024-token chunk when its own buffers would
have fitted 6144.  Measured on the 4-way rig, one env var and nothing
else: chunk 1024 -> 6144, prefill 87 -> 402 tok/s.

So the budget is the constant and the slot count is derived from the
pack's max blob: 384 on Q2_0 (pinned) or 96 (not), exactly as before, and
the pack it was tuned on does not move.  The fused rule (1024 slots, #136)
is preserved the same way, and `ring_cap()` still bounds the result.  This
is also what lets the auto chunk scan treat "the ring is full" as a plain
byte comparison.

Also exports the three accessors the scan needs: bytes_needed_no_ring
(the chunk's own buffers, without the ring), ring_max_slots (the cap in
slots) and ring_slots_for (what the ring resolves to for a chunk, after
the override, the env var and the pinned-share rule - what `init` lays out
and what the INFO line reports).

And one fix on the way: `bytes_needed` counted `carve`'s buffers from
memory and got two wrong - it took `xn` unconditionally, though `carve`
takes it only under STRATA_GR_UNFUSED, and it never took `grs`.  Net
T*(D-HC)*4 bytes too many: 42 MB at a 1024-token chunk.  The direction is
safe - the prompt path was told it had less room than it did - but it
under-sizes every loan.
2026-10-04 03:05:40 +02:00
野生の男 5a8b292417 cpu: preserve full thread affinity on Windows and Linux (#626)
Windows: pool workers pin with SetThreadGroupAffinity (the processor group was dropped: a 3990X's 63 workers
collapsed to 32 group-local masks); the host thread selects a CPU Set and restores the previous selection.
Linux: the complete, dynamically sized affinity mask is kept and restored (CPU IDs past 63).
pool_affinity_test (Windows) and pool_affinity_linux_test. Squashed from the PR's 8 commits (net diff).
2026-10-04 01:29:24 +02:00
Niko1221 cab62c933f #465 engine: batch slots for one GPU - warn instead of refuse, windows over the active slots, slot conversation cache, prompts read beside decoding slots, BYIELD
- --slots N is the same as --batch N; a count the engine cannot run (no --serve, 1, more than 8, a split on one
  GPU, sessions that do not fit, --batch-groups that do not divide) is a warning and fewer slots or none, never a
  refusal; INFO says batch_slots=N when slots run
- a batch window runs over the active slots only (Verifier::run_slot_rows, graphs per row list): an idle slot is
  not written, so it keeps the conversation it served
- a request whose prompt starts with what an idle slot holds (all of it, or its turn checkpoint) continues from
  the slot's sessions instead of reading the history again
- with slots, a prompt is read one prompt chunk at a time (the same chunks as one run) and the slots decode
  between two chunks for half as long as the chunk took (STRATA_BATCH_DECODE_SHARE)
- BYIELD <slot>: the prompt being read stops at its next chunk boundary, the part read is copied into the slot,
  YIELDED <slot> <tokens> + DONE cancel; the same request sent again goes on from there with the same chunks
  and checkpoints (#656's cooperative preemption, a slot as the park)

Default (no --batch) unchanged.
2026-10-04 01:18:02 +02:00
Niko1221 b8dbdfe12a older CPUs (experimental): STRATA_ISA_FLOOR=avx|none, and setup compiles it instead of stopping (#394 #595 #623)
- CMake: STRATA_ISA_FLOOR (empty by default) builds ggml-cpu for AVX (Sandy/Ivy Bridge) or SSE4.2
  (Nehalem/Westmere) instead of the build host or AVX2; every other ggml ISA option off. Strata's own
  AVX2/AVX-512 kernels stay in the binary behind their CPU checks. Excludes STRATA_PORTABLE. The ggml
  block follows the Strata_Dirigo fork's (rwkeyes).
- STRATA_FORCE_ISA=avx2|avx|sse caps the engine's CPU probes, so the older-CPU paths run on any PC
  (how this was tested here: no old CPU). cpu_sse42_ok(), isa_floor_build().
- The engine: a normal build still refuses a CPU without AVX2, now pointing at the experimental build;
  a floor build checks its own floor (AVX, or SSE4.2 + POPCNT) and warns that it is experimental and
  slow (or, on an AVX2 CPU, that the normal build is faster).
- setup: a CPU without AVX2 warns ("experimental, slow") and compiles the engine on the PC with the
  floor it has, instead of failing (#595's idea); the prebuilt AVX2 engine is not used there. Recorded
  in BUILD.json; a build folder for another floor is configured afresh. STRATA_ISA_FLOOR=avx|none asks
  for it on any PC. Windows + AMD has no source build: a clear stop. The MCP server's recommendation
  notes it instead of refusing.
- tools/test_setup_oldcpu.py.

Default build: nothing changes (no new CMake definitions, the probes answer as before without the env).
2026-10-03 23:35:52 +02:00
Anon 4175cbcb5b prefill: a layer split's idle card streams a share of a one-chunk prompt's experts
A prompt that fits one chunk runs a layer split's stages one after the other,
so while one card reads its layers the other idles. --peer-device already has
the mechanism for this (PeerPrefill's peer streaming: a share of the experts
the primary streams goes over the peer's own PCIe link and is computed there),
but it needs P2P and is refused with --layer-split.

This makes PeerPrefill usable as a stream-only peer between the stages:

- set_stage_helper: each stage's prompt path gets the next stage's (the last
  one the first's); a stream-only PeerPrefill holds no experts (peer == null),
  so every use of the peer tier is guarded
- per prompt, bind_stage_helper points it at the idle stage's own lent prompt
  buffers (mixed, Xq, GU, H, Hq, Dm, ring, MMQ context): no VRAM of its own
- without P2P the activations, the row table, the group bounds and the result
  rows go through mapped host memory, read by copy kernels on the device that
  needs them. Copy-engine uploads queued behind the ring's expert blobs (the
  grouping tables' reason for mapped memory), which cost ~20 ms per layer and
  ate the whole gain; cudaMemcpyPeerAsync without P2P is staged by the driver
- only for one-chunk prompts on the MMQ path (not the fused kernels, not with
  --peer-device), with a share that falls with the chunk: min(0.45,
  0.5 - T/16384), off below 0.3 (~3.3K tokens) - measured, below
- STRATA_PREFILL_HELP=0 turns it off, STRATA_PREFILL_HELP_FRAC fixes the share

2x RTX 3090 (no P2P), UD-Q4_K_XL, layer split auto, 262K context,
--adapt-swaps 0, prompt tok/s without / with: 1.1K 496 / 723, 1.5K 674 / 833,
2K 878 / 1128, 2.5K 1017 / 1296, 3K 1260 / 1440, 4K 1605 / 1607, 8K 2215 /
2221; decode unchanged. The output is repeatable (same prompt, same answer)
but not bit-identical to STRATA_PREFILL_HELP=0's: the helped rows are
computed in another MMQ grouping.

0.1.39b (Niko1221): opt-in (STRATA_PREFILL_HELP=1), since the helped rows round differently from the default;
merged with #598 (run_impl / next_run_).
2026-10-03 23:34:07 +02:00
David ff12cbe2b9 hip: retain native wave synchronization on HIP 7 2026-10-03 23:33:46 +02:00
David e3e30eb131 hip: add experimental gfx1012 support and legacy BLAS compatibility 2026-10-03 23:33:45 +02:00
Niko1221 9d638ffed6 gfx906 build (PR #638 follow-up): mrope.hpp's HIP include only in the gfx906 build; tests.list dropped
The RDNA backend (STRATA_ENABLE_HIP) compiles mrope.hpp as before; tests.list was a local test runner's list, not
part of the build.
2026-10-03 23:33:16 +02:00
Danny Werner c3f2a5e63b refill: improve multi-GPU processing 2026-10-03 23:33:12 +02:00
zhedang 5cc53cddbc core: optimize remote expert helper decode
Add default-off cache coordination, helper-side weighted reduction and CPU activation quantization on demand. Allow helper cache capacities to be sized automatically at startup.

Port the saved v2.2 implementation onto upstream 0.1.38 without changing the separate peer tier. Document the helper-only scope and current validation limits.
2026-10-03 23:33:07 +02:00
JeanP00l b18278ce7e gfx906: route the verify window's tokens in one BF16 projection + one top-k (bitwise the per-token calls)
The Coder's router is 256 experts, which the native 512 x 10 router does not take, so a verify window routed each
token with its own projection and top-k. moe_route_window does the window in one multi-column BF16 projection
(bf16_gemv_fp32_mmvf_cols over the existing multi-row kernel) and one n-token top-k; each token's route is bitwise
what moe_route gives it (route_window_parity, new ctest). gfx906 build only, so the CUDA and RDNA paths are
unchanged; STRATA_ROUTE_PER_TOKEN=1 restores the per-token calls.

2x MI50, Coder IQ1_M, 32K, greedy 1,500-token request: verify window 55.5 -> 52.5 ms, generated text identical.
2026-10-03 23:33:00 +02:00
JeanP00l 9ff290b493 hip: AMD gfx906 (Instinct MI50 / MI60, Radeon VII; wave64) as an opt-in build, STRATA_HIP_GFX906=ON
The HIP backend (STRATA_ENABLE_HIP) is wave32 RDNA only, and the engine refuses a wave64 card. This adds a
separate opt-in build for gfx906: the CUDA sources compiled as HIP through a small compat layer, with a CUDA warp
as a logical half of the 64-lane wavefront. With the option off nothing changes for CUDA or the RDNA backend;
every new code path is under STRATA_HIP_GFX906.

Build: -DSTRATA_HIP_GFX906=ON -DCMAKE_HIP_ARCHITECTURES=gfx906 (implies STRATA_ENABLE_CUDA's targets, refuses
STRATA_ENABLE_HIP). The CUDA runtime/cuBLAS names come from include/strata/platform/hip_compat/ (first on the
include path), CUDA::cudart / CUDA::cublas become aliases of hip::host / roc::hipblas, every .cu (and the ggml MMQ
sources, with GGML_USE_HIP) is compiled as HIP. The native kernels' --use_fast_math (an nvcc flag) is not passed.

Correctness fixes found on the way (each one changed results or hung, not only speed):
- __fmul_rn / __fadd_rn / __fsub_rn contracted into FMA under HIP; they no longer do (as on NVIDIA) - the
  elementwise and quantize_act parity tests pass bitwise;
- the device check of the gfx906 build reads the card's gcnArchName (the wave32 check is the RDNA backend's);
- ggml MMQ got NVIDIA compute capability 9.0 for gfx906 and hung the prompt path; it gets the AMD arch code;
- __byte_perm is a scratch-memory array in HIP: the IQ4 table lookup uses llama.cpp's v_perm_b32 sequence
  (agent-request decode 29.9 -> 39.2 tok/s on 2x MI50);
- the stage profiler's wall clock runs at 25 MHz on gfx906, not 100 MHz;
- a CUDA toolkit-mismatch check that reads CUDART_VERSION is skipped (no CUDART_VERSION under HIP);
- gdn_rec_parity (cp.async, CUDA occupancy API) is not built for gfx906.

wave64 kernels (gfx906 only; the CUDA layout stays one env switch away for A/B, every one bitwise-checked):
- grouped native experts, STRATA_EXP_MODE=7 (default): gfx906 has no packed byte SIMD, so __vcmpne4/__vsub4
  became ~12 word ops each; signs are now dp4a(g ^ m, u) - dp4a(m, u) (exact in integers), grids and the group's
  activations sit in LDS, one weight load serves every token of the group. Same k-to-lane order, so the output is
  bitwise the old layout's (native_expert_bench, real GGUF rows). Verify window 60.0 -> 51.3 ms.
  STRATA_EXP_MODE=2 is the previous layout;
- MMVQ: one wavefront per row with a 64-lane butterfly, no LDS/barrier, same kernel for 1..8 columns
  (+2.3% decode; STRATA_MMVQ_WAVE=0: CUDA layout);
- router top-10 in one wavefront (register argmax, no block barriers), bit-identical (router_top10_parity);
- gr_down_multi split along K (8 rows x 5 slices of 2048 per block, fixed-order finish) (+2.9%;
  STRATA_GR_SPLIT=0: old);
- hyper-connection read: norm with its loads in flight and xn in registers, up with 8 lanes per row
  (51.3 -> 50.2 ms per window; STRATA_GR_FAST=0: old);
- the QSA prompt-attention tensor-core/WMMA kernels are not used on gfx906 (the FP32 kernel runs).

Benches (not ctest; they need a model shard): native_expert_bench (one expert layout against another, bitwise,
with timings) and fused_gr_bench (the fast hyper-connection kernels against the old ones).
2026-10-03 23:33:00 +02:00
rwkeyes 51a60af92a router lookahead: an AVX1 dot and a plain fallback for CPUs without AVX2 (Strata_Dirigo)
The routing-aware prefetch's router dot (bf16_rows_dot_multi) lives in kq_avx2.cpp, so an AVX-only
CPU started fine and died on the first request (SIGILL in vpmovzxwd on a Xeon E5-2687W). It now
takes the AVX2 kernel where the CPU has AVX2, kq_avx1.cpp's AVX dot (mul + add, 128-bit integer
widening) where it has AVX, and the same sums in plain C++ below that. cpu_avx1_ok() probes AVX +
OSXSAVE + the YMM state. An estimate only (which experts to prefetch). A CPU with AVX2 runs exactly
what it ran before.

From rwkeyes' Strata_Dirigo fork (MIT; 0d00a66, e0754d3), with MSVC's /arch:AVX added and
__builtin_memcpy replaced by std::memcpy for MSVC.
2026-10-03 23:31:16 +02:00
Niko1221 89351a799d Merge PR #559 (blange48, crazyaimachine): batch slots, pipelined layer split, parking with a split
Conflicts with 0.1.39: --trim-stage-weights is now the same switch as STRATA_STAGE_TRIM=1 (PR #639's
implementation, already in 0.1.39); the #533 VRAM command is taken between batch windows only when no slot
is decoding.
2026-10-03 23:29:40 +02:00
4EverBuilder 3f32e4b66a native experts without AVX2: Q2_0 layers on ggml-cpu, every AVX2 kernel gated (#595)
A native pack's Q2_0 layers (most IQ packs' down projections) ran on Strata's own Q2_0 kernels, which
are AVX2 at least. q2_native_kernels() keeps them there when the CPU has AVX2 and otherwise hands the
layer to ggml-cpu's Q2_0 vec_dot like every other native type (the pool, the dispatcher's activation
quantizer and the parity test). STRATA_KQ256's gate/up kernel needs AVX2 too. native_expert_parity
skips the AVX2 checks on a CPU without AVX2. No change on a CPU with AVX2.

(The engine part of #595; its setup and run-script parts are not in this commit.)
2026-10-03 23:28:19 +02:00
456950beec prefill gemm below sm_80: Volta on the FP16 path by default, Turing opt-in, Pascal through fp32 (#655 #540 #395)
PR #655's FP16 tensor-core path for Gemm::bf16, adapted so the ready-made engine stays byte-identical:
- default on for compute capability 7.0-7.4 only (Volta; the experimental STRATA_EXPERIMENTAL_SM60 build), off for
  7.5 (RTX 20): STRATA_BF16_TC=1 opts in, =0 turns it off on any 7.x card
- from #540: finite values beyond FP16's range clamp to +-65504; a beta = 1 product (the BF16X2 remainder, in
  FP16's subnormal band) keeps cuBLAS; the compute capability is cached per device (a layer split can mix cards)
- from #395: Pascal (6.x; cuBLAS has no BF16 GEMM there) widens both operands to fp32 by an exact shift and runs
  cublasSgemm with the same fp32 accumulator, tiled over T in the instance's own buffers
sm_80 and newer, and HIP, run the same cuBLAS / hipBLAS call as before.

Co-Authored-By: Károly Sárdi <[email protected]>
Co-Authored-By: Rafal R@V Prasal <[email protected]>
2026-10-03 23:28:09 +02:00
Stuart Chapin 200c35a8e9 fix(cuda,decode): resolve sm_120 IQ3_S generic-pointer fault and restore 100% bit-exactness with main 2026-10-03 23:27:36 +02:00
Stuart Chapin a36be1db29 perf(cuda,decode): zero-doorbell verify graph, sub-warp expert packing, shared-mem staging, and batched PLE/MTP 2026-10-03 23:27:36 +02:00
Rafa b34cf81417 BF16 tensor-core path: default on for all of compute capability 7.x (Volta too)
Volta has FP16 tensor cores and no BF16 ones as well, so cuBLAS takes the same SIMT path for BF16 products there.
Suggested in review. Not measured on sm_70; STRATA_BF16_TC=0 turns it off.
2026-10-03 23:27:03 +02:00
Rafa 4dbc4c8b82 sm_75: run the prompt path's BF16 products on the FP16 tensor cores
Turing has no BF16 tensor cores, so cublasGemmEx on CUDA_R_16BF inputs falls back to a SIMT fp32 kernel
(magma_sgemmEx). On an RTX 2080 Ti that was the largest kernel of the prompt path, 20 % of a 4K prompt:
the hyper-connection down / up projections, the router and indexer rows, PLE key / value.

Gemm::bf16 now converts the weight and the activations (in row slices, into the instance's own scratch)
BF16 -> FP16, which is exact for every value inside FP16's range, and runs the existing FP16 GEMM (fp32
accumulate) on the tensor cores. Single-row products keep cuBLAS's GEMV. STRATA_BF16_TC=0/1 overrides;
the default is on for sm_75 only, so other cards run exactly what they ran.

New test gemm_bf16_parity: Gemm::bf16 against cuBLAS's BF16 product over the prompt path's shapes,
within fp32 rounding (worst relative difference 1.7e-5 on sm_75; 0 with the path off).

Qwen3.8-Flash-Next Q2_0, RTX 2080 Ti, i7-6700: prefill 877 / 1134 / 1153 / 1116 / 1009 t/s at
4K / 16K / 32K / 64K / 128K -> 1020 / 1337 / 1366 / 1312 / 1166.
2026-10-03 23:27:03 +02:00
Chanyeong Lim 586367cb00 qsa_select: the top-k past the register kernel's reach on 1,024 threads with a histogram per warp (CUDA) - 9-12x at 262K-524K cells, a 243K-token prompt +26% on an RTX 3060
On CUDA the register kernel holds 33,792 blocks (~135K cells). Past it qsa_block_topk took the original kernel:
256 threads, each reading its own stretch of the scores on all six passes, one shared histogram updated with
atomics. The dispatch follows the capacity (--max-context), so with a context past ~135K that was every prompt
batch and every decode window on a card without the sm_75 active-bound dispatch (#512), whatever the prompt's
length, and on sm_75 every prompt batch past 33,792 blocks and every decode window.

block_topk_wide_kernel is the register kernel's layout (1,024 threads, a histogram per warp, warp scans) with the
keys read from memory:
- the four radix passes: thread t reads blocks t, t + 1024, ... - a histogram has no order, so a warp reads 32
  neighbours at a time;
- the count and the output: warp w holds `per` rows of 32 consecutive blocks, and the cells are written in the
  order (warp, row, lane); a row without a selected block is skipped.
The selection rule is the original kernel's (radix threshold, ties to the lowest index, cells ascending): identical
ids. One launch per call, no scratch memory, no capacity limit.

Dispatch (CUDA; HIP is unchanged and does not compile the kernel): where the blocks do not fit the registers the
new kernel, except a call that names fewer than 4,608 blocks (a prompt batch below ~18K cells), which keeps the
original kernel: its fixed cost is lower there. A decode window (no block count) takes the new kernel at any
context. The register kernel's range, the sm_75 active-bound dispatch and the sm_90+ cluster path are unchanged.
STRATA_TOPK_OLD=1 keeps the original kernel, as before.

qsa_topk_parity (new, with the kernel parity tests): the dispatched top-k against qsa_block_topk_ref on both sides
of the register kernel's reach, at short contexts under a long capacity and up to 524,288 cells, called as decode
and as the prompt path call it, with continuous scores, 64 score levels and equal scores.

RTX 3060 12 GB (sm_86), 256 queries with their block count under a 262,144-cell capacity (524,288 for the last
two), original -> dispatched:
  16K cells    0.199 -> 0.199 ms (the original kernel)
  32K          0.845 -> 0.328
  131K         4.70  -> 0.97
  262K        16.09  -> 1.71
  393K        26.02  -> 2.49
  524K        37.53  -> 3.24
4 queries without a count (a decode window): 0.031 -> 0.028 ms at 8K, 0.176 -> 0.083 at 131K, 0.309 -> 0.127 at
262K.

IQ3_XXS on that card (--resident-experts, --max-context 262144, --kv int8 --kv-resident 32768),
tools/needle_bench.py, STRATA_TOPK_OLD=1 against the default: tok/s of the prompt, and the prompt path's selection
phase (STRATA_PREFILL_TIMING):
  16,471 tokens    1004.5, 1016.2 ->  989.0, 1013.6       195 ->    195 ms
  31,970           1043.3, 1055.5 -> 1060.1, 1060.5       900 ->    731
  61,333            932.9         -> 1013.8             4,061 ->  2,428
  122,755           904.7,  904.2 ->  992.7,  989.6    20,019 ->  9,034
  185,729           819.7         ->  954.7            50,055 -> 20,761
  243,176           743.3         ->  933.7            96,326 -> 35,544
The needles are found in every run.
2026-10-03 23:25:56 +02:00
Niko1221 8ac4b945a0 Merge branch 'f-139-big' into f-139-rel
# Conflicts:
#	serve/server.py
#	setup.py
#	src/program/generate.cpp
2026-10-03 20:05:05 +02:00
Niko1221 3ed9dafa1e Merge branch 'f-139-prs' into f-139-rel
# Conflicts:
#	setup.py
#	src/core/expert_source.cpp
2026-10-03 20:04:42 +02:00
Niko1221 c3c034745b prefill: step diagnosis and a HIP host-DMA switch for the gfx1201 prompt stalls (#579 #613 #541)
- The per-layer wait for the router ids names itself in a stall report ("waiting for
  the GPU (attention, router) at layer"): a GPU stall reads apart from a host one.
- STRATA_PF_STEP_SYNC=1 (opt-in): the compute and copy streams are waited for after the
  K/V staging, the K/V append and each layer; a step over 250 ms is logged with its
  layer and chunk, and a stall names the step.
- STRATA_KV_HOST_DMA=1 (HIP only, opt-in): with KV streaming the prompt path's append
  writes the staging pool and the resident slots only, and one DMA copies the chunk's
  blocks to the host copy - no kernel writes host memory over PCIe. Same bytes for
  every reader (the staged first block is complete; past the chunk nothing is read
  before a later append writes it).

Q2_0 on the 5070: the same tokens with STRATA_PF_STEP_SYNC=1 as without (and as 0.1.38).
2026-10-03 19:12:21 +02:00
Niko1221 27db7cae1d verify: opt-in handshake trace for the gfx1030 timeouts, HIP A/B switches (#649)
STRATA_VERIFY_TRACE=1 records each verify window's handshake on the host (window
start, launch, each layer's ring and service, the final sync, a timeout, the release
and how long the GPU took to drain after it) with the seq / flag / A / B words, and
GPU breadcrumbs: the profiler's stage stamps per layer and token group, written to
mapped memory so they read while the GPU hangs. A timeout prints them before and
after the release; the serve watchdog's stall report (Verifier::diag) prints them too.

HIP only, opt-in, for the reporter's A/B: STRATA_VERIFY_COHERENT=1 allocates the
handshake words and rows with hipHostMallocCoherent; STRATA_DOORBELL_STORE=1 has the
GPU store each step's ring (known at capture) instead of read-modify-writing it over
PCIe. CUDA builds and the default path are unchanged (Q2_0 on the 5070: the same 96
tokens with and without the trace as 0.1.38; a forced stall dumps the breadcrumbs up
to the withheld flag).
2026-10-03 19:12:20 +02:00
Niko1221 89bad9a4ec Hot VRAM resize, opt-in: a segmented expert cache, VRAM <reserve>, POST /v1/vram (#533)
--vram-elastic (the config's "vram_elastic": true) allocates the expert cache as one
reserved address range backed by physical segments (CUDA virtual memory management,
512 MiB each, --vram-segment-mib), so every slot keeps its address. Between
requests, the engine command `VRAM <reserve_mib>` gives the last segments back to
the driver until that much VRAM is free (their experts become CPU misses, the prompt
path's loan moves down and its chunk shrinks to fit), or maps them again and puts
the same experts back when more is free. `VRAM` alone: the start's reserve. The
server's POST /v1/vram {"reserve_mib": N | null} sends it after a running request,
keeps it for an engine that is unloaded, and /v1/status shows the cache's size.
Never automatic. One NVIDIA GPU only (no layer split, helper caches, --peer-device
or the resident low-RAM mode); HIP refuses the flag.

RTX 5070, Q2_0 (4.8 GiB cache): reserve 6000 -> 4.3 GiB freed in 78 ms, decode
44 -> 33 tok/s; grown back in 92 ms, the answers token for token the ones before.
Without the flag the cache is the one cudaMalloc it was (same answers with and
without the flag until a VRAM command).
2026-10-03 18:52:00 +02:00
Niko1221 0b1c763a75 Docs: the five reading errors #604 found (the sixth, the n-gram table's unbuffered reads, is in #605's commit)
- MODELS.md: the RAM rule is the experts + ~10 GB (setup's arena_gb), not shard 1 + 10 GB; the sizes per model.
- DETAILS.md: prompts are read in chunks of up to 8,192 tokens (32,768 opt-in), not 2,048; Q2_0's AVX-512 pack is
  ~40 GB on disk = 34 GB of experts plus the dense weights.
- README: the NVIDIA speed table says which engine measured which row (Q2_0 0.1.36, the others 0.1.26).
- expert_cache.hpp: the comment that the cache "does not yet compute anything" is gone.
2026-10-03 18:32:16 +02:00
Niko1221 b591a45bd7 Hit rate: say the PCIe share beside it (#588)
The decode hit rate is the VRAM share of the experts the pool looked up; the routed experts the GPU reads over PCIe
(--pcie-frac) or another GPU computes are in neither count, so raising --pcie-frac raised the "hit rate" while the
decode got slower. Its meaning stays; now that share is said beside it:

- engine: DriveState::offload_entries counts those entries; the serve line adds "; N more read by the GPU over
  PCIe or from another GPU (X% of all M routed)", and the DONE line a 16th field (an older server ignores it).
- server: /metrics' requests get "pcie_share" (None from an older engine), the console's done line "(+X% of the
  routed experts over PCIe)", and the Monitor's column is "VRAM hit rate" with "+X% PCIe" after it.
- DETAILS.md says what each means.

No number changes; tokens are unchanged.
2026-10-03 18:27:35 +02:00
Niko1221 16a6d77ed4 The expert arena checks the host RAM before it is allocated; the RAM probe reads cgroup v1 too (#633)
The pinned expert arena (the default tier) was allocated without looking at the RAM: on Linux the anonymous mapping
succeeds whatever the host has, and a container whose memory limit is below the arena was killed by the OOM killer
part-way through the load, without a message.

- detail::host_available_memory: the file tier's probe (MemAvailable lowered to the tightest cgroup v2 limit's room),
  now with its files as parameters (tested on fake trees), cgroup v1's memory controller (memory.limit_in_bytes -
  memory.usage_in_bytes), and no cgroup line at all meaning MemAvailable alone (it was "cannot determine", which
  refused a --resident-budget-gib on a v1 host). It also returns the tightest hard limit itself.
- ArenaExpertSource::open, after the size check and before the allocation: a hard cgroup limit below the arena is
  refused with both numbers (a certain OOM kill); less RAM available than the arena is a warning with the numbers
  and what runs with less (--mmap-experts with --resident-budget-gib), and the load goes on. Not for a shared arena.
- file_expert_source_test: v2 limit / max / malformed / missing, v1 limit / unlimited / not mounted, no cgroup.
2026-10-03 18:24:31 +02:00
Niko1221 2b726ba683 q8_1 activations: keep the block's fp16 scale and sum finite (#606)
Both q8_1 activation quantizers (iq_kernels.cu q8_1_store, native_mmvq.cu native_quantize_q8_1_kernel) stored the
sum of a block's 32 raw activations as fp16: past 65504 (one massive-activation dimension is enough) it rounded to
inf, and d = amax / 127 did past amax = 8.3M. The dot products then read inf * 0 = NaN, and a NaN in the residual
answers one token forever (ggml-org/llama.cpp#23606 is the same defect).

Now both are clamped to the largest finite half (and the values to the int8 range they then exceed). A block that
was finite before is stored bit for bit as before - values in (65504, 65520) round to 65504 anyway, and a finite
block's |x / d| is at most 127 - so the default output only changes where a block overflowed (and was NaN). A NaN
input stays NaN, so STRATA_DBG_NAN still finds it.

iq_multi_parity checks both quantizers against the 0.1.38 formula on a host transcription: every finite block as
before, and three overflowing blocks (sum 96,000, sum -69,984, amax 2e7) stored finite.
2026-10-03 18:18:49 +02:00
JeanP00l 6149efd3a4 STRATA_ARENA_MMAP=1: the expert arena as a read-only mapped file (Linux), for small-RAM machines
The host expert arena is normally read into pinned (locked) RAM. On a machine whose GPUs already hold most
experts, that locks memory the CPU never reads: with the Coder (25 GB arena) on 2x 16 GB GPUs and 32 GB of RAM,
~1 GB stayed available at 128K context, the kernel swapped, and a multi-turn checkpoint took 25-80 s.

With STRATA_ARENA_MMAP=1 (opt-in, Linux; off by default, nothing changes without it):

- the first start writes the arena to the pack's experts.bin (padded by one blob so a whole-slot copy may start at
  any expert); later starts map it read-only, not locked, not pinned. The page cache keeps what the CPU pool and
  the prompt path read and gives the rest back under pressure; a dropped page is re-read from the file, so no
  result depends on what is resident. The GPUs get no mapped alias of it: run with --pcie-frac 0;
- after the VRAM caches are filled, the pages of experts a GPU holds are handed back (MADV_DONTNEED,
  STRATA_ARENA_RELEASE=0 keeps them) and the rest are prefetched (MADV_WILLNEED); an adaptive swap releases the
  expert that went to VRAM and prefetches the one that came back;
- HIP: ROCclr copies a pageable source of 1 MiB or more by locking its pages in place and keeps them locked, which
  undid every release. The engine raises GPU_PINNED_MIN_XFER_SIZE (an explicit setting wins) so such copies
  stage through ROCclr's own buffers, and locks an adaptive swap's source pages itself only until the copy lands
  (the page ranges are merged first: a copy from a range only part of which is registered fails);
- STRATA_RSS_TRACE=1 prints the process's RssFile at the startup steps (madvise reports success on locked pages
  and frees nothing; RssFile is the honest check).

The existing --mmap-experts refuses native packs (the Coder); this path is for them.

Measured on 2x AMD Instinct MI50 16 GB + 32 GB DDR4 (gfx906 port, Coder IQ1_M, 128K context, layer split):
RAM available while serving ~1 GB -> 25 GB; multi-turn re-read of a checkpoint 25-80 s -> <= 3.2 s; decode
unchanged (49.06 vs 49.16 ms per verify window). Not measured on NVIDIA (pin_blobs is a no-op there).
2026-10-03 18:14:12 +02:00
JeanP00l d95805c61a layer split: with explicit split points each GPU loads only its own layers' dense weights
Without this every stage of a layer split keeps a full copy of the dense weights (~3.4 GB for the Coder),
although it only ever reads its own layers. That VRAM goes to the expert cache instead.

- the canonical arena of each GPU skips the `blk.N.*` tensors of layers outside its range (PLE tensors stay
  everywhere), read from the pack's index.txt;
- NativeDense::set_layer_range limits the native projections the next `load` uploads the same way;
- only with explicit split points (`--layer-split 27`); `auto` and a single GPU are unchanged;
  STRATA_STAGE_TRIM=0 keeps the full copies;
- one log line per GPU says what it loads, and a later stage logs its free VRAM before the cache is sized.

Measured on 2x AMD Instinct MI50 16 GB (gfx906 port, Coder IQ1_M, 128K context, --layer-split 27):
experts held in VRAM 8,819 -> 10,626 of 12,288, VRAM hit rate 95% -> 98%, decode on a 17K-token agent prompt
39.2 -> 41.7 tok/s. The trimmed tensors are never read by that GPU.
2026-10-03 18:13:54 +02:00
Niko1221 d161e25ff6 File tier: decide unbuffered reads from the expert bytes it reads, again once the RAM copy is built (#577)
0.1.38 made the choice before the resident set existed, from the budget asked for, against every shard's bytes
(dense weights and the PLE table included). On a 96 GB PC with UD-Q4_K_XL (budget 72 GiB, 48.94 GiB really
resident) that read 83.0 - 72 - 4 = 7 GiB of room against 103.7 GiB of files, so every refill after a prompt read
the drive: prompts 15-40% slower than 0.1.34.

- platform::file_cache_keeps(avail, arena, read): the decision, unit-tested in platform_memory_test with the
  reporter's numbers and the 32/64 GB PCs of #357/#362.
- set_unbuffered: the arena is at most every expert's bytes, and the cache has to keep only the experts outside it.
- recheck_unbuffered, after pin_cache_complement: the same decision from the RAM the copy really holds and the
  expert bytes outside it; switches either way (startup, nothing reading) and logs the numbers.
- The 32 and 64 GB PCs still read unbuffered (the profile fill and the copy too, as before).

Same tokens either way: only how the same bytes are read changes.
2026-10-03 18:12:53 +02:00
benoit lange af7918f02d Parking with a layer split: the draft ring once, every stage's K/V reused, checkpoints moved not copied
- conversation_snapshot_*(..., const QsaState* draft): nullptr saves, validates and restores an image without the
  draft layer's K/V. The later stages park that way; the draft ring is saved once, with the first stage's image.
- ConversationKvReuse::stages / ConversationCache::retain(.., stage_kv): every stage's restored K/V is retained, so
  its next park copies only what changed, as the first stage's already did.
- conversation_checkpoints_split / _merge: the checkpoints are moved into their stage parts for the capture and
  back afterwards (and on restore), instead of copied.
- Tests: conversation_cache_test (split/merge round trip without copies, refused misalignments, per-stage reuse),
  conversation_snapshot_test (draft-less images: estimate, validation both ways, A/B/A exactness, incremental reuse).
2026-10-03 17:03:02 +02:00
crazyaimachineandbenoit lange 06125d2e20 Conversation parking with --layer-split
park_current snapshotted stage 0 only, so a split rejected parking. A parked conversation now holds one image per
later stage (SavedConversation::stage_images, captured from each stage's session on its own GPU); a split
checkpoint is cut into its stage-0 part and its stage parts while parking and put back together on restore; every
stage image is validated before anything is overwritten; the guard is removed.

Patch posted on #559 by crazyaimachine (written by their coding agent), tested on 2 GPUs there and on a 4-GPU
split here.

Co-authored-by: benoit lange <[email protected]>
2026-10-03 16:11:44 +02:00
benoit lange 9461f5e3a3 Several conversations at once: batch slots, pipelined layer split, per-stage dense weights
The engine's --serve can now keep up to N conversations open (--batch N) and decode them together, one token
each per verify window; with a layer split, --batch-groups G pipelines the slot groups through the GPUs, and
--trim-stage-weights makes every GPU load only its own layers' dense weights (VRAM back to the expert cache).
The server runs concurrent requests in the slots. See docs/BATCHING.md.
2026-10-03 08:04:53 +02:00
Niko1221 74d81c22c1 hip_compat: the peer-access calls (#531 follow-up)
peer_experts.cpp calls cudaDeviceCanAccessPeer, cudaDeviceEnablePeerAccess and cudaMemcpyPeerAsync and tests
cudaErrorPeerAccessAlreadyEnabled; the HIP shim had none of them, so the Windows HIP build stopped there. Mapped to
their hip* names.
2026-10-02 22:44:26 +02:00