Compare commits

...
524 Commits
Author SHA1 Message Date
Niko1221 1cbcacbcae fix(build): dma_batch.hpp copy_expert_blobs uses plain arrays (nvcc on Linux rejects its std::array locals)
dma_batch_parity.cu failed to compile on Linux/WSL (nvcc 13.0 with g++): 'template argument 2 is invalid' at the
std::array<T, cap> locals of copy_expert_blobs, whatever the form of the size (int, size_t, a literal). The engine
objects and the Windows build were not affected; the Linux tests build was.
2026-10-06 03:12:25 +02:00
Niko1221 12d835dd61 test(vmm): drain the device before unmapping chunks, and poll the free-VRAM readout after the release
vmm_test failed on the RTX 5070 / driver 610.47: the pageable host-to-device copy was still in flight when
a.unmap() pulled the chunks out from under it, an illegal memory access that stuck to every later call (the last
two checks, the 8-chunk free-VRAM gain and cudaGetLastError, failed as a result). It was not a VRAM leak:
released memory comes back at once (18 MiB gained for 9 chunks of 2 MiB, polled 10 s). generate.cpp already
synchronizes before it unmaps.
2026-10-06 02:50:22 +02:00
Niko1221 89ae08e67f fix(hip): package_windows copies rocBLAS arch subfolders (the gfx1151 wheel's library/gfx1151/) instead of crashing 2026-10-06 02:03:21 +02:00
Niko1221 c4a329fefe setup: recognize AMD Strix Halo (gfx1151) exactly, size its unified memory, recommend UD-IQ4_XS
gfx1151 is now a supported AMD arch (TheRock gfx1151 index; HIP build on Linux, docs/STRIX_HALO.md). The check is exact (gfx_arch_is, as the
engine's gfx_arch.hpp): gfx1150 / gfx1152 / gfx1103 are named as Strix Point / Krackan / Phoenix-Hawk Point and stay unsupported. PCI 1002:1586
(one id for the 8060S / 8050S / 8040S) and the Windows adapter names map to gfx1151; the KFD simd_count names the 8060S / 8050S / 8040S.
Unified memory: the BIOS carve-out + the shared pool (GTT; Windows half the RAM) size the context; only the carve-out counts as extra
room in the low-RAM mode. A discrete card is picked before an APU. A Windows zip without gfx1151 code is refused with the reason.
UD-IQ4_XS is the recommended model from ~80 GB of memory; setup changes no host setting. tools/test_setup_strix_halo.py uses Aurora's real sysfs.
2026-10-06 01:02:10 +02:00
Niko1221 e8e11a915f Windows HIP zip: build for gfx1151 (Strix Halo) too
The default STRATA_HIP_ARCHS of tools/hip/build_windows.bat gains gfx1151. The ROCm 10.2.0a20260930 Windows SDK compiles the strata target for
gfx1151 (stream 140-j built it on this PC) and its rocm-sdk-device-gfx1151 wheel carries the rocBLAS kpack and the hipBLASLt kernels
package_windows.py copies, as for gfx1201. Unvalidated on a Windows Strix Halo.
2026-10-06 01:02:10 +02:00
Niko1221 023dd01531 fix(mtp): a NUL byte had replaced the backslash-zero of the STRATA_NO_MULTI_GR flag (#783 PR-g) 2026-10-06 00:51:56 +02:00
Niko1221 660e495e2d perf(quantize): quantize_act.cu is already one warp a block in f-140; keep f-140's file (#783 PR-k) 2026-10-06 00:51:56 +02:00
Niko1221 46d0136607 perf(gr): STRATA_GR_DOWN_MAX4 stays opt-in (reverts the sm_120 default of 6d60dfc1)
Bitwise on the 5070 (identity gate: Q2_0 and IQ3_XXS identical 10/10 with it on against off, same exe), but the 10-pair
interleaved decode A/B (Q2_0, default config) shows no win: B/A medians 0.97 / 0.97 / 0.99 on the three greedy requests.
2026-10-06 00:51:56 +02:00
Niko1221andstuchapin909 b1efb70c01 test(rope): rope_parity check 6 is skipped where the fused norm+rope kernel is not enabled (#783 PR-f)
On the HIP build the fused kernel is off until check 6 passes there, so check 6 reports SKIPPED unless STRATA_NORM_ROPE=1 asks for it. (gfx1151 on Aurora, 0.1.40: with STRATA_NORM_ROPE=1 it does not pass - about 7% of the rotated elements differ - which is why the kernel stays off on HIP.)

Ported from #783 (stuchapin909, head 4327807), split per piece.

Co-authored-by: stuchapin909 <[email protected]>
2026-10-06 00:51:56 +02:00
Niko1221andstuchapin909 0c86bbecb6 fix(gdn): gdn_step_norm_multi takes its norm shuffles outside the rg == 0 branch (#783 PR-b)
On gfx1151 the shuffle reduction of the sum of squares inside the divergent-looking branch rounded differently for ~3% of the outputs than the single-token kernel (gdn_parity section 5, Aurora); the base kernel had the shuffles outside it, and with them back out the multi kernel is bitwise the single one for T=1..8 on gfx1151. No change for CUDA.

Ported from #783 (stuchapin909, head 4327807), split per piece.

Co-authored-by: stuchapin909 <[email protected]>
2026-10-06 00:51:56 +02:00
Niko1221 118937c1ee test(gdn): gdn_parity section 5 says how many y words differ and whether the state does 2026-10-06 00:51:56 +02:00
Niko1221andstuchapin909 9d9c9ca2a4 fix(moe): spell out the FMA chain of the float4 k=10 combine (#783 PR-c)
Found by native_multi_parity on gfx1151 (Aurora): the float4 combine differed from the scalar kernel there, because the first pair's other product was contracted into the FMA. Written as fmaf per expert it is the pinned contract (first product rounded, FMA per expert in order) on every compiler.

Ported from #783 (stuchapin909, head 4327807), split per piece.

Co-authored-by: stuchapin909 <[email protected]>
2026-10-06 00:51:56 +02:00
Niko1221andstuchapin909 868f2ef0c0 perf(mtp): multi-token router and combine in the drafter (#783 PR-c, MTP wiring)
For a window of more than one token the drafter routes with one multi-column router GEMV and one multi-row top-10 and combines all rows in one launch (verify already did; each row is bitwise the single-token call, native_multi_parity). Needs the native router (512 experts, k = 10) and the native combine; otherwise the per-token calls stay.

Ported from #783 (stuchapin909, head 4327807), split per piece.

Co-authored-by: stuchapin909 <[email protected]>
2026-10-06 00:51:56 +02:00
Niko1221andstuchapin909 3281ac326d perf(quantize): one-warp-per-block Q8_0 scaled quantize and fused SwiGLU + Q8_0 in the grouped S2 experts (#783 PR-k)
quantize_q8_0_scaled_kernel reduces the block's max with a warp shuffle (one warp per 32-value block) instead of one thread walking the block; fmax ignores NaN the same way in both. moe_grouped_s2 fuses SwiGLU with that quantize and writes the activation back to gate_up, so the grouped parity checks see the same buffer. STRATA_S2_SWIGLU_Q8=0 keeps the two launches; a static_assert pins FF % 32 == 0 for the fused kernel.

Ported from #783 (stuchapin909, head 4327807), split per piece.

Co-authored-by: stuchapin909 <[email protected]>
2026-10-06 00:51:56 +02:00
Niko1221andstuchapin909 8b5dd7adef perf(verify): the shared expert forks after the doorbell when CPU experts are in the window (#783 PR-j)
STRATA_SH_FORK_LATE=0 forks at the top as before. The other two commit-4 changes of the PR are not taken: the shared-expert
Q8_1 reuse is already in f-140 as the opt-in STRATA_VERIFY_QDEDUP (and the fused q8_1 store), and the PLE native-key batch is
already in f-140 as the opt-in STRATA_PLE_BATCH=1; both would have changed output on the native packs by default.
Dropped on purpose: STRATA_VERIFY_PRELAUNCH and the dead copy_rows_from_mapped<ZERO_HITS>.

Ported from #783 (stuchapin909, head 4327807), split per piece.

Co-authored-by: stuchapin909 <[email protected]>
2026-10-06 00:51:56 +02:00
Niko1221andstuchapin909 3fd0460ba2 perf(mtp): the drafter's catch-up skips the K/V of rejected rows (#783 PR-i)
MtpDrafter::draft now runs the round for T = a + 1: the catch-up writes the K/V of the accepted cells [0, a) and the full layer covers row a, instead of writing all T cells and rewriting cell a. No later attention reaches a cell past the one being drafted and the next round writes it first, so the draft tokens and probabilities are unchanged. draft_first (a = 0) no longer writes its repeated rows at all.
STRATA_MTP_CATCHUP_ALL=1 restores the whole-window catch-up (and the old staging sizes).

Ported from #783 (stuchapin909, head 4327807), split per piece.

Co-authored-by: stuchapin909 <[email protected]>
2026-10-06 00:51:30 +02:00
Niko1221andstuchapin909 2715a75248 perf(iq): 16-lane sub-warp expert gate/up and down kernels, exact-N MMVQ columns, decoded-once small-format tables, two rows per block (#783 PR-h)
iq_kernels.cu: row_dot_80_sub16 (two rows per warp, all lanes active) for the n_embd == 2560 gate/up of every split format, exact-N instantiations of the multi-column MMVQ (launch_mmvq_multi_nc), the 3-entry tail in the grouped gate/up and down kernels. native_mmvq.cu: SmallTraits for Q4_0, Q5_0, Q8_0 and IQ4_NL decode the weight chunks and block scale once per (row, block) and apply them to every activation column with the arithmetic of small_q8_dot, and the multi-column launch runs two rows per block for any K.
Kill switches: STRATA_NO_SUB16_GU=1 (sub-warp gate/up/down), STRATA_OLD_IQ_MMVQ=1 (per-column kernels, existing), STRATA_NO_MMVQ_ROWS2=1 (one row per block at large K). mmvq_multi_parity, iq_multi_parity, native_grouped_parity and native_expert_parity pin the columns to the single-column results.

Ported from #783 (stuchapin909, head 4327807), split per piece.

Co-authored-by: stuchapin909 <[email protected]>
2026-10-06 00:51:30 +02:00
Niko1221andstuchapin909 104a8485e9 perf(gr): multi-token hyper-connection read/write, exact-T GR and BF16 projections (#783 PR-g)
gr_read_multi / gr_write_multi (and native_gr_rms_norm_weighted_multi, native_gr_pre_gated_multi, native_gr_post_multi under them) run T tokens in one launch per step; the verify window's PLE writes, last-layer write and final mixer, and the MTP drafter's write and final mixer use them. The BF16 MMVF multi kernel and the GR up / staged down kernels are instantiated per exact T (1..6, generic above), the BF16 weight tile is unpacked once for all tokens, with the same FMA order as before. STRATA_NO_MULTI_GR=1 restores the per-token calls and the generic kernels.
STRATA_GR_DOWN_MAX4 keeps its meaning from the previous commit (sm_120 default on, explicit setting decides); this commit does not change that default.
gr_multi_parity (new, memcmp): gr_read_multi (with and without injection) and gr_write_multi (in place and out of place) against the single-token calls at the real geometry, T=1..8.

Ported from #783 (stuchapin909, head 4327807), split per piece.

Co-authored-by: stuchapin909 <[email protected]>
2026-10-06 00:51:30 +02:00
Niko1221 5400f39a1f perf(gr): STRATA_GR_DOWN_MAX4 defaults on for sm_120 only (bench #832)
The 4-token accumulator variant of the fused GR down read keeps the same tile, block size, accumulation order and
variant as the kFusedGrMaxT one, so its bits are the same; #832 measured +2.8-3.5% decode with it on an RTX 5070.
Unset, the engine now turns it on for compute capability 12.0 only (decided once per device), and off on every
other card and on the HIP build, where it was not measured. STRATA_GR_DOWN_MAX4=0 or =1 still decides explicitly.
2026-10-06 00:51:30 +02:00
Niko1221andstuchapin909 088e8a8220 perf(rope): fused per-head RMSNorm + RoPE for the verify window and the MTP drafter, off on HIP (#783 PR-f)
native_qsa_rms_norm_rope does norm<256> and the rope in one launch with the same FMA order as the pair (register barriers keep nvcc from contracting across the norm/rope boundary); rope_parity check 6 compares it with native_qsa_rms_norm_weighted + native_rope_apply by memcmp for head_dim 128/256, strided q|gate and contiguous inputs, unscaled and YaRN. The in-place rope also launches one 32-thread block per row.
Verify's norm_rope and q split, and the MTP drafter's q path (all T tokens at once; K for a one-token window) use it. native_norm_rope_usable() gates it: on the CUDA build it is on unless STRATA_NO_NORM_ROPE=1; on the HIP build it is off until rope_parity check 6 passes on AMD hardware, and STRATA_NORM_ROPE=1 turns it on there. The MTP q path's split, FWHT and gate-apply now run over all T tokens in one call each (row-independent).

Ported from #783 (stuchapin909, head 4327807), split per piece.

Co-authored-by: stuchapin909 <[email protected]>
2026-10-06 00:51:29 +02:00
Niko1221andstuchapin909 fe4de5de85 perf(verify): resident_plan as a parallel grouping and prefix scan (#783 PR-e)
The plan kernel grouped duplicate experts and numbered the groups with thread 0 walking all entries; every entry now finds its group in parallel and a packed (entries << 16 | groups) warp prefix scan numbers them. The residency check, the empty plan and *plan_err of #871 are kept as they are. static_asserts pin the 4-warp / packing limits.
resident_plan_parity (new, memcmp): n=1..80 entries over pools of 3..512 experts with and without a slot table, with and without a skip word, plus the non-resident case, against a host replay of the plan's contract.

Ported from #783 (stuchapin909, head 4327807), split per piece.

Co-authored-by: stuchapin909 <[email protected]>
2026-10-06 00:51:29 +02:00
Niko1221andstuchapin909 44ffa86c53 perf(kv): batched K/V and indexer append for the verify window, the commit graph and the MTP catch-up (#783 PR-d)
kv_append_q8_steps / kv_append_q4_steps append n cells in one 3D-grid launch per pool, and native_qsa_indexer_append_steps walks the n indexer cells in order inside one CTA (one barrier between cells). Verify record_window, the commit graph and MtpDrafter::record_forward use them; a multi-slot batch keeps the per-token loop (each row has its own K/V). STRATA_NO_BATCH_KV_STEP=1 restores the per-token calls.
The kv single-cell calls are now the n=1 case of the same kernel. kv_steps_parity (new, memcmp) covers INT8, Q4_0 and the hybrid's folded single-plane calls for n=1..8; qsa_parity gains append_steps against the sequential indexer append at strides 1 and 5 under none/linear/YaRN.

Ported from #783 (stuchapin909, head 4327807), split per piece.

Co-authored-by: stuchapin909 <[email protected]>
2026-10-06 00:51:29 +02:00
Niko1221andstuchapin909 822251bef6 perf(moe): multi-token router kernel, float4 k=10 combine and float4 sigmoid scale (#783 PR-c)
native_router_top10_multi runs one warp per token (8 per CTA) instead of one CTA per token; native_moe_combine(_multi) takes a float4 kernel for k=10 with a shared row on aligned buffers; the shared expert's sigmoid-scale is vectorised. Same arithmetic order, so same bits.
New native_multi_parity (memcmp): the multi router against the single call for n=1..19 on random, tied and NaN logits, and the combine against a host replay of its contract for the float4 and scalar paths.

Ported from #783 (stuchapin909, head 4327807), split per piece.

Co-authored-by: stuchapin909 <[email protected]>
2026-10-06 00:51:29 +02:00
Niko1221andstuchapin909 ae3b249fd2 perf(gdn): exact-T gdn_ab_multi, double-buffered gdn_step_norm_multi with an all-outputs path, and the gdn_parity T=1..8 memcmp check (#783 PR-b)
gdn_ab_multi is instantiated per T=1..6 (generic above) with the weight unpack hoisted out of the token loop; gdn_step_norm_multi prefetches the next token's q/k while the current one runs, and when no state is written and every token is an output (the verify half) it skips the per-token bookkeeping. gdn_step_norm_kernel splits its reduction buffer so one barrier goes.
gdn_parity gains section 5: for every T = 1..8, gdn_ab_multi against fused_gdn_ab, and gdn_step_norm_multi (verify half, commit half, t_out_begin, state-only commit) against the single-token kernel chained, compared with memcmp.

Ported from #783 (stuchapin909, head 4327807), split per piece.

Co-authored-by: stuchapin909 <[email protected]>
2026-10-06 00:51:29 +02:00
Niko1221andstuchapin909 1690a4a38b perf(qsa): early exit in block_scores_multi_kernel and skip a no-op atomic in kv_stream resolve (#783 PR-a)
A CTA of the fixed 256-CTA grid whose first block is past the batch's largest n_bid returns before staging the queries. Same scores, same bits.
STRATA_QSA_EARLY_EXIT=0 restores the old staging. kv_stream resolve_kernel: only threads that looked something up add to the shared counter.

Ported from #783 (stuchapin909, head 4327807), split per piece.

Co-authored-by: stuchapin909 <[email protected]>
2026-10-06 00:51:29 +02:00
Niko1221 16642363bd arena: the per-layer deferred registration is opt-in (STRATA_DEFERRED_REGISTER=1); the default is 0.1.39's order
On the RTX 5070 (Windows, 4 KB pages, the whole-arena registration refused) the deferred per-layer registration made
greedy decode 4-10% slower than registering the sliced arena after the load (0.1.39's order), with identical output
(Q2_0: STRATA_DEFERRED_REGISTER=0 gave +10% on the same build; 12-pair A/B against 0.1.39 flat after).
2026-10-06 00:35:02 +02:00
Niko1221 3ed6276cae docs: STRIX_HALO.md - the shared-expert fork is a gfx1151 default 2026-10-06 00:27:30 +02:00
Niko1221 58f91c144c docs: STRIX_HALO re-measured on the merged 0.1.40 code; STRATA_SH_STREAM=1 on gfx1151 2026-10-06 00:26:51 +02:00
Niko1221 ae93de6261 gfx1151: the shared-expert stream fork is an arch default there (decode +1.8% UD-IQ4_XS, +6.7% UD-Q4_K_XL at 8K, ids identical)
#826 turned the fork off on HIP (it costs discrete RDNA cards). On Strix Halo's unified memory it pays, measured on the
merged 0.1.40 code (stream 140-m). The switch is read on first use, after the arch table ran.
2026-10-06 00:26:51 +02:00
Niko1221 44b0ed4aa6 iq_kernels: q4_0_centred in unsigned arithmetic (signed overflow made the Q4_0 expert kernel wrong on sm_60)
(b & 0x08080808) * 0x1E reaches 0xF0F0F0F0 and overflowed a signed int; on sm_60 (strata_dp4a
fallback) the compiler turned that into garbage: native_expert_parity q4_0/q4_0 and q4_0/q4_1
gpu rel 1.7e4 and 6.8e2. Unsigned wrap is defined and gives the same bits.
2026-10-05 23:54:06 +02:00
Niko1221 4a7a9871fb serve tests: the image-marker test no longer pins control-token text as special (#931 makes it text) 2026-10-05 22:46:53 +02:00
Niko1221 f53eb1b96a tools tests: the golden compare replaces only the whole engine name (#973)
On Linux the engine is called strata, which also rewrote strata-<model>.log and made
every Enter-for-every-question case differ from the baseline.
2026-10-05 22:46:53 +02:00
Niko1221 d1ec32b4ff tools tests: no WSL in the unsloth tests, and the HIP zip test names the engine for the platform (#974 #975) 2026-10-05 22:46:53 +02:00
Gabe Ortiz bae372b9aa serve: pin Claude Code's per-request billing stamp, so the conversation cache reuses agent prompts
Claude Code starts its system prompt with
`x-anthropic-billing-header: cc_version=2.1.170.bf4; cc_entrypoint=sdk-cli; cch=b145e;`.
cch changes on every request and the version's 4th part on every session. The
template renders the tool list first, so every turn's prompt differed ~22K
tokens in and only the last 16K checkpoint was reused. Both stamps are pinned to
f's, as llama.cpp does (ggml-org/llama.cpp#21793); only a header at the very
start of the system text is touched.
2026-10-05 22:46:53 +02:00
Niko1221 e9fc48f3f7 tools tests: the #425 disk-room test starts its second run without a draft layer (#897 counts an existing one) 2026-10-05 22:46:53 +02:00
Niko1221andChanyeong Lim 98b7ea944d serve: a control token's text in a message is encoded as text (<|im_start|>, <|im_end|>, <|endoftext|>) (#931)
Port of PR #931 onto the #554 vision-tag marks: literal_tags() adds the tokenizer's
control tokens to LITERAL_TAGS, longest first; a prompt with none of them is
rendered and encoded exactly as before.

Co-authored-by: Chanyeong Lim <[email protected]>
2026-10-05 22:46:53 +02:00
Niko1221 3ce3358232 setup: kv_streaming_wanted follows the current rule (K8V4 streams since the KV_CELL_BYTES change)
Port of #945 onto 0.1.40: the PR excluded k8v4, which setup no longer does.
2026-10-05 22:46:53 +02:00
pipeob0andStrata qwen3.8-flash-next-iq3_s 3b2e14cc11 setup: the low-RAM and KV-streaming decisions as functions (low_ram_wanted, kv_streaming_wanted)
Step 5 and step 7 decided these two settings inline. A front-end that has to say what a model would change needs the
same answer, so the rules are named and setup calls them itself: low_ram_wanted is the old `budget is None and (...)`
(budget is set only for a size that has a RAM budget), and the streaming branches keep their messages and say they
are kv_streaming_wanted.

Behaviour unchanged, and checked: test_setup_risk's new RulesAgreeWithTheWrittenConfig runs 54 mocked installs over
three sizes, two contexts, three --kv and three --kv-streaming choices, and 24 more over two PCs and four --low-ram
choices, and compares each written config's --kv-resident / --resident-experts / --mmap-experts with the function.

Co-Authored-By: Strata qwen3.8-flash-next-iq3_s <strata@localhost>
AI-developed: written by an agent (pi) running on Strata's own local model, qwen3.8-flash-next-iq3_s; tested by hand on an RTX 5060 Ti 16 GB / Ryzen 7 5700X3D / 64 GB PC with engine 0.1.39
2026-10-05 22:46:53 +02:00
bsvinay c8c7e216ba sycl/tools/build.sh: keep the REPO override next to the git-root default (#972) 2026-10-05 22:46:53 +02:00
bsvinay ed15a3d04b setup: the disk check counts only what step 6 still writes
An existing pack experts.bin (low-RAM copy or the AVX-512 Q2_0 conversion) and an
existing MTP draft layer need no new room. A re-run of setup on an installed model
used to demand ~arena_gb + 9 GB free and fail on a nearly full models volume.
2026-10-05 22:46:53 +02:00
Joe-Huber 1c59b7087f chore(git): add standard IDEs to gitignore
- Idea/ for jetbrains
- .vscode/ for vs code
- .cursor/ for cursor
2026-10-05 22:46:53 +02:00
LJYBowser c7aec446e3 Fix hard-coded path in sycl/tools/build.sh 2026-10-05 22:46:53 +02:00
Niko1221 e02815a5e5 serve: ending the server removes the image encoder's scratch directory (#914) 2026-10-05 22:46:53 +02:00
Niko1221 4f131fcdcc setup --check: RAM below every model's floor is a warning and the verdict says so (#977)
The RAM line was [ok] and the verdict 'This PC can run Strata' although every size
said 'does not fit'. Now the line is [!], the verdict says no size fits, and the exit
code is 1 for scripts. Check only: --model NAME --yes still installs one anyway.
2026-10-05 22:46:53 +02:00
Niko1221 d034539560 docs: --kv-grow's conditions as the engine states them 2026-10-05 21:53:03 +02:00
Niko1221 07780dae1c docs: the 0.1.40 opt-ins and switches without a doc line; Strix Halo linked from the README (7 languages) 2026-10-05 21:50:43 +02:00
Niko1221 4552a7f164 setup: MIN_ENGINE 0.1.40 (#848 split+resident, #711 K8V4 streaming); CMake version 0.1.40
MIN_ENGINE >= RESIDENT_SPLIT_ENGINE now turns on the resident low-RAM mode on a layer split: the golden
baseline records the new 2-GPU configs (both cards, --resident-experts, --remote-expert-opt), and the
tests of the pre-0.1.40 behaviour pin MIN_ENGINE to 0.1.39. CMakeLists' project version is what
source_version() reads, so a compiled engine must say 0.1.40 to satisfy MIN_ENGINE.
2026-10-05 21:48:46 +02:00
Niko1221 3772ded7c6 setup: bench-data recommendations (tips only), calibrate tries more pool-worker counts
- bench_tips(): a warning for a fixed --expert-cache on Windows (#780 #781), --prefill auto:32768 only as a tip
  with 96+ GB RAM and a warning below 64 GB, STRATA_RESIDENT_HEADROOM_GIB=6 on resident configs with <= 48 GB,
  --conversation-cache-mib 8192 for agent clients when 24+ GB stay free, --vision cpu on <= 12 GB cards.
- a 2-socket tip for --pool-workers (one socket's cores - 1).
- calibrate also tries a quarter of the default, P-cores - 1 and one socket's cores - 1.
No default, config or engine argument changes; --mtp-window left out on purpose.
2026-10-05 21:44:09 +02:00
Niko1221andEddoursul 076108957c F12: --host-core first|last (fork 5137b33)
--host-core: the host thread's core, first (the default) or last, the workers on the others.

Windows sends a GPU's interrupts to one logical processor, usually the first, and every copy that lands raises one
(~13 us in the ISR and a DPC): a host that spins on the GPU's flags there waits for them (eddoursul's fork, F12:
the host saw a layer's ring up to 30-110 us late, p99).  --host-core last (or STRATA_HOST_CORE=last) puts the host
on the last physical core and lets the workers have the first.  The default is the layout the pool has always used;
a hybrid CPU keeps it.  The startup log names the processors the pool workers and the host thread run on.  The
placement changes no result, only where threads run.  The fork's other part (a per-second re-pinning from the
processors' interrupt time) is not ported.
Ported from eddoursul's fork, commit 5137b33 (MIT).

Co-authored-by: Eddoursul <[email protected]>
2026-10-05 21:00:01 +02:00
a3642eb4be F12: one copy_blobs helper for the expert uploads: the PCIe share's and the adaptive refills' copies as one batch.
Builds on midhatn's #807 (cudaMemcpyBatchAsync for the verifier's DMA group).  dma_batch.hpp now holds one
helper, copy_blobs, for independent host-to-device copies on a stream, which the verifier's PCIe share
(copy_expert_blobs) and the adaptive tier's refills (both loops of generate.cpp: --serve's main device, the CLI's)
use.  Changes against the pull request, from the review: mode 0 is the old loop untouched (it never stops the
engine: the PR aborted on any copy error, which 0.1.39 ignored); the Windows host alias (cudaHostGetDevicePointer)
is looked up once per blob; a batch the runtime refuses is redone as the loop with one warning; HIP, CUDA before 13
and groups over 128 keep the loop; the batch order is the stream's.  eddoursul's fork batches the same copies
(F12: under WDDM each copy raises an interrupt where it lands, on the processor the GPU's interrupts go to).

STRATA_DMA_BATCH is 0 (off) unless set; 1 batches, 2 adds CUDA 13.4's overlap hint.

Co-authored-by: midhatn <[email protected]>
Co-authored-by: Eddoursul <[email protected]>
2026-10-05 21:00:00 +02:00
midhatn c8a73fbf98 cuda: add opt-in batched expert upload submission (PR #807 by midhatn)
Cherry-picked unchanged from the pull request, for F12; the follow-up commit turns its helper into the single
copy_blobs helper and fixes what the review found.
2026-10-05 20:59:49 +02:00
Niko1221andEddoursul edc592b1ef F7: the final mixer in one fused read (fork 2ec7a80)
The final mixer in one fused read.

The hyper-connection read before the head ran gr_read's kernels once per token of the window (lm_head_mix), and once
per draft pass in the draft layer.  Both now go through fused_gr_read_multi, the layers' read (no injection weight:
the final mixer), the window's tokens in one launch.  Its sums are gr_read's (fused_gr_read_multi is bitwise
fused_gr_read, which gr_parity checks against the plain read).  CUDA only; HIP keeps gr_read per token
(STRATA_HEAD_MIX_MULTI=0 does too).  This tree's opt-in STRATA_FUSE_HEAD_GR path is unchanged.
Ported from eddoursul's fork, commit 2ec7a80 (MIT).

Co-authored-by: Eddoursul <[email protected]>
2026-10-05 20:59:49 +02:00
Niko1221andEddoursul 4a5713fbb4 F7: the draft pass's kernels in the main layer's forms (fork d3d48b1)
The draft pass's kernels in the main layer's forms.

- quantize_q8_0_scaled runs a warp a 32-value block (it ran a thread a block: 80 threads for the draft layer's
  input): the block's |max| over the same values, then each value's bytes as before.  CUDA only.
- The draft graphs' inputs from mapped memory in one copy kernel (copy_from_mapped_multi): 7 launches in the round
  graph and 2 in a step become one each (CUDA; HIP keeps its separate copies).
- The shared expert on a branch of the draft graphs beside the router and the routed experts, joined before the
  combine (CUDA; HIP keeps one stream; STRATA_MTP_SHARED_BRANCH=0 turns it off).

The fork's verify_router in the draft pass and its native-format draft experts are not part of this commit (they
belong to F1 and F15).  Ported from eddoursul's fork, commit d3d48b1 (MIT).

Co-authored-by: Eddoursul <[email protected]>
2026-10-05 20:59:49 +02:00
Niko1221andEddoursul 352cad8f0d F7: a one-token window commits itself (fork 235784c)
A one-token window commits itself.

A one-token window always keeps its token, so its graph now advances the sequence state itself: the GDN step kernel
writes the state (n_keep = 1, the arithmetic the commit's replay used), the conv kernel writes the history it read
(gdn_conv_l2_multi's commit flag), and its indexer append and PLE history were final already.  Verifier::commit
launches no commit graph after it: the draft round graph then runs alone instead of beside the commit's kernels,
which read and write each GDN layer's 3 MB state.

CUDA only (HIP keeps the commit graph after every window; STRATA_ONE_TOKEN_COMMIT=0 does too); batch windows keep
theirs.  Ported from eddoursul's fork, commit 235784c (MIT).

Co-authored-by: Eddoursul <[email protected]>
2026-10-05 20:59:49 +02:00
Niko1221andEddoursul ca83311616 F7: the draft round graph runs the draft layer's front once (fork 22b46b6)
The draft round graph runs the draft layer's front once.

MtpDrafter::capture_round ran the draft layer's front (the embedding, the fc projections, the attention
hyper-connection read, the K/V projections and appends) over the window's T cells, then the whole layer again for
row a, repeating row a's front.  record_forward is now record_front (T rows) and record_rest (one row: the attention
from the q projection, the MLP, the final mixer, the head); the round graph runs the front once, copies row a's
residual, injection and mixed input to row 0 (copy_row_to_first, the row read on the device) and runs the rest
there.  The multi-row front is per row the one-row front's arithmetic (checked by the window hashes on fixed text).
The coupled-sampling and rotated-KV paths of this tree stay in the same functions.
Ported from eddoursul's fork, commit 22b46b6 (MIT).

Co-authored-by: Eddoursul <[email protected]>
2026-10-05 20:59:26 +02:00
Niko1221andEddoursul 3e4aa1d618 F7: the head's argmax and the draft's top probability over many blocks a row (fork fbb8bfa)
The verify head's argmax and the draft's top probability over many blocks a row.

The draft head's top probability ran row_top_prob, one 1024-thread block a row over the 40,525-token draft head
(and the head's greedy pick, sampler_greedy_kernel, one block a row on sm_80 to sm_89).  argmax_rows scans a row
in up to 128 blocks (4,096 logits each), the row's last block merging their picks (the larger value, on equality
the lower index: the one-block kernel's order); row_top_prob_split computes row_top_prob's 32 warps' sums in 8
blocks, the row's last block adding them in order.  Both keep their counters and partials in per-row slots of one
scratch.  Bitwise: the new verify_parity test checks them against sample_tokens' greedy pick and row_top_prob (ties,
NaN, -inf, 1-8 rows on one scratch).

CUDA only: argmax_rows is used on sm_80 to sm_89 (sm_90+ keeps sample_tokens' cluster kernel); row_top_prob_split on
every CUDA card; HIP and the cards below sm_80 keep the old kernels (STRATA_MULTI_BLOCK_ARGMAX=0 turns both off).
Ported from eddoursul's fork, commit fbb8bfa (MIT).

Co-authored-by: Eddoursul <[email protected]>
2026-10-05 20:59:25 +02:00
Niko1221andEddoursul ac131a3af2 F19: --spec-follow and --window-hashes test tools (from eddoursul's fork)
--spec-follow PATH makes a generate run emit a given continuation (token ids)
instead of the window's argmax, and accept the drafts that match it, so runs
with different speculation settings (or two builds) process the same text.
--window-hashes PATH writes one line per verify window: its index, position,
size, an FNV-1a hash of its final residual rows and its argmaxes; two builds
doing the same arithmetic write the same file.  Both are off by default and
change nothing when unused.

Co-authored-by: Eddoursul <[email protected]>
2026-10-05 20:59:25 +02:00
Niko1221 654f4af665 CMake: conversation_file.cpp back in strata_engine after the rebase 2026-10-05 19:58:18 +02:00
Benjamin Larsson d69424c965 decode: STRATA_ADAPT_LAG - #463's reproducibility without its stall (#764, ported onto 0.1.40's adaptive tier)
A verify window takes the previous adapt round's swaps once they are STRATA_ADAPT_LAG windows old. The default is 1,
exactly 0.1.39; lag 2 is opt-in. pending_age resets after every pending.clear(). The batch paths, --pipeline-windows
and the peer tier are not lagged.

Co-authored-by: Benjamin Larsson <[email protected]>
2026-10-05 19:58:18 +02:00
Niko1221 9c2be9fdb3 Revert "STRATA_HC_Q8: raise the dynamic shared-memory cap of the Q8 down-kernel instances (the verify window failed in capture)"
This reverts commit e44764435abc2907dbed1019be1bfb7bccd4d6a9.
2026-10-05 19:58:18 +02:00
Niko1221 2fb9b6440a STRATA_HC_Q8: raise the dynamic shared-memory cap of the Q8 down-kernel instances (the verify window failed in capture) 2026-10-05 19:58:18 +02:00
Niko1221 956fdfcbfb iq_kernels: redo the Q6_K gate/up hunks on the rebased file (opt-in STRATA_Q6K_EXPERTS) 2026-10-05 19:58:17 +02:00
Niko1221 b55c48b29e CMake: fix the merge of the parity-pair list after the rebase (q4_0 pairs, Q6_K refusal test when the option is off) 2026-10-05 19:58:17 +02:00
Niko1221 edc0cb6490 rebase onto f-140: leave the PLE Q8_0 reader to the #651 series already in f-140 (drops the duplicate hunks of #860) 2026-10-05 19:58:17 +02:00
Niko1221andJakub Luwierski 4a9b9041c2 prefill: owned buffers take the ring in one allocation, exact owned pricing (STRATA_OWNED_PRICE=exact), OOM diagnostics (#796 part A)
- carve: with owned buffers the streamed ring is one cudaMalloc instead of up to 1,024 pieces each rounded to 2 MiB
  (~0.5 GiB). A borrowed region keeps its per-slot layout and price, so the loans' slot counts do not move.
- Prefill::bytes_needed_owned prices the real allocation (2 MiB pages, the ring in one piece). The cache sizing uses it
  only with STRATA_OWNED_PRICE=exact; the default is still 0.1.39's 160 + chunk * 680 / 1024 rule.
- the "device buffers do not fit" error names the free VRAM, the bytes taken and the buffer that failed.
No refusal was added: parts B (borrow_residency) and C (the advisory planner) stay out.

Co-authored-by: Jakub Luwierski <[email protected]>
2026-10-05 19:58:16 +02:00
Niko1221 aaf99d3074 native experts: IQ3_S one-token AVX-2 kernel is opt-in (STRATA_IQ3S_MT1=1); the default keeps 0.1.39's rounding
Items 1-2 of #863 (gathered decode per core, AVX-VNNI rows) are bitwise and stay. Item 3 changes a lone token's IQ3_S
rounding on Intel hybrid CPUs, so it needs the switch. The #706 bit-plane path stays in front of the VNNI/plain rows.
2026-10-05 19:58:15 +02:00
Francesco Albano d4b8b6ad5c native experts: IQ3_S takes the AVX-2 kernel for one token too where its grid is gathered
#152's rule gives a lone token's gate/up rows to ggml's dot, because the multi-token kernels were no faster at one
token. With the gathered decode that no longer holds for IQ3_S on the Intel cores that gather it: an expert's gate/up
rows for one token on an i9-14900KF P-core take 0.34 ms with ggml's dot and 0.19 ms with the AVX-2 kernel. And in
decode most CPU experts serve one token of the window.

So on a CPU without AVX-512 where cpu_gather_fast() holds (Intel, Alder Lake or later, see the gather commit) and
STRATA_IQ256_GATHER is not 0, IQ3_S takes the multi-token kernel from one token on. This only pays together with the
gathered decode: on the same CPU's E-cores, which keep the scalar decode, it is 0.71 -> 0.68 ms, about even. That is
why it follows the gather's machine-wide rule and not the per-thread one. Measured in the engine with Swift 1.5
IQ3_XXS (whose IQ3_S layers this reaches): the CPU expert time per verify window -4%.

This changes results. A lone token's IQ3_S gate/up rows now round like the multi-token kernel's (the order of the
float additions), no longer like ggml's dot. They are the same rows the token gets in a group, so on these CPUs IQ3_S
experts no longer depend on how many drafts a window held (#152), and every core rounds them alike. Down rows and
the other formats keep the default rule. STRATA_IQ_MT_MIN set (2 is the old default) keeps its rule for every format.

The decision is native_gu_mt_min(): every probe is in expert_layout.cpp and cpu_avx2_ok() comes first. Nothing
compiled for AVX2 runs before that check, including on an older-CPU build (STRATA_ISA_FLOOR) on an AVX-only CPU.
iq_avx2_parity checks native_gu_rows wherever native_gu_mt_min is 1: each token's rows must match alone and in groups
of 2..8. docs/DETAILS.md notes the new default next to STRATA_IQ_MT_MIN.
2026-10-05 19:58:15 +02:00
Francesco Albano b211ba173a cpu kernels: AVX-VNNI forms of the AVX-2 i-quant, IQ4_NL and Q2_0 rows (cpu_avxvnni_ok)
Alder Lake, Sapphire Rapids and later have AVX-VNNI on every core: vpdpwssd does the madd and the add of an i-quant
row in one uop, and vpdpbusd the maddubs + madd(ones) of the IQ4_NL and Q2_0 down rows. The sums are integer, so the
results are bit for bit those of the AVX2 forms: vpdpwssd wraps like madd + add, and the maddubs it replaces in the
down rows cannot saturate (IQ4_NL |w| <= 127, Q2_0 codes 0..3).

The row kernels move to iq_avx2_rows.inl and q2_avx2_rows.inl, which iq_avx2.cpp and q2_avx2.cpp include twice: once
as they were (namespace plain) and once with AVX-VNNI (namespace vnni). In the second copy every function is declared
with target("avxvnni") under GCC/Clang, so the intrinsics compile there and nowhere else: neither file gets
-mavxvnni, which would let the compiler use the extension in the plain path. MSVC takes the intrinsics under
/arch:AVX2. CMake checks that the compiler has them (GCC 11, Clang 12) and otherwise builds without the copies
(STRATA_AVXVNNI=0).

cpu_avxvnni_ok() in expert_layout.cpp, which is not compiled for AVX2: cpu_avx2_ok() and CPUID 7.1:EAX[4], off under
STRATA_FORCE_ISA (a CPU that stops at AVX2), off in a STRATA_ISA_FLOOR build, and STRATA_NO_AVXVNNI=1 turns it off.
The VNNI copies are reached only through the AVX2 kernels' entry points, which the engine already calls only behind
cpu_avx2_ok().

Measured on an i9-14900KF on top of the gathered decode: another 4-10% on a P-core's i-quant gate/up rows, 15-25% on
an E-core's Q2_0 down rows.

iq_avx2_parity runs the VNNI variants as well (iq4nl256_down_rows_v, q2_0_gguf_rows_multi_avx2_v): every combination
bit for bit against the scalar one-token rows, 1..8 tokens.
2026-10-05 19:58:15 +02:00
Francesco Albano a733617558 iq_avx2: the gathered IQ3_XXS / IQ3_S / IQ2_S decode on the cores where it is faster (STRATA_IQ256_GATHER auto)
0736f05 reads the IQ3 grid words with one gather under STRATA_IQ256_GATHER=1, on every core, and keeps it off by
default because AVX2 gather throughput depends on the core. This makes the gathered decode its own Fmt32 (118, 121,
122), chosen once per call instead of a branch in every decode, and takes it further:

- IQ3_XXS: the four even-sign masks by one 64-bit gather as well;
- IQ3_S: the 9th index bit by one sllv and one and; the sign bytes by one broadcast and one shuffle;
- IQ2_S: the four 10-bit grid indices built in a register and read by one 64-bit gather, the signs as IQ3_S's.

Each is its format's scalar decode bit for bit: the same words land in the same lanes. IQ2_XXS measured 4% slower
gathered and IQ2_XS has its own sign decode, so both keep the scalar one.

STRATA_IQ256_GATHER: unset = auto, 0 = the scalar decode everywhere (as before), 1 = the gathered one everywhere.
Auto gathers on a thread whose core is an Intel performance core of Alder Lake / Sapphire Rapids or newer:

- GenuineIntel with AVX-VNNI (CPUID 7.1:EAX[4]) as the generation marker. That leaves out Haswell/Broadwell (slow
  gathers) and Skylake through Ice Lake / Tiger Lake (gathers slowed by the Downfall/GDS microcode);
- not the E-core-only parts, which are not hybrid: Alder Lake-N / Twin Lake, Grand Ridge, Sierra Forest and
  Clearwater Forest, by model;
- on a hybrid CPU only a thread on a P-core (CPUID 1Ah core type 40h): the E-cores gather slower than they assemble.

AMD keeps the scalar decode (Zen 2/3 gather slowly; Zen 4/5 take the AVX-512 kernels). STRATA_FORCE_ISA turns auto
off. The probes are in expert_layout.cpp, which is not compiled for AVX2, and the core type is asked once per thread
(the pool pins its workers).

Measured on an i9-14900KF, gate/up rows of one expert at three tokens on one P-core, scalar -> gathered: IQ3_S 0.40
-> 0.27 ms, IQ2_S 0.31 -> 0.27, IQ3_XXS 0.26 -> 0.24. On its E-cores the gathered decode was slower.

iq_avx2.cpp has no runtime initializer again (d072203): the switch and STRATA_IQ_PREFETCH are read on first use.

iq_avx2_parity, a CPU-only CTest: random blocks (every grid index and sign pattern occurs) for IQ2_XXS, IQ2_XS, IQ2_S,
IQ3_XXS, IQ3_S and IQ4_XS gate/up rows and IQ4_NL / Q2_0 down rows; every variant, 1..8 tokens, each token's rows bit
for bit the scalar variant's rows for that token alone; the scalar rows against ggml-cpu's vec_dot (Q2_0: a double
reference). --bench: ms per expert on one thread, each variant, ggml's per-token dot and the engine's dispatch, over
more expert blobs than an L3 holds.
2026-10-05 19:58:14 +02:00
Niko1221andThomas Weaver ac7db2e522 UD-Q6_K_XL: Q6_K gate/up kernels behind -DSTRATA_Q6K_EXPERTS=ON; the Q8_0 PLE hunk is left to the #651 series
The PLE Q8_0 reader duplicates #651 and is dropped here. The grouped Q6_K instance is built only on request
because every instance is loaded at engine start (EAGER) and costs slots for every other model.

Co-authored-by: Thomas Weaver <[email protected]>
2026-10-05 19:58:13 +02:00
Thomas Weaver 8c4a6e1de7 docs: record the UD-Q6_K_XL server run and register its parity pair
Gate/up is Q6_K on 47 of 48 layers and Q8_0 on layer 2; down is Q8_0 on
every layer. The page had those roles swapped. native_expert_parity
q6_K/q8_0 is now a normal test: the old one expected the pair to be
refused, and native_expert_supported accepts it.

The measured server runs are on this machine (RTX PRO 4500 Blackwell,
engine 0.1.39 after the rebase, and one cold prefill on 0.1.38). Quality
against llama.cpp is still unmeasured, and the three Q6_K parity
programs have not been run on a GPU.
2026-10-05 19:58:05 +02:00
Thomas Weaver 87659162ed Q6_K gate/up through the native expert paths (Unsloth's UD-Q6_K_XL experts) 2026-10-05 19:58:05 +02:00
Thomas Weaver 9227973b36 PLE table as Q8_0 (Unsloth's UD-Q6_K_XL) + docs
PleTable accepts per_layer_token_embd.weight as Q8_0 (170-B rows): the
UD-Q6_K_XL file ships its PLE table in Q8_0, not IQ4_NL. The row reader
is already parameterized by bytes-per-row; only the type check, a
dequantize_q8_0 block loop (5 x 34 B) and the PLE_ROW_BYTES_MAX bump
(160 -> 170) were new. No GPU kernel changes: gather outputs floats.

- ple_reader_test --selftest also runs 170-B rows (90/110 were already run)
- ple_q8_parity: real-artifact Q8_0 PLE rows against ggml's reference
  dequantizer, through Mmap and Direct (bit-exact on the synthetic
  fixture from tools/ple_q8_fixture.py)
- docs/UNSLOTH_Q6.md: the manual workflow, verified shard inventory
  (169.2 GB, 6 shards, table alone in shard 3), pending real-run numbers
2026-10-05 19:57:36 +02:00
Niko1221 fa9bd5fbfa layer split auto: the coverage curve only from 4 GPUs (2- and 3-GPU placements keep the 0.1.39 curve) 2026-10-05 19:57:36 +02:00
Gopi e2b32892a6 layer split: score every four-way placement instead of guessing it
A four-GPU run picked its split from a formula that reads only `layer_ms`
(SMs x clock) and shares the layers in proportion to speed; the free VRAM
that decides how many experts a stage can actually hold never entered it,
so the search could hand the most layers to the card with the least room
and never notice.  All C(L-3,3) placements are now scored - 15,180 at 48
layers - and 2,068 of them cost nothing extra, because a stage's fill
depends only on its own layer range and its own free VRAM: `predict`
already hands each ranked pair to the one stage that owns its layer and
stops each stage at its first pair that does not fit, so a range is walked
once and memoised.

The cost model's mass curve changes with it.  `mass[r]` was (r+1)^-1.2,
fitted to the 5080+3090 sweep - still right on that rig (99.35% predicted
at 20,000 held, 99.4% measured) and wrong on IQ3_S, where it claims the
top 5,805 pairs hold 94.9% of the routed mass and the rig measures 69.2%
over four boots and 14.9M decode lookups.  No exponent reconciles them,
because the curve is a property of the model's routing and the profile
carries only a ranking, with no counts.  The mass is now the increment of
H(f) = 1 - (1 - f)^b over the held fraction, whose prefixes sum to exactly
H(N/total); b = 3 (STRATA_SPLIT_COVER_B overrides it) is chosen to leave
the validated regime where it was - at f = 0.8138 it gives the old fit's
99.35% - while at f = 0.59 it gives 93.2% against 92.2% measured.

This is the placement search from #584, carried on v0.1.39 and without the
weight carve it rode in on: v0.1.39 ships its own carve (#559 / #639), and
the search only runs under `--layer-split auto`, where nothing is carved -
so `cap[i]` is already net of the dense weights and a stage's room is what
`predict` already subtracts, its session.

Measured on the 4-way IQ3_S rig (2x RTX 3060 + 2x RTX 5060), same config and
same flags in both arms, the binary the only difference: the four-way split
moves from K=10,19,34 to K=14,31,41, the prompt chunk the starved cards allow
grows 768 -> 1280 tokens, prefill goes 425 -> 597 tok/s (+40%) and decode
44 -> 51 tok/s (+16%), with the decode expert-cache hit 74.7% -> 83.3%.  Two
boots per arm, and the split is deterministic.  The curve and the search are
one commit, so that measures the pair, not the search alone.
2026-10-05 19:57:36 +02:00
konijiwa110 1bb5c72484 qsa top-k: Turing prefill above ~90K cells takes the wide kernel (from #743; STRATA_TOPK_STREAM=0 restores)
The duplicate block_topk_stream_kernel is dropped: 0.1.39's block_topk_wide_kernel gives the same ids.
Only the 90-135K Turing band is new (counted bound, sm_75). Parity cases from the PR are kept.
2026-10-05 19:57:36 +02:00
Niko1221andarchitectds 5e53c516b3 prefill: equal chunks per prompt segment, opt-in STRATA_PREFILL_EQUAL=1 (from #693)
A segment reads in ceil(tokens / max_chunk) chunks of equal size, kept only where the last chunk would stream
every expert anyway. Used only at the segment call sites; request_chunk is unchanged.

Co-authored-by: architectds <[email protected]>
2026-10-05 19:57:36 +02:00
Niko1221 f07f060ae7 session files: RESTORE makes room in the elastic K/V (--kv-grow) before the device copy (review finding) 2026-10-05 19:57:36 +02:00
Niko1221andBenjamin Larsson 2467bb6899 cpu: Q2_0 AVX2 bit-plane kernel is opt-in (STRATA_Q2_BITPLANE=1), image built only when on, q2_bitplane_parity test
Co-authored-by: Benjamin Larsson <[email protected]>
2026-10-05 19:57:36 +02:00
Benjamin Larsson 9c9f639f25 cpu: Q2_0 bit-plane rows reduced four at a time
Down rows are only 10 blocks, so the per-row reduction was a large share; four rows now share one hadd tree. The single-row reduction uses the same tree, so a row's bits do not depend on how rows are split into tasks. Down rows +6-7% per core.
2026-10-05 19:57:36 +02:00
Benjamin Larsson 1048d2392b cpu: bit-plane AVX2 Q2_0 row kernel
The activation is pre-permuted once per token into the 2-bit codes' plane order (ActQ::qp/psum/pscale), so rows need no unpack interleave: four planes summed in int16, one madd, the -1 code offset subtracted exactly in the integers, one FMA per two blocks per token. 2.3-2.8x faster per core than the b256 kernel, closer to exact (rms rel 1.0e-7 vs 2.7e-7), width-invariant. STRATA_Q2_LEGACY=1 selects the previous kernel.
2026-10-05 19:57:36 +02:00
Maurizio Verde ffaf958eaa session files on 0.1.39: refused with --batch (#465 parallel requests: the server answers 501, the engine SERR); the STRATA_ARENA_MMAP arena records its experts.bin as a model input; docs 2026-10-05 19:57:36 +02:00
Maurizio Verde 1263ccdc91 docs: session files r4 (SWAIT/progress protocol, preflight wording, version identity, POSIX/Windows flush, continuation limit, r4 measurements) 2026-10-05 19:57:36 +02:00
Maurizio Verde 8b2160888b session files r4: SAVE RAM preflight before any checkpoint copy; progress after every block and bounded SWAIT allowances for blocking steps (review A/B) 2026-10-05 19:57:36 +02:00
Maurizio Verde 07e0c26f94 session files r3: RAM preflight for save; keep.bin.tmp planted-name test 2026-10-05 19:57:35 +02:00
Maurizio Verde d0289b88ba session files r3: include <memory>; Out initialised by members 2026-10-05 19:57:35 +02:00
Maurizio Verde 796bb8fc9c session files: resolved expert inputs, runtime read bounds, published-save status, typed errors, strict protocol, UNC free space (review R1-R6) 2026-10-05 19:57:35 +02:00
Maurizio Verde 71faad4f4d conversation_file_test: format v1 golden (header offsets, size and hash of the fixed sample) 2026-10-05 19:57:35 +02:00
Maurizio Verde ace0cfc866 docs: session files after the review fixes (identity, durability, errors, measured) 2026-10-05 19:57:35 +02:00
Maurizio Verde e41463a995 Session files: review fixes (exclusive temp files, no-follow restore, full identity, bounds, durability, server hardening) 2026-10-05 19:57:35 +02:00
Maurizio Verde 0c9f21e257 docs: session files (the slot save/restore API, its answers and errors, format v1, the checks, measured at 63K) 2026-10-05 19:57:35 +02:00
Maurizio Verde aa17fe8648 Session files: save and restore a conversation to disk (SAVE/RESTORE, POST /slots/0?action=save|restore)
A conversation the engine holds (what a parked snapshot holds) goes to one file and comes back, also after a
restart, so a long prompt is not read again:
- engine: `SAVE <path>` / `RESTORE <path>` between requests, answered `SAVED|RESTORED <tokens> <bytes> <ms>` or
  `ERR <reason>`; single GPU only (refused with --layer-split) and needs --prompt-cache > 0.
- format v1 (src/core/conversation_file.cpp): 64-byte header bound to a model fingerprint (name, size, first and
  last MiB of each weight file) and a config fingerprint (engine version, --kv, STRATA_KV_ROT, --kv-resident,
  --max-context, --mtp-window, control vectors), the payload, a payload hash and an end marker; written to
  `<path>.tmp` and renamed over `path` (MoveFileExW on Windows).
- fail-closed restore: size, header, both fingerprints, every count against the bytes left, the payload hash and
  the usual snapshot validation all run before any device write; a refused file leaves the session untouched.
- only the deepest checkpoint is saved (the next turn's resume point) and the K/V is streamed from the pools
  straight into the file's 16 MiB blocks, without a full host capture; Linux uses O_DIRECT when the filesystem
  takes it (buffered otherwise or with STRATA_SESSION_BUFFERED=1), Windows buffered Win32 I/O.
- serve/server.py: llama-server's `POST /slots/0?action=save|restore {"filename": NAME}` with --slot-save-path DIR
  (or "slot_save_path" in the config); NAME must be a plain file name inside DIR.

RTX 4070 Ti, IQ3_XXS, a 63,025-token conversation: file 1.20 GB, save 0.72 s, restore 0.89 s, the next 32-token
turn after a restore 1.15 s (first token 0.55 s) with the same output tokens as without saving.
Tests: conversation_file_test (CPU, no model; also built for Windows and run under Wine),
conversation_snapshot_test, serve/test_slots.py, serve/test_security.py.
2026-10-05 19:57:35 +02:00
Niko1221 f81876ecef kv-grow: ported over --vram-elastic (#533); off, with a warning, under --batch, --vram-elastic and --peer-device
The expert cache arena now has three backings (segmented #533, one VMM range for --kv-grow, cudaMalloc), tried in
that order.
2026-10-05 19:57:35 +02:00
sergiywith 025bf3b2ef Elastic K/V: VMM access failures roll back, failed growth stops the engine, the trim maps one run
From an audit of this PR, each finding checked against the code:
- VmmRange: a run whose cuMemSetAccess failed stayed counted as mapped (a later map_range skipped it, so the chunks
  were never made accessible); it is unmapped and freed again (commit_run).
- a failed growth or trim could leave host_res, d_res and the slot range half-changed while serve went on to the
  next request; such a failure now stops the engine, as other CUDA failures do. The residency uploads are checked.
- the trim maps the chunks it takes back as one run (one cuMemSetAccess instead of one per 2 MiB chunk) and refills
  the slots with async copies and one sync instead of a blocking cudaMemcpy per expert.

IQ2_XS: the served scenario (grow at a request and mid-decode, trim and refill, regrow, a parked conversation's
restore) runs as before; vmm_test passes.
2026-10-05 19:57:35 +02:00
sergiywith 9e3dbfe74a Elastic K/V (--kv-grow): the K/V takes VRAM as the context grows, the expert cache holds the rest
At --max-context 262144 the K/V is allocated whole at start (3.35 GiB at int8 with the drafter's) and the expert
cache is sized around it. With --kv-grow (or STRATA_KV_GROW=1) the K/V pools and the cache arena are CUDA VMM ranges
(core/vmm.hpp; the driver calls through the runtime's entry-point query, nothing links the driver library). The K/V
maps 2 MiB chunks for the cells the requests reach (16384 at start); when a request needs more, slots just below the
prompt path's loan give theirs up, a hotter expert in them first moving into the slot of the coldest one in the loan
region. A short request after a long one hands the chunks back and the slots refill from the profile. No address
moves, so no graph is captured again, and --max-context cells always fit.

- serve: grows at a request (its prompt, and a parked conversation before its restore) and in the decode rounds;
  trims once the outgoing session is parked
- generate: grows once for the prompt and max_new, before the loan
- one GPU with a profile, the whole K/V in VRAM (no --kv-resident), every expert in RAM (not the low-RAM tier);
  STRATA_KV_GROW_INIT / _STEP / _FLOOR for tests. Off by default: more slots move experts between GPU and CPU.
- tests/core/vmm_test: map, a chunk moved between ranges keeps its bytes, release

IQ2_XS, RTX 5090, 262K, fixed cache: first-token logits and 64 tokens bit-identical to 0.1.31 off, mapped whole,
grown into new VRAM, and grown by giving up 266 slots. Cache auto: 15,119-15,207 -> 17,300-17,503 slots, short chat
15.06-15.33 -> 14.71-14.83 ms a round; an emulated 16 GB card: 3,396 -> 5,719 slots, short chat 100 -> 115 tok/s,
a 32K prompt 7.44 -> 6.36 s and 24.6 -> 22.2 ms a round after it.
2026-10-05 19:57:35 +02:00
Niko1221 91cc8149b0 serve: the message-boundary checkpoint is the tail kind (evicted first, like --prompt-cache-tail's) 2026-10-05 19:57:35 +02:00
Moor Wu b5de344b73 Reuse long chat history when the last user message is edited 2026-10-05 19:57:35 +02:00
imanuandCodex 9c139846d5 GPT6-CACHE-Salva un checkpoint opzionale vicino alla fine del prompt
Co-Authored-By: Codex <[email protected]>
2026-10-05 19:57:35 +02:00
Niko1221 f71a128126 prefill: KV prefetch - return early while a prefetch is pending, read the env once (#732 review) 2026-10-05 19:57:34 +02:00
Moor Wu f1635c2174 Overlap streamed KV uploads with prefill computation 2026-10-05 19:57:34 +02:00
Benjamin LarssonandTrevin 458bcbff6f setup: --kv k8v4 streams with --kv-resident (the setup hunks of #705)
Setup no longer excludes K8V4 from KV streaming (KV_CELL_BYTES prices 816 B/cell), and docs/DETAILS.md says
it streams. This needs the engine of the PR #711 commit: MIN_ENGINE must be raised to 0.1.40 at the release,
or an older engine refuses the pair.

Co-authored-by: Trevin <[email protected]>
2026-10-05 19:57:34 +02:00
Trevin fea9ff5a4d kv: --kv k8v4 streams with --kv-resident
K8V4 (INT8 K + rotated Q4_0 V, 816 B/cell) was refused with --kv-resident. It now streams like
the other formats:

- kv_stream: a fourth block format, kKvHybrid (= 3, the number conversation snapshots already
  use for K8V4), moved as three runs per block - K codes, K scales, V q4_0 - by the same resolve,
  copy, ring-restore and stage paths; kv_block_bytes knows its size.
- kv_hybrid_k_half / kv_hybrid_v_half: each half of the host copy (and of the prompt path's
  staging pool) as the single-format append kernels see it, so a hybrid layer's host copy is
  written by the same folded calls that write its VRAM pools. Both fields of a half point at the
  same array: the kernels test the K-side pointer for presence and pick K or V per lane.
- The decode (layer.cpp), verify-window (verify.cpp) and prompt (prefill.cpp) appends pass those
  halves; the prompt path's staging pool gets a K8V4 layout; the host copy is allocated in it.
- Conversation snapshots read a streamed K8V4 state's host copy, as they do for the others.
- qsa_kv_format returns kKvHybrid instead of exiting; the two refusals are gone.

setup.py still keeps K8V4 unstreamed: enabling it there needs a MIN_ENGINE that has this.

Tests: kv_stream_parity runs K8V4 too (streamed vs resident attention, bitwise): 306 batches,
70,449 misses, 0 failures, ring restore identical; a negative control (the V half's K-side
pointer left null) fails it from the first batch. kv_hybrid_parity: all pass.
2026-10-05 19:57:34 +02:00
Niko1221 65fc896329 iq_kernels: the aligned shared declaration is HIP only; CUDA keeps alignas(16) (#778)
The attribute-after-declarator form from #778 does not compile with nvcc and MSVC as host (the CUDA build failed in mmvq_multi_kernel). The macro uses it under __HIPCC__ only, so CUDA is as it was.
2026-10-05 18:34:29 +02:00
Niko1221 1e3a9a4700 tests: gemm_f16_io_parity builds its BF16 weights by hand and refuses an all-zero reference (#835) 2026-10-05 18:34:29 +02:00
Niko1221andxjc10 01f21ea683 hip: FP16 prompt GEMMs are opt-in (STRATA_HIP_PROMPT_F16=1), CUDA-guarded, decided once per stage, non-finite count (#835 review)
Review changes on #835: off unless STRATA_HIP_PROMPT_F16=1 (a tip on gfx103x); act16/g_act_f16 and set_act_f16 are HIP only so CUDA kernels are unchanged; Prefill::init decides the mode once per stage on the stage's own device and bf16x2 takes it as an argument; widen_rows_f16 counts non-finite outputs under STRATA_DBG_NAN; new hip_gemm_f16_io_parity test.

Co-authored-by: xjc10 <[email protected]>
2026-10-05 18:34:29 +02:00
xjc10 5d83b431ba docs: AMD_HIP.md RDNA2 - the FP16 prompt GEMMs, their switch, the measured gain and the HIP ctest result
A bullet in the RDNA2 section: rocBLAS on gfx1030 only has tuned FP16-in/FP16-out kernels, what #835 changes,
STRATA_HIP_PROMPT_F16=0 for the old path, the measured prompt speeds on a second community machine (2x RX 6900 XT,
ROCm 10.0, engine 0.1.39), that the output is not bit-identical (link to the distribution check), and the ctest result
with -DSTRATA_BUILD_TESTS=ON: 60 of 65 pass with and without the change, the 2 skipped and 3 failing tests and why.
2026-10-05 18:34:17 +02:00
xjc10 99a3fe8df9 bench: where the FP16 prompt path's rounding moves, and what the distribution check does not show
Per-value rounding bounds of both paths (BF16-weight products: the BF16 activation rounding goes, an FP16 output
rounding comes; quantized-weight products: an FP16 output rounding comes), why the two are not compared by argument,
how the BF16X2 reference relates to them, and the open follow-up (an fp32 forward pass as reference).
2026-10-05 18:34:17 +02:00
xjc10 972cad1bc5 HIP gfx103x: the prompt path's 16-bit GEMMs in FP16 in and out (prompt reads ~2x on RX 6900 XT)
rocBLAS on gfx103x has tuned GEMMs for FP16 in / FP16 out only; FP16 in / FP32 out (Gemm::f16, Gemm::native) and BF16 in
(Gemm::bf16) fall back to generic kernels, 5.6 / ~5.3 TFLOPS against 37.7 on gfx1030 (N=10240, T=7313, K=2560).

On gfx103x only (prompt_f16(), per device; STRATA_HIP_PROMPT_F16=0/1 overrides):
- Gemm::f16 / native run FP16 out into the start of each FP32 row of Y (ldc = 2 ldy halves); widen_rows_f16 widens each
  row in place from its end - no extra buffer, no host sync
- Gemm::bf16 converts W in row slices to FP16 through the dequantization scratch and takes X as the FP16 image the
  gr_* / to_bf16 kernels now write (set_act_f16; saturated, a NaN stays a NaN); BF16X2 is off there
- Gemm::set_f16_io is an explicit per-instance switch that only Prefill::init turns on, so every other caller
  (gemm_bf16_parity) runs what it ran; CUDA builds are unchanged

2x RX 6900 XT, IQ3_S: one card 439 / 466 / 461 -> 744 / 915 / 926 tok/s at 9.4K / 34.7K / 105.8K tokens; layer split
444 / 707 -> 833 / 1,356 at 8.3K / 33.6K. Needle 15/15 on both paths; teacher-forced, the FP16 path is closer to a
higher-precision reference (BF16X2) than the old path. Details: bench/results/2026-10-04-rdna2-fp16-prompt/README.md.
2026-10-05 18:34:16 +02:00
Niko1221 771d5b73a7 hip: STRATA_HIP_ADAPT_KERNEL_COPY=1 copies adaptive swaps with a kernel instead of SDMA (#884, opt-in)
Workaround for the gfx1030 hang that needs MMQ prompt + adaptive swaps + SDMA together. Off by default; a source outside the registered arena, or not 16-byte aligned, takes the normal cudaMemcpyAsync. Untested on the reporter's 2x RX 6900 XT.
2026-10-05 18:33:39 +02:00
Niko1221andMagh97 b72ea678be sycl setup: write_run_script takes setup.py's open_browser argument; Arc Pro B60 also as PCI id e211 (#870)
Fixes 1 and 4 of #870, plus a test that every function setup_intel.py swaps into setup.py accepts all of the original's parameters. Fixes 2 and 3 are PR #784's session/native_dense ports.

Co-authored-by: Magh97 <[email protected]>
2026-10-05 18:33:39 +02:00
Niko1221andunimo-chan fa228b094d device (Windows HIP): one hipFree(0) warm-up and one hipMemGetInfo retry per process, before the DXGI budget (#873, #654)
Reworked from #873: no 8 x 250 ms retry at every failing call site, no #if in device_info (cudaMemGetInfo already maps to this function), no retry of real failures (only hipErrorInvalidValue, once), before/after lines in the log. The strata-device 5x retry loop is dropped.

Co-authored-by: unimo-chan <[email protected]>
2026-10-05 18:33:39 +02:00
Niko1221 e3336a13ba cmake: gfx1034 joins the unvalidated HIP archs (#778)
The native_dense split-shard arch guard from the same PR is #648's change and is left to it.
2026-10-05 18:33:39 +02:00
jollyroger1480 d81a57d5f4 iq_kernels: portable aligned shared-memory declaration for gfx1030
HIP clang for gfx1030 (ROCm 7.1) rejects 'alignas(16)' placed between
__shared__ and the declaration ('alignas attribute cannot be applied to
types'). Use __attribute__((aligned(16))) after the declarator instead —
same layout, compiles everywhere.
2026-10-05 18:33:30 +02:00
xxDoman 6b2572e45f hip/gfx906: cudaFuncSetAttribute as a function, not a macro
A function-like macro splits its arguments on the commas inside a template
argument list, so cudaFuncSetAttribute(kernel<1, 6, true>, ...) failed to
compile (fused_gr.cu:1089-1093, 'too many arguments provided to function-like
macro invocation'). Restore the template function 0.1.38's hip_compat/
cuda_runtime.h had, and add the two carveout definitions used beside it.

gfx906-only: include/strata/platform/hip_compat/ is on the include path only
inside if(STRATA_HIP_GFX906); CUDA and the wave32 RDNA backend are untouched.
2026-10-05 18:33:30 +02:00
bsorensen110 fd98b23849 tests: the tuning-header gate's test builds on Windows, and RDNA1/RDNA2 stays rejected (#695)
Two findings from the Copilot review on #695.

The five-character prefix match (replaced by the named sibling sets in the
parent commit) also grouped gfx1010 (RDNA1) with gfx1030 (RDNA2), so the gate
would have passed a table between two architectures that share no solution
IDs.  The test now pins that pair in both directions, and the third RDNA3
member (gfx1102) alongside the pair already covered.  Measured against the two
implementations with MSVC, same test binary source: the prefix-match gate
fails exactly these two cases (13 cases, 2 failures), the sibling-set gate
passes all 13.

<unistd.h>/getpid() and a hard-coded /tmp broke the test on Windows/MSVC with
STRATA_BUILD_TESTS=1.  It now uses std::filesystem::temp_directory_path with a
steady-clock token and a per-case file name, the shape
tests/core/gguf_split_test.cpp already uses, and it reports a scratch-file
write failure instead of letting it read as a gate rejection and pass for the
wrong reason.

Built and run with MSVC 14.4x (/std:c++20, /W4): 13 cases, 0 failures.
2026-10-05 18:33:30 +02:00
Soren-ldandCopilot Autofix powered by AI 12d6af092b Refactor architecture check for tuning compatibility
Co-authored-by: Copilot Autofix powered by AI <[email protected]>
2026-10-05 18:33:30 +02:00
bsorensen110 2748e1471c hip: the tuning table's arch gate accepts the RDNA4 sibling (gfx1200 <-> gfx1201)
The header gate compared the table's architecture to the runtime's with
`arch != expected_arch`.  A card that reports its RDNA4 sibling - a Radeon
AI PRO R9700 (gfx1201) under HSA_OVERRIDE_GFX_VERSION=12.0.0 reports
gfx1200 - rejected its own calibration at startup and lost the tuned
prefill route entirely:

  prefill gemm: tuning architecture mismatch: file=gfx1201 runtime=gfx1200; using hipBLASEx

Solution IDs are scoped to the architecture FAMILY (gfx1200/gfx1201 share
them, as gfx1100/gfx1101/gfx1102 do), and a solution ID that does not exist
on the running card already fails the per-solution gate in prefill/gemm.cu
(getAlgosFromIndex / matmulIsAlgoSupported) and falls back to hipBLASEx per
call.  The header gate can therefore be family-wide: the version gate stays
strict, cross-family pairs stay rejected, and the per-row gate decides which
rows actually run.

Adds tests/prefill/tuning_header_arch_family_test.cpp (synthetic tables, no
model, no GPU): 10 cases, RED on the strict gate (3 sibling failures),
GREEN on the family gate.
2026-10-05 18:33:30 +02:00
lawsirlawsir-png 534d07f5da tools/hip: add gfx1100 hipBLASLt tuning table for hipBLASLt 1.2.2 (100202)
Calibrated with tools/hip/tune_hipblaslt.cpp on a Radeon PRO W7800 (gfx1100) under ROCm 7.2.4. setup.py:hipblaslt_table() resolves tools/hip/<arch>-hipblaslt-<version>.txt, and no 100202 table exists for gfx1100, so prefill silently falls back to plain hipBLAS. Requested in #505.
2026-10-05 18:33:30 +02:00
Zac Chen d74e110f0e hip: hipBLASLt tuning table for gfx1100 (RX 7900 XTX) with ROCM 10.2.0a nightly (hipBLASLt 1.5.0)
tools/hip/gfx1100-hipblaslt-100500.txt: the same 16 dense GEMM geometries as
the other gfx1100 tables (bf16 and f16, T=4096 and T=8192, 32 rows), calibrated
on an RX 7900 XTX with tune_hipblaslt (relative L2 <= 1e-4, max abs <= 1e-2, no
padding writes) against a ROCm 10.2.0a20261003 nightly SDK
(libamdhip64.so.7.17.26392), hipBLASLt 1.5.0 (version number 100500).

The engine refuses a table of another hipBLASLt version, so on this ROCm the
shipped gfx1100 tables (100100, 100200) are refused and prefill runs on plain
hipBLAS - the same gap the gfx1201 1.5.0 table (#481) closed for the R9700.
setup picks this one by name once the installed hipBLASLt reports 100500.

Measured on a 0.1.38-lineage nightly build of this tree, Qwen3.8-Flash-Next
IQ3_S greedy, arms differing only in STRATA_HIPBLASLT_TUNING (median of 3
clean runs each):

| prompt tokens | with table | plain hipBLAS | gain |
| 131071        | 1687 tok/s | 926 tok/s     | 1.82x |

decode unchanged (62.3 vs 60.7 tok/s, overlapping ranges). Per-run data,
the calibration command and the tuner log: #745
(bench/results/2026-10-04-community-rx7900xtx-hipblaslt-100500/).

docs/AMD_HIP.md: the shipped-tables list carries the entry.
2026-10-05 18:33:30 +02:00
miskahm 7984688e3c hip: ship a gfx1100 tuning table for hipBLASLt 100401 (ROCm 10.0.0)
ROCm 10.0.0's hipBLASLt is 1.4.1, whose solution ids are version 100401, and no
gfx1100 table shipped for it - so on an RX 7900 XTX with a packaged ROCm 10 the
prompt path's dense GEMMs fell back to plain hipBLAS. This is the same version
mismatch PR #745 measured on ROCm 10.2 (their 100500), reproduced on 10.0.0.

tools/hip/gfx1100-hipblaslt-100401.txt: calibrated with tools/hip/tune_hipblaslt
over the 26 dense geometries of the shipped gfx1100 table, on a Ryzen 7 9800X3D.
Swift 1.5 IQ3_XXS, 262144 ctx, --kv int8, fresh prompts (a nonce per request, or
the server's prompt checkpoint reads nothing), medians of 3:

  1.7K tokens   682 -> 1,038 tok/s  (+52%)
  6.5K tokens   857 -> 1,524 tok/s  (+78%)

Decode unchanged (79.0 vs 80.3 tok/s, inside this machine's 2.3% run drift), and
hip_prefill_hipblaslt_gemm with STRATA_HIPBLASLT_VERBOSE=1 reports fallbacks=0.
2026-10-05 18:33:29 +02:00
Niko1221 ce37456ccd prefill: dense MMQ is HIP only; no q6_k instance or untested code in CUDA builds (#820)
Gemm::native_mmq, its call, the f16_to_f32/set_bounds kernels and the Q6_K case are under __HIPCC__; the CUDA type list loses q6_k. The K % 256 != 0 out-of-bounds read is already refused in native_mmq (kept). Still opt-in: STRATA_DENSE_MMQ=1 on a STRATA_MMQ_KQUANTS=ON HIP build.
2026-10-05 18:33:29 +02:00
xendak 43881f14a3 prefill: the dense GGUF projections through MMQ on HIP (opt-in STRATA_DENSE_MMQ=1), the K-quant instances on the HIP build
The prompt path's dense projections (attention qkv/gate, the PLE key, the shared
experts, the router, the head) run dequantize-to-FP16 + cuBLAS: on the RDNA2
cards that is 68% of the prefill GPU time at ~25% of the card's fp32-accumulate
rate, while the same shapes through llama.cpp's int8 MMQ run ~7x faster
(measured with bench_dense_gemm).  This wires Gemm::native's beta = 0 products
through the MMQ path the MoE experts already take: quantize the FP16 activations
to q8_1 (they are converted to FP32 in the dequant scratch, which the old path
only fills afterwards) and run one mul_mat_q product per 1024-row chunk.  It is
opt-in (STRATA_DENSE_MMQ=1; unset, nothing changes) and falls back silently to
the old path on any shape MMQ does not take.

- STRATA_MMQ_KQUANTS also compiles the Q4_K / Q5_K / Q5_1 / Q6_K instances on
  the HIP branch (the mixed-quant packs' dense tensors are K-quants: this
  model's attention/PLE matrices are IQ4_XS/IQ3_S/Q4_K/Q5_K/Q6_K), adds Q6_K to
  the CUDA list, and defines STRATA_MMQ_KQUANTS for the HIP target.
- Gemm::native_mmq: one MMQ context, xq/ident/bounds buffer (~9.5 MB) and a
  failed-flag per Gemm; fp16->fp32 and the {0, rows} bounds helper join
  moe_mmq's kernel set.
- K must be a full 256-value multiple.  MMQ loads the weights in 256-value K
  chunks, and past a partial row the chunk reads bytes past the row - the next
  row's, or for the last weight row bytes past the tensor, which no caller
  guarantees to be zeros (the MoE path is safe because its gather buffers end
  in a zeroed tail).  On this pack the 640-value IQ4_NL shared-expert down
  picked up a NaN scale from the next tensor's blocks and zeroed the output;
  the odd shapes stay on the dequantize + cuBLAS path.

Measured on the RX 6900 XT (gfx1030), swift-1.5-iq3_xxs, WORKERS=15 ADAPT=1
PREFILL=8192, bench_prefill.py fresh medians: 4K prefill 436.1 -> 757.2 tok/s,
8K fresh 447.7 -> 773.9 tok/s, 8K followup 163.9 -> 206.1 tok/s; decode within
noise (40.6 vs 40.3 tok/s at --pcie-frac 0, 52.7 vs 49.9 at the 0.55 default).
Greedy output: bit-identical for the first 200 tokens of a short prompt; a 4K
prompt's greedy text diverges at token ~26 (the q8_1 activation rounding) and
stays coherent.
2026-10-05 18:33:29 +02:00
Niko1221 0f1390bd11 hip: fence after the doorbell ring store so the host sees it (#697)
HIP only (__HIPCC__); the CUDA kernels are unchanged. A volatile store to mapped host memory can stay in L2 on RDNA4 until the stream syncs; __threadfence_system() after the store flushes it. Reporter's one-line diagnostic fix, applied to doorbell_ring/publish/publish_res (publish_value already had it). For #579 and #613 to test.
2026-10-05 18:33:19 +02:00
pugolini 7fd67ddd6d verify: the shared-expert stream fork starts off on HIP (#816)
The fork overlaps the shared expert with the routed experts on a second stream.
It pays off on CUDA, where a36be1d added it, but on HIP the cross-stream event
dependency costs more than the shared expert takes to run, so there the fork
starts off and STRATA_SH_STREAM=1 turns it back on. CUDA's default is unchanged.

Same shape as mtp.cpp's STRATA_MTP_PREFILL_SYNC (#382): the env var always wins,
only the default differs per backend. The profiler takes the fork off to time the
stages, so a profiled run never showed this - which is why STRATA_SH_STREAM=0 was
the only way to see it.

Radeon AI PRO R9700 (gfx1201, ROCm 6.4.3), Qwen3.8-Flash-Next IQ3_XXS, --kv int8
--kv-resident 32768 --resident-experts, temp 0, 3 runs each, same binary and
prompt, 48,067-token prompt so the KV stream path runs:

  stock 0.1.39 (fork on, default)   43.40 t/s   (38.90 - 44.60)
  this change (fork off, default)   62.90 t/s   (55.40 - 63.80)
  STRATA_SH_STREAM=1                43.50 t/s   (39.10 - 45.20)
  STRATA_SH_STREAM=0                63.00 t/s   (55.90 - 64.10)

The fork is a scheduling change only: the same four prompts answer byte for byte
the same with it on and off. #816 reports the same fall on a gfx1030 (RX 6800,
Windows, 42.5 -> 56.4 t/s), and a comment on #646 sees it on a gfx1100.

Short prompts are barely affected here (65.4 against 62.7 t/s at 256 tokens), so
this only shows once the KV stream path is in play.
2026-10-05 18:33:19 +02:00
Niko1221 a2ef9db5cd docs: STRIX_HALO.md - IQ3_XXS 8K re-measured (6 pairs): prompt equal 2026-10-05 17:24:55 +02:00
Niko1221 2e0f28e7d7 Merge branch 'f-140' into f-140-j 2026-10-05 16:36:52 +02:00
Niko1221 254415eac4 docs: STRIX_HALO.md - build from source, the defaults, the opt-in switches, the measured table 2026-10-05 16:36:48 +02:00
Niko1221 0d5b12b82e serve: the image encoder's file arguments are absolute (it now runs in its own directory, #480) 2026-10-05 15:58:38 +02:00
Niko1221 4f41b51c6b setup: the 200K step is never the recommended context (#608)
ram_ctx() skips 204800: where 200K fits and 256K does not, the rule stays at 128K. A test covers every RAM size.
2026-10-05 15:58:38 +02:00
Niko1221 3982b2250a docs: TROUBLESHOOTING for Smart App Control, the port error, the engine rollback, Linux read-ahead and THP (#735 #769 #670 #699 #771) 2026-10-05 15:58:38 +02:00
Niko1221 74d566bd1c generate: warn when the profile caps an explicit --expert-cache; retry a split stage's cache at 90% (#831 #841)
- #831: an explicit --expert-cache above the profile's length was cut to it with no word on the native-pack path; a
  warning now says so (the allocation is unchanged).
- #841: a layer-split stage whose expert cache fails to open (out of memory with the VRAM free) is retried with 90% of
  the slots, up to 3 times, each try logged. Nothing changes when the first open works.
2026-10-05 15:58:38 +02:00
Niko1221 bb19becc24 pinned arena: no VirtualLock on large pages, no MADV_HUGEPAGE under THP always (#779 #771)
- #779: a large-page arena cannot be paged out, so the three working-set lock sites skip it (the note says so).
- #771: with /sys/kernel/mm/transparent_hugepage/enabled = [always] the kernel already backs the arena with huge
  pages where it can; the extra MADV_HUGEPAGE only added direct reclaim and compaction on every fault (a 25 s start
  took 432 s on a fragmented machine). It is not requested there, and STRATA_NO_ARENA_THP=1 skips it anywhere.
2026-10-05 15:58:38 +02:00
Niko1221andjordicor f60ae8d0a4 Windows mapped-page release: the switch is STRATA_FILE_RELEASE (#818)
STRATA_ARENA_RELEASE already exists and means the opposite (=0 keeps the experts a GPU holds, generate.cpp), so the
opt-in is renamed in the engine, the tests, DETAILS.md and the bench folder. The working-set assert in
file_expert_source_test reports, not fails, a trimmed page that Windows maps straight back with the switch on. The
bench folder keeps the README and the two scripts (the raw export and the harness self-tests are dropped).

Co-authored-by: jordicor <[email protected]>
2026-10-05 15:58:38 +02:00
jordicor 441a8f792a Add opt-in Windows mapped expert page release 2026-10-05 15:58:38 +02:00
sergiywith ebe1db1a0b Start: register the expert arena per layer on a thread ahead of the readers (Windows; part 2 of #285)
0.1.31 registers the whole arena with CUDA before the first byte is read. With 4 KB pages, which every account
without the Lock Pages privilege gets, that registration is ~1.5 s of the start that nothing hides (IQ2_XS:
33 GiB).

- PinnedArena(bytes, bounds, Deferred) reserves only. register_slices() registers the per-layer slices in order on
  a thread and publishes how many are done.
- Every reader (load_experts_gguf buffered and unbuffered, load_experts_ranges, load_experts_direct) writes layer L
  only once slices L and L+1 are registered. Writing a page while cudaHostRegister runs on it corrupted the arena
  under WDDM, even on large pages. A layer boundary inside a page puts that page in both registrations, so the next
  slice must be done too (page-aligned in effect, without moving the slice bounds an expert must not straddle).
- A slice CUDA refuses ends the registration, and the rest is locked resident as in the sliced fallback.
- Only where the whole arena would be registered anyway: no cap (STRATA_ARENA_PIN_GIB, multi-GPU under WDDM) and no
  shared arena file. Linux keeps #253's whole-arena pin. STRATA_DEFERRED_REGISTER=0 restores the old order.
- STRATA_VERIFY_ARENA=1 prints the loaded arena's FNV-1a checksum (tests).

Checksums: the same with the old order and the deferred one, 4 KB and large pages, the GGUF in place and experts.bin
(IQ2_XS; 13 deferred loads).

IQ2_XS without experts.bin, RTX 5090, 128 GB. Each cell is the time until the arena is loaded / until a 16-token
answer is done. Cold: the files pushed out of the file cache first. Two to three runs each:

| | cold | restart |
|---|---|---|
| 4 KB pages (STRATA_NO_LARGEPAGES=1), 0.1.31 | 12.4-13.0 / 16.3-16.8 s | 7.8-8.5 / 10.9-11.7 s |
| 4 KB pages, parts 1+2 | 11.1-11.5 / 14.9-15.4 s | 5.5-5.7 / 8.5-8.9 s |
| large pages, 0.1.31 | 17.6-18.7 / 21.1-22.4 s | 4.6-5.1 / 7.0-7.6 s |
| large pages, parts 1+2 | 17.6-19.9 / 21.3-23.6 s | 4.1-4.5 / 6.7-7.1 s |

The cold spread with large pages comes from the drive: the same build gave 17.6 and 19.9 s with the deferred
registration and the cache probe switched off as well.
2026-10-05 15:58:38 +02:00
Pc1 066094c81f file tier: STRATA_UNBUFFERED_LOAD=1 is not honoured without a RAM budget
Review of #773 (2 x RTX 5060 Ti 16 GB, 32 GB RAM, --mmap-experts, no --resident-budget-gib): with no RAM copy the
file cache is the expert tier, and forcing unbuffered reads there re-read 163-629 GB from the drive and halved the
speed. The automatic decision never armed in that mode; now the forced one does not either, and the log says so.
2026-10-05 15:58:38 +02:00
Pc1 2d929a28b3 file tier: unbuffered reads on Linux too (O_DIRECT + the kernel's asynchronous reads)
through the page cache, as page faults of the mapped GGUF - small requests on the critical path, and pages that a
32 GB PC cannot keep beside a 16 GiB RAM copy anyway, so they were read again and again.
- open_direct opens the files with O_DIRECT; read_direct queues every 4 KiB-aligned window of a batch with one
  io_submit (linux/aio_abi.h, no library; allowed by Docker's default seccomp profile, unlike io_uring) and waits for
  all of them, each thread with its own context and aligned buffer.  Windows merges windows up to 1 MiB apart for
  NTFS; here only windows that touch are merged.  A refused submit or a failed read falls back to the mapping, as on
  Windows.
- The same decision as Windows' `cache_counts = false` arm: unbuffered when the file cache could not keep the expert
  bytes outside the RAM copy (platform::file_cache_keeps, from MemAvailable and the tightest cgroup limit), re-checked
  once the copy is built (#577).  STRATA_UNBUFFERED_LOAD=1 / 0 forces it either way.  A pack's experts.bin works too
  (its Linux open now records the path).
- direct_fallbacks() counts the blobs an unbuffered read could not deliver.
- Tests: file_expert_source_test reads a synthetic pack unbuffered - one blob at a time, a batch of adjacent experts,
  copy_blob - and requires the file's bytes and no fallback (a mutation that fails every read is caught).  ctest: 63
  of 66 pass; the three others need a Q2_0 pack, a higher `ulimit -l` and AVX-512, as before.
Measured on an RTX 3080 Ti Laptop 16 GB, i9-12900H, 30 GB RAM, KIOXIA PCIe 4 NVMe, Linux 7.0, ext4: Qwen3.8-Flash-Next
Abliterated Q4_K_M in place (--native), --resident-budget-gib 16, --expert-cache auto, 8K context, greedy; three
prompts (code, Russian prose, a 7.8K-token summary), each with and without MTP, two rounds of buffered
(STRATA_UNBUFFERED_LOAD=0) against unbuffered, the same binary.  Decode 4.38 -> 7.90 tok/s on average (x1.80;
x1.49-x2.01 per prompt and mode), the 7.8K prompt read at 154/172 -> 184/290 tok/s, startup 36 -> 18-21 s.  The
code prompt's answer is the same text in all 16 runs; the other two differ between runs of either arm (the
adaptive GPU cache changes which experts the CPU computes), as before.
2026-10-05 15:58:38 +02:00
Jesus Gulfo 760c775434 Linux: read ahead of the head, embedding and vision encoder loads; time them
The native output head (437 MB) and the token embedding (260 MiB) were copied
out of the mapped GGUF one page fault at a time, and strata-vision's projector
(907 MB) went through llama.cpp's sequential reads, all before the server was
ready. Each now asks for its bytes ahead (the vision encoder honours
STRATA_READ_AHEAD=0 too), and the head, the embedding, the projector load, the
encoder warm-up and the pinned cache complement log their times.
2026-10-05 15:58:38 +02:00
Jesus Gulfo cb33c0bac5 Linux: read ahead of the mapped loads at start
On Linux every load phase read its files one page fault (one 128 KB readahead)
at a time, so a cold start spent most of its time waiting on the drive at
queue depth ~2. A new platform::advise_willneed asks the kernel for a range in
128 KiB steps (WILLNEED reads at most one readahead window per call), and is
called before the weight load, the native dense upload, the profile fill (256
pairs ahead, STRATA_FILL_AHEAD), the peer tier's fill (--peer-device), the
resident RAM copy and the MTP draft files. STRATA_READ_AHEAD=0 turns it off.
Each phase logs its time.

Gen3 NVMe, RTX 5090, 32 GB, Q2_0 resident at 262K: ready in 70 s, was ~920 s.
2026-10-05 15:58:38 +02:00
Maxim Shipko 2de94bca5e Respect Windows commit capacity 2026-10-05 15:58:37 +02:00
Niko1221 fb57b6cd81 setup: quick fixes and OS items (#657 #737 #670 #798 #775 #779 #841 #881)
- #657: check_shards refuses a one-tensor shard that is longer than its tensor (the PLE table), naming both sizes.
- #737: UD-Q4_K_XL on several GPUs below the 135 GB estimate is a warning and a question (explicit --gpus --yes is
  the consent), not a refusal.
- #670: an engine update keeps the engine it replaces in engine/.previous (one generation, all-or-nothing swap);
  setup.py --rollback-engine puts it back.
- #798: setup counts P/E cores from the hybrid PMU lists, else by 90% of the largest capacity (tests on a fake /sys).
- #775: a learned --expert-profile / --expert-profile-save added by hand survives a setup run.
- #779: a tip (--vram-reserve-mib 1500) when nvidia-smi says the card drives a display.
- #841: typed-ahead keys are flushed before a setup question on a terminal.
- #881: read_text() with encoding utf-8 in setup.py and package_windows.py; the CPU image encoder build finds vcvars on
  Windows; PCI 73DF (RX 6700 XT / 6800M) is gfx1031 with a self-build hint.
2026-10-05 15:58:36 +02:00
Niko1221 bea064293f ngram: the PLE size-mismatch message says a longer-than-its-table file is a bad download (#657) 2026-10-05 15:58:36 +02:00
Niko1221 7b80403c7b cpu pool: tell P-cores from E-cores by the hybrid PMU lists or 90% of the largest capacity (#798)
Linux counted a core as a P-core only when cpu_capacity equalled the maximum. Turbo Boost Max 3.0 gives the "favored"
P-cores a slightly higher capacity, so a Core Ultra 7 270K Plus (8P + 16E) counted 2 P-cores and started one pool
worker. Intel's /sys/devices/cpu_core/cpus and cpu_atom/cpus decide when they exist; without them an E-core is one
under 90% of the largest capacity. A CPU whose capacities differ by a few percent is no longer hybrid.
2026-10-05 15:58:36 +02:00
Niko1221andziphao2010 2ab6304867 serve: vision temp files, Smart App Control message, engine start line, real listen error (#480 #874 #735 #779 #769)
- #480: the image encoder's directory has no space in its path (a TEMP under a user name with a space broke every
  image: the ENC and GENI lines are split at spaces); the ENC line names its files relative to the encoder's cwd.
- #874: the per-request combined embeddings file goes to /dev/shm on Linux when it has room, and is opened
  short-lived (FILE_ATTRIBUTE_TEMPORARY) on Windows, instead of heavy steady disk writes.
- #735: WinError 4551 / 1260 on starting the engine or the encoder says Windows blocked the (unsigned) program
  (Smart App Control) and what to do. A message only; no signing.
- #779: a timestamped "engine started: exe args" line in the engine log.
- #769: only EADDRINUSE says "already in use"; any other listen error says what the OS said.

Co-authored-by: ziphao2010 <[email protected]>
2026-10-05 15:58:36 +02:00
Niko1221 8fb2dd34cb winjob: opt the server and its children out of Windows power throttling (#691)
A minimized console window made Windows 11 treat the server and its children as background work and move their
threads to the E-cores (about 20% slower on a hybrid CPU). contain() now sets ProcessPowerThrottling off for each
child, and the server does it for itself. Windows 10 knows only the EXECUTION_SPEED bit, so it is tried on its own
when both bits are refused.

Reported with the patch by mozophe.
2026-10-05 15:58:36 +02:00
Niko1221andWesleyvdk 035da7f8c1 iq_pack: IQ3_XXS and IQ4_XS PLE keys stay native (#381)
NativeDense serves these formats; the packer sent them down the BF16 path, so such packs failed at start.

Co-authored-by: Wesleyvdk <[email protected]>
2026-10-05 15:58:35 +02:00
Niko1221andLéon Avic Simmons 4f4c923cb2 strata_pack build --force; setup passes it when it rebuilds a pack (#634)
Co-authored-by: Léon Avic Simmons <[email protected]>
2026-10-05 15:58:35 +02:00
Léon Avic Simmons f0b083efed strata_pack.py build: refuse an output directory that already holds a pack, naming the file
build did mkdir(exist_ok=True) and then wrote experts.bin, dense.bin,
embd.bin and manifest.json over whatever pack the directory held, with
nothing said. Now a manifest.json, index.txt or native_experts.txt in
--out (the files the engine reads a pack's identity from) stops build
before it opens the GGUF or writes a byte, naming the directory and the
file; it returns 1 like the other refusals. An empty or absent directory
proceeds as before, and so does experts.bin without manifest.json: build
writes the manifest last, so that is a build that did not finish, not a
pack.

tools/test_shards.py: each marker is refused with the path and the file
named and nothing in the directory touched; an empty and an absent
directory go on into the build; an unfinished build is rebuilt.
2026-10-05 15:58:35 +02:00
Niko1221andChristoph Wübbels 62aaea4e14 setup: the shared Chat settings file is no model config (#346)
model_config_files() leaves strata-<model>.shared-settings.json out in the start menu, previous_config, the
data-folder move and strata_mcp.configs().

Co-authored-by: Christoph Wübbels <[email protected]>
2026-10-05 15:58:35 +02:00
MingShi_ 621f84e62d setup: a 200K context choice between the 128K rule and 256K (#406)
#406 lets a PC below setup's 256K estimate keep 256K with a note, but the list
jumped 128K -> 256K: there was no step between the 128K rule and a size that PC
may not hold.  204800 is inside the trained 262144, so it needs no rope scaling,
and with IQ3_S it costs ~2.8 GB of 8-bit KV against ~3.6 GB at 256K.

Measured on an RTX 4080 16 GB, Ryzen 9 9950X3D, 62 GB usable, IQ3_XXS, engine
0.1.37, 8-bit KV (setup turns KV streaming off at this size, so the KV stays in
VRAM and the expert cache loses the difference):

  context            128K      200K
  decode, shallow     94.1     85.1 t/s   (median of 10, warm)
  decode, 30K deep    87.2     85.3 t/s
  decode, 124K deep   94.1     79.2 t/s
  prefill          3257-3284  3189-3318 t/s
  experts cached      3661     2951       (-710)
  a 203259-token prompt: 3093 t/s prefill, 68.6 t/s decode

tools/strata_mcp.py's fallback list mirrors CONTEXTS and moves with it, so a PC
that cannot import setup sees the same menu.  tools/test_setup_risk.py picks 256K
at "6" now and a new test covers a 200K pick: kept, no rope scaling, and the RAM
note a 64 GB PC gets.
2026-10-05 15:58:35 +02:00
Akido 5f6b7c2766 setup: a download stopped before its rename is finished without asking for more
A .part holding every byte (setup stopped between the last byte and the
rename) was resumed with a range past its end; the server answers 416,
which download() retried 30 times 10 s apart before renaming the file.
2026-10-05 15:58:35 +02:00
Sunil Srivatsa 645cbb66e4 fix(setup): drop an engine archive that fails to unpack
get_prebuilt kept an engine archive that is not a zip (or a damaged one)
together with its .done mark. download() trusts the mark, so every later run
failed on the same file, even after the right archive was published; on the
update path the engine was never updated. The archive and its mark are now
dropped before the error propagates, as 0.1.32 does for a refused archive.
2026-10-05 15:58:35 +02:00
Niko1221 d215016f40 Merge f-140 (3f8f333e) into the Aurora port 2026-10-05 14:54:29 +02:00
Niko1221 a751fbb128 gfx906 compat build: s26_tsum.cuh takes its AMD branch only on gfx10+ (permlanex16 / DPP), device_free_bytes skips the integrated-GPU attribute the compat layer lacks
Compile check on ROCm 7.14.1 (Aurora): f-140 itself stops in fused_gr.cu (12 errors) and mtp.cpp (2) for gfx906 there; with this
commit the port adds no error of its own (fused_gr.cu's pre-existing ones remain).
2026-10-05 13:59:39 +02:00
Niko1221 92dc6f1de8 tests: hip_gdn_rec_head compares y16 and the state (the FP32 scratch y is no longer stored by the base norm kernel) 2026-10-05 13:27:10 +02:00
Niko1221 0fff115dfd Merge branch 'f-140' into f-140-j 2026-10-05 13:24:21 +02:00
Niko1221 3f8f333e92 docs: DETAILS lines for --adapt-async limits and STRATA_EXCHANGE_ROTATE 2026-10-05 13:19:39 +02:00
Niko1221 d90028c755 tests: batch_test --dump (an A/B of two engine builds) and the parallel-above-8 message with --batch-mtp 2026-10-05 13:19:39 +02:00
Benjamin Larsson 3216d27118 serve: --mtp is optional
Without --mtp, serve drafts with the suffix/prompt-lookup drafter only (or one
token per round), as the plain generate command already does; every token is
still verified against the model. The draft layer's VRAM goes to the expert
cache instead (1,000 -> 1,678 slots in an A/B on an RTX 2060 SUPER 8 GB). The
bind, the prompt path's draft prefill (batched and per token), the draft-KV
restore, prompt length, max drafts, draft sampling, coupled-draft history and
the draft call itself are skipped without it. The conversation cache is off
without it: a parked conversation always carries the draft layer's K/V.
2026-10-05 13:19:39 +02:00
Niko1221andMoor Wu 028cb234a4 disjoint helper cache (#731) on top of #854: a live RemoteExperts::holds() check in the serve, async and CLI adapt loops, STRATA_DISJOINT_ADAPT off by default, no static mask
Co-authored-by: Moor Wu <[email protected]>
2026-10-05 13:19:39 +02:00
Niko1221andFrancesco Albano 2bfac510d7 pipeline-windows (#859): every STRATA_PIPELINE_* debug hook behind STRATA_PIPELINE_DEBUG=1, exclusive with --adapt-async, merged with batch MTP and the async tier
Co-authored-by: Francesco Albano <[email protected]>
2026-10-05 13:19:39 +02:00
Francesco Albano bfa532e97c Layer split: pipelined verify windows (--pipeline-windows, opt-in)
With a split the two cards take turns on every verify window: stage 0 runs its
layers and hands off, then waits while stage 1 runs the rest, the head and the
draft. The batch pipeline (--batch-groups) overlaps different conversations;
one conversation's decode stayed serial.

--pipeline-windows 2 runs window K+1 on stage 0 while stage 1 verifies window
K. The guess for K+1 comes from the MTP chain, launched as soon as K's verdict
is known and teacher-forced through K+1's drafts: if K is accepted as guessed,
K+1 is already half done; if not, stage 0 restores its GDN state from a
snapshot, replays K's commit with the real count, and K+1 is rebuilt from the
real tokens. --pipeline-windows 1 overlaps only the prompt's short reads
through the windows (--short-read). Default 0: nothing changes.

What it adds:
- Verifier: a non-blocking form of run()/commit() (capture_all, pl_launch,
  prestage, service, done, pl_finish, pl_commit_async), the batch pipeline's
  batch_launch/batch_poll pattern for one sequence. The window, its staging,
  the PLE gather at layer 0, the zero-doorbell graph's flag, the #267 release
  on a timeout and the commit graph are run()'s. Two verifiers per stage (one
  per window parity) share one stream of their card (set_stream) and each
  pair has its own hand-off.
- set_always_publish: the pipelined verifiers copy the token rows to the host
  at every layer, because the adaptive tier now marks experts evicted on the
  host while windows are in flight, before the device residency table follows.
- MtpDrafter: chain_launch (the round and its steps back to back, an event
  after each output, the first steps fed the in-flight window's drafts by a
  one-thread forcing kernel), prefill without a host sync, 8 rows with 2, the
  drafter's stream at the highest priority (CUDA only).
- the adaptive tier beside the pipeline: evictions are marked first, then the
  refill copies wait (on the tier's thread) for every window in flight; the
  RAM side of a swap (exchange, release, unpin) waits for the windows that may
  still pull the incoming experts over PCIe.
- an agreement gate: a speculative window is launched only when the chain's
  estimate says it is likely kept (STRATA_PIPELINE_THETA, default 0.2); a
  window known to be wrong gets empty plans for its remaining layers; a lookup
  window's continuation becomes the speculative window behind it.
- VRAM: 160 MiB more kept out of each card's cache for the second verifier,
  and the two GDN snapshots on the first card.
- off, with one log line saying why, for anything it does not support: --batch
  slots, --peer-device, the helper caches, three or more stages, a split on
  one GPU, no drafter. Requests with repetition penalties or coupled draft
  sampling decode serially. Nothing is in flight outside the two pipelined
  loops, so checkpoints, parking and the serial paths are unchanged.

Measured: Swift 1.5 IQ3_XXS at 160K (q4_0 KV, the stock draft layer), RTX 4060
Ti (layers 0-19) + RTX 5080 (20-47), i9-14900KF, 32 GB of RAM, the resident
RAM mode on the split (#848); greedy, 500 tokens, two interleaved pairs of
three rounds, decode tok/s, serial -> pipelined: Python code 90.4 -> 103.1,
C code 76.6 -> 81.6, English prose 63.0 -> 71.1, Italian prose 41.6 -> 46.3,
a copy-heavy edit (a 5 KB file back with a rename) 90.5 -> 117.9; mean
72.4 -> 84.0 (+16%). With the experts read through the OS file cache
(--mmap-experts on that PC) the file reads dominate and it measured no faster.

Correctness: with STRATA_IQ_MT_MIN=1 --pcie-frac 0 --adapt-every 0 the
greedy text is identical to the serial loop's when both have the same expert
caches (4/4 prompts, 500 tokens; the serial run given the pipeline's reserve
through --vram-reserve-mib). The same text with STRATA_PIPELINE_THETA=2 (no
speculative window), STRATA_PIPELINE_FORCE_MISS=1 (every speculative window
wrong, rolled back) with and without DOOM_SKIP, PRESTAGE=0 and
SNAP_OVERLAP=0. Against a run without the flag a few experts move from a card
to the CPU (the pipeline's VRAM), which rounds them differently, so a near-tie
can flip. With the adaptive tier on: 22 varied requests at temperature 0.6
(thinking on and off, five languages, code), and 11 more with
STRATA_PIPELINE_FORCE_MISS=2, gave no degenerate answer, no stall.

The watchdog prints the pipelined loop's state when it fires;
STRATA_PIPELINE_TRACE=<file> writes a timeline.
2026-10-05 13:19:38 +02:00
Francesco Albano 8ba583fec2 MTP drafter: a teacher-forced chain as one asynchronous launch
For the pipelined verify windows of a layer split (next commit). Nothing calls
it yet, and nothing changes unless set_force_capture is set before load.

- chain_launch: the catch-up round and its steps launched back to back with no
  host wait, an event after each output (chain_outputs_ready reads them as they
  land, chain_poll once the chain is done). Its first steps can take given
  tokens as their input instead of their own picks (teacher forcing: the drafts
  of a window already in flight), through a one-thread kernel at the end of the
  round and step graphs that does nothing while its mapped token is -1; the
  outputs stay the chain's own picks.
- prepare_chain / prepare_prefill: capture every graph and size the prefill's
  input records up front, so neither a later chain nor prefill(sync = false)
  waits for the device.
- set_source_R: the rows a round reads, copied into the bound buffer first (the
  pipelined loop has one stage-1 verifier per window parity).
- with forcing on, the drafter's stream gets the highest priority (CUDA;
  STRATA_MTP_PRIORITY=0: the default).

draft() resets the forced tokens and copies the source rows the same way, so
the serial loop's drafts are unchanged.
2026-10-05 13:19:38 +02:00
David d91f2960f7 bench: document the standard Q2 token-identical rotation pair 2026-10-05 13:19:38 +02:00
David 82db014cc1 bench: verify rotation-only build with an exact native 1K pair 2026-10-05 13:19:38 +02:00
Niko1221andDavid e57c907221 exchange rotation (#864) on top of the async tier (#876): rotation-aware commit_flip/commit_copies/resident_blob, test only under STRATA_BUILD_TESTS, drop a banner edit
Co-authored-by: David <[email protected]>
2026-10-05 13:19:38 +02:00
David 1e6cec80d1 perf: opt in to resident expert exchange buffer rotation
Extract rotation and atomic ownership publication from aa6e0e3 and 1a50d91 onto upstream a1641e9. Preserve current host-memory tests. Include one historical token-identical A/B with explicit provenance and document fresh CPU/CUDA validation.
2026-10-05 13:19:38 +02:00
Niko1221andFrancesco Albano 5288cd2902 adapt-async (#876): keep the blocking tier when the exchange buffers are pageable, and report ms per round
Co-authored-by: Francesco Albano <[email protected]>
2026-10-05 13:19:38 +02:00
Francesco Albano b437b8ea13 Asynchronous adaptive expert tier (--adapt-async 1, opt-in)
In the resident RAM mode (--resident-experts) an adaptive round runs on a
thread the decode loop joins after the draft: choose, copy the evicted slots
back with a stream sync, copy the new experts in from the RAM copy (through
the driver's staging when it is pageable). With ~90 swaps that was ~24 ms
every 4th window (measured on our fork). --adapt-async 1 splits a round into
four steps that advance between windows on a helper thread, so no window
waits for one:

  1. choose (adapt()'s rule, over snapshots of the counts and the residency)
     and copy the evicted slots back (D2H), each from the cache of the card
     that owns its layer
  2. stage the exchanges, mark the evicted experts non-resident (the CPU reads
     them from the exchange buffers), copy the new experts in: straight from
     a page-locked RAM copy, else through a pinned bounce buffer
  3. mark the new experts resident, move the evicted blobs into the RAM places
     the new ones left
  4. flip the copy's offsets

A slot is overwritten only after its old expert stopped being planned for the
GPU, and a RAM place is reused only once its expert is resident. The residency
tables on the devices are uploaded at steps 2 and 3, as apply_pending does
(the verify graph's doorbell reads them). A request starts with the round
drained, before its prompt lends any slot. --expert-profile-save counts the
routing at the start of each round, as the blocking tier does. Swaps that
need no exchange (the evicted expert is in the RAM copy already, or the new
one is not: a RAM budget) take the same steps without a copy back; a new
expert outside the RAM copy is read from the files on the helper thread.

Default off: which window first computes a swapped-in expert on the GPU
depends on when its copy lands, so the tier is not bit-exact from run to run.
Without the flag nothing changes. With it, the blocking tier still runs
outside --serve, without the resident RAM copy, and with --batch slots,
--peer-device or --remote-expert-opt (said once at startup). The exchange
buffers are twice as many with it (the bounce half): 96 more blob-sized
pinned buffers, ~130 MiB with 1.38 MB blobs.

Layer split (with the resident mode on a split, #848): each swap's copies run
on the owning card's refill stream under that card's device; the homes are
looked up the way #848's resident_stage_swaps does.

Measured: Swift 1.5 IQ3_XXS at 160K (q4_0 KV), i9-14900KF, 32 GB of RAM,
Windows 11, greedy, decode tok/s, blocking -> --adapt-async 1:
- RTX 4060 Ti + RTX 5080 layer split with the resident RAM mode (#848),
  --trim-stage-weights, the stock draft layer, five prompts x two rounds,
  two interleaved pairs: 79.7 / 78.8 -> 87.6 / 86.2 (+10%).
- the same split with a fine-tuned draft layer and --spec-min-p 0.65, four
  prompts x three rounds:
  81.0 / 81.8 -> 108.1 / 107.0 (+32%).
- RTX 5080 alone with --resident-experts: 26.7 / 26.6 -> 29.5 / 28.5 (noisy).
Without the flag the greedy output is the same as main's, byte for byte (4/4
prompts, 500 tokens, STRATA_IQ_MT_MIN=1 --pcie-frac 0 --adapt-every 0). With
it, 22 varied requests at temperature 0.6 on the split (43,825 experts
swapped in over 622 rounds) gave no degenerate answer.
2026-10-05 13:19:38 +02:00
Niko1221 388db6b106 layer split (#880): later stages' weights load after the search only for --layer-split auto with the trim asked; every other run keeps the 0.1.39 order and log
Drops the dead stage_pool trim block (the search only runs for auto, where stage_trim is false), keeps the search's number
for each card unchanged (the full untrimmed arena), and fixes the 'needs an explicit --layer-split' warning, which now
fires only when there is no GPU split at all.
2026-10-05 13:19:38 +02:00
Gopi 4cee400777 layer split: the stage weight trim works under auto too
--trim-stage-weights (PR #559) / STRATA_STAGE_TRIM=1 (PR #639) gives a stage only its own layers' dense
weights, but it refuses to run unless --layer-split is written out: `auto` chooses the split points 590
lines after the stages' weights are loaded, so by the time the split is known there is nothing left to trim.

Those weights now load after the search. The search still prices each card's arena from the pack index
(`WeightTable::pool_bytes` reads the index and allocates nothing), and takes it off the card's measured
free VRAM, so the number it sees is the number it has always seen - the split it picks is the split it has
always picked. This buys cache, not a different placement. The expert caches are sized later, from live
free VRAM, so they collect the freed bytes.

On the 4-way IQ3_S rig, `--layer-split auto` (which picks K=10,19,34):

  CUDA1 3650 -> 4608 expert slots, CUDA2 1241 -> 2485, CUDA3 533 -> 1772
  prompt chunk 768 -> 6144 tokens
  prefill 469.7 -> 1929.7 tok/s on the same 107,082-token prompt

CUDA0 still trims only with an explicit --layer-split. Its arena is read out to build the prompt path's PLE
tables, 590 lines before the split exists, so moving it is the larger half of this change. It is also the
cheap half to leave: measured, it costs 1% (1929.7 against 1950.8 for an explicit `10,19,34
--trim-stage-weights`, where CUDA0's cache is 4598 slots instead of 3017).
2026-10-05 13:19:38 +02:00
Niko1221andrkcth c8c79b17ea batch MTP (#846): opt-in --batch-mtp / STRATA_BATCH_MTP=1, warn instead of refusing, 1-row groups keep the 0.1.39 commit kernel, LRU graph cache only under the flag, the real draft geometry
Co-authored-by: rkcth <[email protected]>
2026-10-05 13:19:38 +02:00
rkcth 8cfb3fd7cc Keep MTP drafting in concurrent batch slots 2026-10-05 13:19:38 +02:00
xjc10 5ab99da482 bench, docs: the helper GPU's experts and the PCIe share on 2x RX 6900 XT; SECOND_GPU.md states the HIP measurement
bench/results/2026-10-04-rdna2-helper-pcie-share: the rig (two cards on PCIe 4.0 x8, probe 14.1 GB/s -> pcie_frac 0.39),
the method (STRATA_DECODE_TIMING per request), and the per-window breakdown for the baseline on a1641e9 (with and
without #835 on top: 59.6-60.4 ms per window, 4 PCIe entries per layer-window, the GPU-reach wait at 43 ms), for the fix
with the probed share (two runs: 37.2 / 33.8 and 35.2 / 33.8 ms, the share down to 0.06-0.26 entries) and with
--pcie-frac 0, and for the --pcie-frac 0 workaround on the baseline; the limits and the configuration used.

docs/SECOND_GPU.md: the helper section said HIP had not been validated; it now gives the two-card HIP measurement, what
the share cost before this change and the workaround, and keeps three/four cards as not validated.
2026-10-05 13:19:38 +02:00
xjc10 abe8919c06 expert plan: a helper GPU's experts are not part of the PCIe share
The verify window's GPU plan counted a layer's misses and moved the last pcie_num/256 of them to the primary over PCIe,
leaving out the primary's resident experts and the peer tier's but not those a helper GPU (RemoteExperts) holds.  The
plan runs before RemoteExperts::begin(), which only claims rows the plan left at kind -1, so a PCIe share above 0 read
part of the helper's experts over the primary's link while the helper idled for them.

RemoteExperts::holds(layer, expert) (the helper cache's slot_of), and the plan treats such an expert as it treats the
peer tier's: not a miss, never in the PCIe share; its rows stay at kind -1 for begin().  No default changes.

2x RX 6900 XT, IQ3_S, --remote-expert-opt with the probed share 0.39: 59.6 / 58.5 -> 37.2 / 33.8 ms per window,
PCIe entries per layer-window 4.00 -> 0.26 / 0.06, decode 40.6 / 39.3 -> 65.6 / 67.7 tok/s (--pcie-frac 0: 66.9 / 67.1).
2026-10-05 13:19:38 +02:00
Yoshiki-MatsudaandCursor affb999c63 verify: batch windows honor the all-resident stage (no host doorbells)
Co-authored-by: Cursor <[email protected]>
2026-10-05 13:19:38 +02:00
Niko1221 8e368af984 serve: tool_choice required with a single tool names it (#790) 2026-10-05 13:16:54 +02:00
Niko1221 82579827e9 serve tests: put the StopStrings and ConnectionClose classes back together after a merge (#715) 2026-10-05 13:16:54 +02:00
Niko1221 ee412cd776 serve: a <tool_call> in the reasoning that never closes stops holding the thinking back after 32 KiB (#804) 2026-10-05 13:16:54 +02:00
Niko1221 378dc39348 setup: a tip for a Q8_0 vision encoder on the CPU; docs: the llama-quantize command and measured fidelity (#625) 2026-10-05 13:16:54 +02:00
Niko1221 0eacbaba81 vision: --min-tokens N passes mtmd's image_min_tokens (vision.min_tokens, hand-edited); CPU encode time in the docs (#767) 2026-10-05 13:16:54 +02:00
wangmeng c347326242 serve: opt-in recovery from repeated reasoning passages
Maintainer: separate stop and recover modes (reasoning_loop_recovery: false|stop|recover), token-level splice of the effort sentence (EFFORT_TEXT), a client's top_p/top_k never touched, checks at the first clean boundary after each 512 tokens, bounded history, tests incl. a literal think tag (#728, #869)
2026-10-05 13:16:54 +02:00
Elias Klughammer add41aef65 server: "return_progress": true puts the prompt's PP line on the stream as llama.cpp's prompt_progress
Maintainer: prompt_progress() returns nothing under --batch (one global progress line), test (#837)
2026-10-05 13:16:54 +02:00
Breno Perucchi 600c428821 ckpt=0: still print STRATA_STATE_HASH for the request, so the parity gates see every request 2026-10-05 13:16:54 +02:00
Breno Perucchi 5f317a3afc ckpt=0: its session is neither continued nor parked; docs say what is skipped and when the root is taken 2026-10-05 13:16:54 +02:00
Breno Perucchi f6af1ddef9 serve: "strata_checkpoint": false lets a one-shot request skip its conversation checkpoint (#830)
A request with "strata_checkpoint": false sends ckpt=0 to the engine. That request reads its prompt in one part and
saves no checkpoint at its last turn boundary, nor the periodic ones of a long prompt. It still resumes from a
checkpoint it matches and still saves the system-prompt root. Absent or true: today's behavior; an engine without the
key skips it.
2026-10-05 13:16:54 +02:00
sergiywith d50dc91177 serve: a request's combined image file is deleted when the request is refused or its answer never starts
prepare() wrote the request's combined embeddings file (req-<id>.sve in the vision directory, ~10 MB a picture) right
after encoding, and only run()'s finally deleted it. But prepare() could still raise after writing it - EngineStarting
(the 503 while the engine starts, #344), the "no room to answer" and "exceeds the context" 400s - and a request can end
between prepare() and run(): its client gone before the first chunk was written, anything raised in between. run() is
a generator, so one that never started has no finally to run. Each of these left its file behind for good, and
clients that retry 503s added one per retry.

prepare() now writes the file only after those checks, and records its path before writing it. run() takes the path
over as it starts and deletes the file as before; do_POST's finally calls the new Service.drop_embeddings(), which
deletes a file that no run() took over.

Tests: EngineStarting, no room and exceeds-the-context leave no req-*.sve; over HTTP, on both APIs, a 503, a 400, a
client gone after prepare() and answered requests leave none.
2026-10-05 13:16:54 +02:00
sergiywith d6527e395a serve: the vision markers inside a message's text stay text and no longer take a picture's place (#150 follow-up)
The markers are special tokens only where the template writes them for an image item. The same strings in a
message's text (an agent reading chat_template.jinja, a tool result quoting it) became the same special ids, so text
with <|vision_start|><|image_pad|> before a picture took that picture's embeddings, every later picture moved up one
and the last real marker became text - every count still matched.

On 0.1.39 this takes #537's path: the vision markers join <think> / </think> as literal tags (frontend.VISION_TAGS),
swapped for private-use marks before the template and encoded as text through the tokenizer's plain spans. A pair
of markers cut apart across text parts is refused ("the prompt and its images do not match") instead of shifting.
2026-10-05 13:16:54 +02:00
sergiywith 3709c15f1d serve: a tool's picture the server cannot read becomes a note, not a 400
With tool_result pictures reaching the encoder, a server without one (every lazy-start config is text-only) or an
encoder that refuses a picture (WebP without Pillow, an error from strata-vision) answered 400. Clients resend the
whole history every turn, so one such picture failed every later request of the conversation; before, the picture
was dropped silently. Now a tool's picture that cannot be read becomes "[image omitted: <reason>]" in its place; a
picture the user sends keeps the 400.

Maintainer: log a left-out tool picture, a tool picture's URL is downloaded once (bytes reused by prepare), tests (#529; #582 and #819 are duplicates)
2026-10-05 13:16:54 +02:00
sergiywith d6b3f57811 serve: an image in an Anthropic tool_result reaches the encoder (Claude Code's Read of a picture)
Claude Code returns a picture it opens with Read as an image block inside a tool_result. The Anthropic
path turned every tool_result into its text, so the image never reached the encoder and the model
described a picture it had not seen. The tool message now carries the image item, as the OpenAI path's
tool messages already did; the chat template renders it inside <tool_response>. An image sent beside
tool results in the same user turn is kept too. A text-only tool result is still one string, so text
prompts are unchanged.
2026-10-05 13:16:54 +02:00
Niko1221 64be7561c5 serve tests: a fenced or prose-wrapped JSON object is now accepted in json_object mode (#762) 2026-10-05 13:16:54 +02:00
sergiywith 2190b48a82 serve tests: test_json_schema_text_format's schema failure only where jsonschema is installed
jsonschema is optional (serve/structured.py): without it a json_schema answer is checked to be one JSON object, so
the answer the test expects to fail passes, and the test failed on every install without it (0.1.39's own as well).
2026-10-05 13:16:54 +02:00
cipgysmo 0d95958f51 serve: extract JSON from prose and code fences 2026-10-05 13:16:54 +02:00
robwijnhoven 3794826b07 serve: map namespace calls written as namespace__name back to their namespace
Codex sends its MCP tools as namespace tools (mcp__websearch with web_search
inside). The Responses adapter shows the model mcp__websearch.web_search, but
Qwen3.8-Flash-Next writes the call as mcp__websearch__web_search, the flat
form Codex used for MCP tools. That name had no entry in the reverse map, so
the call came back without a namespace and Codex rejected it as an
unsupported call. Register the double-underscore spelling too (setdefault, so
a real tool of that name still wins).
2026-10-05 13:16:54 +02:00
Juan Canfield bcc39a1e42 serve: accept json_schema roots that are unions of object schemas
prepare_format refused every schema whose root was not literally "type":"object",
so a root anyOf/oneOf of object shapes - what apps written for llama.cpp's
json_schema send - got a 400, although every answer it allows is one JSON object.

_only_objects() accepts a root that can only be an object: "type":"object", an
anyOf/oneOf whose branches all qualify, an allOf with a qualifying member, or a
local $ref to one (a $ref cycle never qualifies). The "return exactly one JSON
object" directive and the object check without jsonschema stay true. A root that
also allows an array, string, number, boolean or null (including
"type":["object","null"] and a schema without a type) is still refused before the
engine loads.

Tests: serve/test_structured.py, unions accepted (a non-object answer is still
refused) and non-object roots refused: 10 OK with and without jsonschema
installed. test_server/security/lifecycle/detok/mcp/monitor/winjob: 214 OK.
docs/DETAILS.md: the root rule.
2026-10-05 13:16:53 +02:00
siddolo 773657cd1f serve: tolerate broken stdin pipes during engine cleanup 2026-10-05 13:16:53 +02:00
Evan a42a0c0b00 serve: say Connection: close on every response
The server is HTTP/1.0 and closes the connection after each response, but
never said so. A client that pools connections (.NET's SocketsHttpHandler)
then sometimes put its next request on the closing socket and got
'response ended prematurely' without the server ever reading it: 25 of
~1000 requests in one night from an agent app, each 1-24 ms after the
previous answer.
2026-10-05 13:16:53 +02:00
Trevin 38c9a4dce2 serve: a <tool_call> named in prose before the real call is content, not a malformed call
A reply such as "I will use the `<tool_call>` format now." followed by the call raised
ValueError("malformed tool call") in OutputParser.feed and ended the request: everything after the
first <tool_call> was taken as the call body. A call is the tag followed (after whitespace) by
<function=, as the chat template writes it; the tag with anything else after it is now content, and
it is held while streaming until its follower arrives, so streamed and whole output agree.

Found while porting tolerant tool-call recovery from a Qwen engine (ninfer --tolerant-tool-calls):
of its three recovery cases, Strata already handled values that quote the markup (#210) and prose
after the calls; this was the one that failed. Tests: ToolCallTagInProse (fails on main with 21
subtest errors, passes here); python -m unittest serve.test_server: 120 tests OK.
2026-10-05 13:16:53 +02:00
Amir Guindehi b3c63f7e0b serve: honour OpenAI tool_choice "required" and a named function
Without a grammar decoder a forced tool_choice was ignored, so clients
that need a call (an agent's first step, its "done" call) got text.
The server now writes the call's opening itself - <tool_call> and
<function= (with the name for a named choice) - and the model can only
go on with a call:

- without thinking, the opening ends the prompt;
- with thinking, it is written where the thinking ends, at a clean
  point, the way the budget's wrap-up is (continuing from the prefix
  the engine already holds);
- when reasoning_budget_tokens wraps up the thinking, it follows the
  wrap-up.

"auto" and no tool_choice are unchanged.  "none" offers no tools, as on
the Responses route.  An unknown name, a forced choice without tools
and other values are a 400 instead of being ignored; a forced choice
with MCP tools too.  The arguments are not constrained; a reply cut by
max tokens stays "length" without tool_calls, as before.

Maintainer: unknown tool_choice values are logged and act as auto (no 400), the flat {type:function,name} shape, Anthropic any/tool/none mapped and passed to /v1/messages, tests (#790). Note: tool_choice none now offers no tools on both routes.
2026-10-05 13:16:53 +02:00
Niko1221 da170f8ae7 serve: /config and /load bodies are marked read, so the close does not wait for the drain (#594) 2026-10-05 13:16:53 +02:00
Guillaume PUTIER a8497cf157 serve: the unread body is dropped under a time limit only, so megabytes still get their answer
A body over 1 MiB was left unread, and its answer was reset on Windows. The limit is now 5 s for the whole body, read one socket read at a time: a body of any size gets its answer, and a client that sends a byte at a time is let go after the 5 s (read() waited for the whole piece, with a new timeout for each byte).
2026-10-05 13:16:53 +02:00
Guillaume PUTIER 612a4d450c serve: read the request body an answer left unread, so the close is not a reset
The server answers before reading the body in some cases (401, 403, /load, /unload, a method with no handler) and then closes. Closing on unread bytes makes Windows send a reset, and a client that sends its body after the headers (http.client, urllib, requests) gets WinError 10054 instead of the answer.

After an answer, the body no handler took is now read and dropped, up to 1 MiB and with a 5 s timeout. A larger or chunked body is left unread, as before.
2026-10-05 13:16:53 +02:00
Niko1221 0ad6e8ac54 serve: validate stop in Responses and count_tokens; tests for stop in --batch and after a thinking budget (#454) 2026-10-05 13:16:53 +02:00
Zuraiz Anjum e7c2edb857 serve: honor stop and stop_sequences, cut the answer before the stop string and stop the engine there 2026-10-05 13:16:53 +02:00
Pravesh Khatana d398d7c33f fix(serve): safely handle malformed or partial tool call arguments in history
Maintainer: one frontend.tool_arguments helper for both frontends, bad JSON, a list or a number is kept as {"arguments": raw}; tests (#510)
2026-10-05 13:16:53 +02:00
Niko1221 9cd3d00b6d serve: STRATA_KEEP_EMPTY_TURNS=1 restores rendering of empty assistant turns (#843)
#886 skips them by default (recommended on; the owner can flip it). Only prompts that carry an empty
assistant turn change.
2026-10-05 13:16:53 +02:00
Arthur031221andjramirez-uab ffbc39f4fb serve: skip completed empty assistant turns in chat history
Filter earlier assistant messages with no text, images or tool calls
before rendering the prompt. Keep the final message.

Co-authored-by: jramirez-uab <[email protected]>
Signed-off-by: Arthur031221 <[email protected]>
2026-10-05 13:16:53 +02:00
Niko1221andkossum 8d0bb44620 serve: a tool call inside the reasoning counts only for a declared tool (#804, strict form of #787)
With tools declared, a <tool_call> in the reasoning is held whole; a declared name becomes a tool call,
anything else (undeclared name, malformed, unfinished, cut by </think>) stays reasoning text. finish() never
makes a call from it. Without tools the parser is byte-identical. Tests: parser (split tags, undeclared,
no tools, unfinished), OpenAI and Anthropic finish reasons, Anthropic block order.

Co-authored-by: kossum <[email protected]>
2026-10-05 13:16:53 +02:00
Strata Contributor 0b30051cb1 serve: invalidate the engine after fatal verification timeouts
Maintainer: fatal lines checked before batch routing, 'an earlier window never finished' is fatal, alive() check in generate_batched, tests run on Windows (#748).
2026-10-05 13:16:53 +02:00
Niko1221 264e81265b serve: /v1/responses refuses a file image from another origin (#553); a bad data: URL or unreadable file is a 400 (from #582) 2026-10-05 13:16:53 +02:00
sergiywith 58dd26b9f2 serve: image sources - network paths refused, URLs capped at 32 MiB and fetched outside the FIFO, no local files for other sites' pages
Vision.load read whatever an image source named. A network path (\\host\share\x.png, //host/share/x.png,
\\?\UNC\host\..., \??\UNC\host\..., file:////host/share/x.png) made Windows connect to that host and sign in with the
user's NTLM credentials - os.path.isfile alone does it. Such paths are now refused before anything looks at them, and
so is file://host/share/x.png (that names \\host\share; the old code read it as the relative path host/share/x.png).
An http(s) image was read whole, with no cap, inside prepare()'s `with self.fifo`: a huge one filled the memory and a
slow one held every other request for as long as urlopen waited (60 s per read). Now it is read up to 32 MiB
(IMAGE_URL_MAX; a larger Content-Length is refused unread), downloaded before the FIFO is taken, and a URL that cannot
be read is a 400 instead of a dropped connection.

Other sites' pages: with no api_key (the default) _authorized() lets every request in, and /v1/chat/completions and
/v1/messages read the body as JSON whatever its Content-Type; only /settings, /v1/load, /v1/unload and the MCP tools
check the Content-Type and Origin (_own_page). So any web page open in the user's browser could POST a chat request as
text/plain - a CORS "simple" request, no preflight - that names a file on this computer (or a network path) as an
image, and the server read it. The page gets no answer to read (no CORS headers unless cors_origins lists it or "*"),
but the read, and a network path's sign-in, happened. Reproduced over HTTP: answered 200, the file handed to the
encoder. Now a request whose Origin is neither the server's own address nor in trusted_origins may send data: and
http(s) images only; a file path or file:// URL is a 400 that says so. Clients that send no Origin (CLI tools, SDKs,
agents), Strata's own page (it sends data: URLs) and trusted origins are unchanged. As for /settings and MCP, a
DNS-rebinding page, whose Origin matches its Host header, still counts as the server's own.

Tests: each network form is refused with os.path.isfile never called, and local files, file:// and data: URLs still
load; the cap with and without a Content-Length, and a 404; the URL is downloaded with the FIFO free; a foreign-Origin
text/plain request naming a file is a 400 on both APIs, while data: images and no-Origin, own-origin and
trusted-origin requests are answered.
2026-10-05 13:16:53 +02:00
Niko1221 f96392dce8 build: the Aurora decode kernels compile in a CUDA build (host-device constexpr helpers, block_q8_1 copies by memcpy, plain loads for the AMD-only non-temporal ones, no wmma_gemm symbol off HIP) 2026-10-05 13:13:46 +02:00
Niko1221 bd50ff7a0a docs: the PLE table formats the engine reads, and how far each is from the BF16 table
Row error on 4,000 random rows against the checkpoint's BF16 table (Q8_0 0.53%, FP8 2.64%, IQ4_NL 7.60%, matching the
numbers reported in #651), and teacher-forced KL on the Q2_0 model with each table.
2026-10-05 12:18:48 +02:00
Niko1221 a5ada869ae docs: INTEL_ARC says what ran on an Arc (#868 follow-up)
The 0.1.39 paragraph said nobody had run the 0.1.39 port on an Arc. Two Arc Pro B60 reports (#867, #870) did, with
the two sycl/ fixes now in 0.1.40 (#784 the compile fix, #866 the ring wait). Say that, and that an unpatched 0.1.39
port did not compile against its own engine sources.
2026-10-05 12:18:48 +02:00
LocalXPU 525017f443 docs: e221 is also an Arc Pro B60 id (seen on another B60 pair) 2026-10-05 12:18:48 +02:00
LocalXPU b70c9cbbfe docs: the host has 64 GB of RAM (62 GiB usable), not 62 GB 2026-10-05 12:18:48 +02:00
LocalXPU 7cd96e3eef docs, setup: Arc Pro B60 rows, B60 notes, PCI id e211
INTEL_ARC.md: "What has been run" rows for 2x and 1x Arc Pro B60, plus notes for the card: PCI id 8086:e211 (bmg-g21), setvars.sh
stopping at OCL_ICD_FILENAMES under `set -u`, running natively without Docker, and the layer-split startup warning that counts the
other card's layers.

sycl/setup_intel.py: the B60 table entry was e221, which the kernel's xe id list files under the BMG-G31 cards (0xE220-0xE223). Both
Arc Pro B60 cards measured here are 8086:e211, so setup named them "Intel GPU e211 (xe)" with a guessed VRAM size. e211 is added;
e221 stays, since there is no card here to say it is wrong. tools/test_setup_sycl.py checks the table.

COMMUNITY_BENCHMARKS.md and bench/results/2026-10-04-community-2x-arc-pro-b60/: the report, in the template's format, with the
server configs, engine logs and per-request timings.
2026-10-05 12:18:48 +02:00
LocalXPU a5682fea16 sycl: keep cudaStreamQuery's answer in the per-layer ring waits
The migration turned `q = cudaStreamQuery(cs)` into `DPCT_CHECK_ERROR(cs->ext_oneapi_empty())`. That macro yields 0 whenever
the call does not throw, whatever the queue holds. The guard `q != 1 && *seq < want` (verify.cpp, session.cpp) therefore
fired on any ring that took more than 2 ms and reported "never rang (graph finished)" while the graph was still running.

With every expert resident the host does not wait per layer (STRATA_VERIFY_NO_HOST), so the path measured on the B70 never
reached it. With host-mirrored experts the first layer whose GPU time passes 2 ms does: layer 15 of the Coder on one Arc Pro
B60, every time.

q is now 0 when the queue is empty and 1 while it is running, as the guard and the trace comment ("1 = still running") expect.
fixups.py gets the same rewrite (entry 8), so a fresh dpct run of session.cpp produces it too.

Measured on one Arc Pro B60 (4,042 of 12,288 Coder IQ1_M experts mirrored in RAM, 128 greedy tokens, AOT bmg-g21): before,
exit 1 at layer 15 in 5 of 5 tries; after, exit 0 in 7 of 7 runs, at 11.2-11.9 tok/s with coherent text.
2026-10-05 12:18:48 +02:00
sarashin 9661ecb882 sycl: follow #626 (ThreadAffinity) and #559 (NativeDense::load layer range) so v0.1.39 compiles
The sycl port went in as 5ffcce4 ("sycl: port 0.1.39"). Two engine-side changes
were merged into the release branch after it (4f5e6c8, f5410ae) and nothing in
sycl/ was updated for them, so the v0.1.39 tag does not compile:

- #626 (5a8b292) made the thread affinity a value: pin_current_thread()
  returns strata::kernels::cpu::ThreadAffinity and restore_thread_affinity()
  takes it. sycl/include/strata/core/session.hpp still stored a `long long`
  (no conversion exists, and it did not include the header that names the
  type); sycl/src/core/session.cpp still assigned the returned struct to it
  and passed the `long long` back.
- #559 (9461f5e) added the two layer-range arguments to NativeDense::load.
  The sycl out-of-line definition was still the 4-argument one and no longer
  matched the declaration in include/strata/core/native_dense.hpp.

Both are fixed by mirroring the CUDA sources: the same member type, the same
`outside()` predicate, and the same place it is applied in the tensor loop.

`pinned` now follows CUDA as well: it is pinned_core.valid, so a pin that
failed is not "restored". Nothing changes when the pin succeeds.

No caller passes layer_lo/layer_hi yet, so `outside()` is never true today:
this restores the build and does not change what the engine computes.

Measured here: Arch Linux, Intel oneAPI DPC++ 2026.0.0 + oneMKL 2026.0,
SPIR-V (JIT), cmake -S sycl -B build-sycl -G Ninja -DCMAKE_C_COMPILER=icx
-DCMAKE_CXX_COMPILER=icpx, then
`cmake --build build-sycl --target strata -- -k 0` -> 0 errors, links. The two
errors are a type and a declaration mismatch, so the compiler version does not
matter.

Not built here: the other targets (kernel parity tests, benches) and the AOT
path. Not run on an Arc in this change.
2026-10-05 12:18:48 +02:00
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 626a89bc1e ple: --ple-io ram faults the table in on up to 16 threads before mlock (#651)
mlock alone brings a cold file in one page at a time from one thread, ~0.47 GiB/s (about two minutes for a 54 GB
Q8_0 table); 16 threads reach ~2 GiB/s on anon761's machine. The pages are then resident and mlock only pins them.
When mlock fails, the pages stay faulted in (the message says they may be reclaimed) instead of being touched again
one by one.

ple_reader_test gets --make-table PATH FORMAT (a big synthetic table, with --rows) and --ram-time PATH (open mapped
and locked, print the seconds) to time it cold.

Ported by the maintainer from anon761's PR #651.

Co-authored-by: ksanislo <[email protected]>
2026-10-05 12:18:47 +02:00
Niko1221andCC-David-CC e79b8fc143 ple: check the table's size arithmetic before using it (#865)
PleTable::open compared rows x row_bytes against the file after the 64-bit product could already have wrapped, and
mapped the tensor before checking it fits. Now: refuse a row count that overflows rows x row_bytes and a data
section that starts past the end of the file, check offset and size without an addition that can wrap
(offset > have, need > have - offset), require a single-tensor shard to start at offset 0 and fill the file, and map
the table only after all of that. close() also resets the FP8 scale and the byte counter.

ple_reader_test --selftest writes tables that claim one row too many, one too few and a wrapping row count, for every
format, and checks both readers refuse each with a message and stay closed.

Checks taken from CC-David-CC's #865 (its opt-in Q8_0 table is #651's; the direct reader reads Q8_0, so the
experimental switch and the mapped-only limit are not needed).

Co-authored-by: CC-David-CC <[email protected]>
2026-10-05 12:18:47 +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 e51fa6c362 prefill: STRATA_HCD_EXACT says once when it cannot take the kernel (no tuning table, or a solution other than 1176 / 1177), and hip_prefill_hcd_exact_parity checks the kernel against hipBLASLt
The kernel reproduces one hipBLASLt solution's summation order; the test compares both on the same inputs for T from 64
to 16384 (bitwise) and SKIPs without STRATA_HIPBLASLT_TUNING.
2026-10-05 11:27:31 +02:00
Niko1221 6bd7e930da merge fixes: the stray #endif of the two fused-expert dev_info blocks, the DONE line's newline escapes 2026-10-05 11:27:30 +02:00
Niko1221 9708b549a4 gfx1151: the exact (bitwise) Aurora switches are on by default on gfx1151 only (arch_defaults), the rounding-level ones stay opt-in
16 switches whose output was byte-identical to the switch off on Aurora (GDN head / PP / CONVL2 / NOY / SPLIT, QFUSE,
CVEC_FUSE, HCD_EXACT, PF_PAD, Q8 / Q6 packed, MMVF_ROWS, ATTN_LANECELL, EXPERT_V2, TSUM, LFUSE, PLE_BATCH) are set at
start-up when device 0 is gfx1151 and the user has not set them; STRATA_GFX1151_DEFAULTS=0 turns the table off.  The
GDN head kernels are AMD-only now (HIP builds; CUDA keeps its key-head kernel), and verify.cpp's namespace-scope env
reads became lazy so the table reaches them.
2026-10-05 11:15:33 +02:00
Niko1221 8a0755a1da core: device_free_bytes caches the unified-memory check per device (#409 review), not in a process-wide static 2026-10-05 11:10:11 +02:00
Niko1221 1bb3279d74 prefill: the gfx11 WMMA gates match the compiled arch list (no silent zeros on gfx1103 / gfx1152), cached per device
WmmaTraits::mma's #else returned its accumulator (zeros) for a gfx11 part outside the __gfx1100__..__gfx1151__
list; it traps now, and the runtime gates of the WMMA GEMM, the prompt attention, the block scorer and the HC
up-projection take exactly gfx1100 / 1101 / 1102 / 1150 / 1151 (strata/kernels/gfx_arch.hpp) so a gfx1103 or
gfx1152 falls back to hipBLASLt instead of launching code that is not there.  The gate is cached per device
(a process-wide static answered for the first GPU only; hipGetDeviceProperties on every call was the review's other
complaint).
2026-10-05 11:10:11 +02:00
Niko1221 cbe3fddc59 build: wmma_gemm.cu is added to strata_prefill only under STRATA_ENABLE_HIP (it includes hip_runtime.h; the CUDA build broke) 2026-10-05 11:10:10 +02:00
Niko1221 1fb37cf810 Merge f-140 into the Aurora port; verify: halo's STRATA_VERIFY_RESIDENT is 0.1.39's all-resident graph
0.1.39 already skips the host round trip when every expert is in VRAM (all_resident_, on by default, #871's
refresh_ar), so the port keeps that graph and adds only what halo built on top of it: the LFUSE gated combine.
2026-10-05 11:08:25 +02:00
Niko1221 bc6b6f87a9 Merge the Aurora gfx1151 branch (f-halo-int + HCD_EXACT + the expert bundle) onto 0.1.39
Merges f-halo-int (82cf9b8 + 56 commits: the gfx1151 target, WMMA prompt attention / GEMM / scorer, GDN recurrence,
fused gfx11 prompt experts, decode switches, drafting) with f-halo-int-S (STRATA_HCD_EXACT, 203ec5c) and
f-halo-int-W (table16, float4 combine, LDS alias + occupancy, d23d4e2).  Conflicts resolved against 0.1.39's own
all-resident verify graph, forked shared-expert stream, fused swiglu q8_1, PLE batch, GDN commit split and peer
prompt path; the gfx1151-only code stays gated (build fixes and the arch defaults follow in separate commits).
2026-10-05 11:07:15 +02:00
Niko1221 258532d667 test: the checkpoint split's allocation-failure test covers 1-3 stages and an incomplete checkpoint (#752) 2026-10-05 11:03:50 +02:00
Niko1221 604056878e serve: a Stop during the prompt read goes to the engine at once (#879 abort latency) 2026-10-05 11:03:50 +02:00
Niko1221 cfe09ab5f5 prefill: STRATA_TEST_PAGEABLE=1 forces the pageable fallbacks of Prefill::init (#828 test switch) 2026-10-05 11:03:50 +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
Niko1221 82bc5de632 generate: res_put syncs stream 0 (HIP has no cudaStreamLegacy) (#550) 2026-10-05 11:03:50 +02:00
sergiywith 53ab5f7161 generate: residency-table uploads wait for their own copy before the non-blocking streams read the table
Every upload of the expert residency table (host_res -> d_res, and each stage's) was a plain cudaMemcpy: a pageable
copy on the legacy stream, which may return before its DMA has landed, while the kernels that read the table run on
streams created cudaStreamNonBlocking, which do not wait for the legacy stream. A kernel enqueued right after could
read a table half old, half new: an expert computed by both the GPU and the CPU, or by neither. The ~100 KB copy
lands in microseconds, so this rarely fires, but nothing ordered it. The uploads now go through res_put(), which
waits for the copy on the current device's legacy stream only. Same values, same output.
2026-10-05 11:03:50 +02:00
Niko1221 b4405a0eaa serve: a split's stage-0 GPU profile line keeps its text (#848); later stages say which stage 2026-10-05 11:03:50 +02:00
Francesco Albano 43c8cfa9e2 setup: the low-RAM mode's resident variant on several GPUs (#642)
With an engine that keeps --resident-experts on a layer split, setup no longer
steers a low-RAM PC with two cards to one of them:

- at install, the low-RAM mode on several GPUs takes the resident variant by
  the same rule as on one card (what no card holds fits the RAM with
  LOW_RAM_HEADROOM_GB beside it), without the one-card question;
- at a start, a resident config on several GPUs keeps --resident-experts
  (split_mmap leaves it alone);
- a resident config started on a PC with two cards is offered both, "y" by
  default, as any other config.

It is gated on MIN_ENGINE >= RESIDENT_SPLIT_ENGINE (0.1.40 here, a guess at
the release that would carry the engine change), so until then setup behaves
as before and the golden configs are unchanged. Two tests run the new paths
with MIN_ENGINE patched; tools/test_setup_*.py pass (260 tests).
2026-10-05 11:03:50 +02:00
Francesco Albano fe9c10cad7 Resident RAM mode with a layer split
--resident-experts (the low-RAM mode's resident variant) ran as plain
--mmap-experts as soon as a layer split was set. It now keeps its RAM copy on
a split:

- the experts every stage's cache holds are left out of the RAM copy, not only
  CUDA0's;
- when the rest does not fit whole (#467), the copy keeps the hottest by the
  whole expert profile. Each stage only keeps its own layers' share of the
  profile, and ranked by CUDA0's share alone the copy held layers 0..K-1's
  experts and read every later layer's from the files (4.0 GiB in RAM instead
  of 21.2 on the PC below);
- an adaptive swap copies the evicted expert back into RAM from the cache of
  the card that owns its layer, on that card's stream. host_res holds slot
  numbers of whichever cache owns the layer: reading a later stage's slot out
  of CUDA0's cache put another expert's bytes in RAM, and after the first
  swaps the answers turned to garbage. (Latent until now: the guard kept the
  mode off a split.)
- the decode profile prints every stage's GPU profile, not only CUDA0's.

Remote expert caches (--expert-cache-remote) are still refused with it.

Swift 1.5 IQ3_XXS at 160K, q4_0 KV, RTX 4060 Ti (layers 0-19) + RTX 5080
(20-47), i9-14900KF, 32 GB of RAM, Windows 11, four greedy prompts, decode
tok/s (first round / fourth):
- 5080 alone, --resident-experts: 25 / 29
- split, --mmap-experts (what --resident-experts became): 32 / 64
- split, --resident-experts (22 GiB of experts locked in RAM): 71 / 69
With --pcie-frac 0 --adapt-every 0 the split's greedy output is the same as
--mmap-experts' (4/4 prompts). With adaptive swaps on, 33 varied requests at
temperature 0.6 (thinking on and off, five languages, code, each checked for
degenerate output) exchanged 81,783 experts with the two caches and gave no
degenerate answer.
2026-10-05 11:03:50 +02:00
Francesco Albano 5c4105c073 Layer split: --vram-reserve-later-mib, a separate reserve for the later cards
--vram-reserve-mib is every card's reserve, but the card that drives the
monitors needs more headroom than one that drives none: one value has to fit
the display card, and the other cards then keep that headroom for nothing.
--vram-reserve-later-mib N is the reserve of every card after the first
(default: --vram-reserve-mib's value), in the split search and in each later
card's cache sizing.

RTX 4060 Ti (no display, layers 0-19) + RTX 5080 (display, 20-47), Swift 1.5
IQ3_XXS at 160K: --vram-reserve-mib 1000 gives the 4060 Ti's cache 4551 slots;
--vram-reserve-mib 300 --vram-reserve-later-mib 1800 gives it 4867, and the
5080 keeps 1.3 GB free with everything loaded.
2026-10-05 11:03:49 +02:00
Niko1221andzengyishou 1ec615f585 serve: park_current's try covers the checkpoint split too (#752): a host OOM skips parking
Co-authored-by: zengyishou <[email protected]>
2026-10-05 11:03:49 +02:00
Strata W 528c1fb185 prefill: fused-expert exact wins (table16 via direct selector, float4 moe_combine, LDS alias + occupancy)
Bitwise-identical to e58d7fc (generated ids identical on UD-IQ4_XS, UD-Q4_K_XL, IQ3_S, IQ3_XXS at 8K/4K/64K; harness fused-output hash unchanged):
- table16: IQ4_NL / IQ4_XS decode through the direct-selector v_perm form (X5; exhaustive 2^32 check). -DSTRATA_W_NO_T16 restores the old form.
- moe_combine: four columns per thread with float4 loads, same fma order (X5); falls back to the scalar kernel if Dm / shared / bo are not 16-byte aligned. -DSTRATA_W_NO_COMB.
- native_w11_kernel: the H tile aliased onto the weight buffers (gate/up LDS 46.5 -> 30.2 KB), amdgpu_waves_per_eu(8), 4 blocks per WGP (X1), only for the types that do not spill under the 192-VGPR cap (gate/up IQ2_*, IQ3_*, IQ4_XS; down Q2_0, Q8_0, IQ4_NL). -DSTRATA_W_NO_OCC.
- X3 on1 (skip the second 16-row n tile of the down kernel) NOT included: no gain alone (-0.1%), and with the occupancy cap it makes IQ4_NL down spill 62 VGPRs and the whole bundle 2.3% slower.
2026-10-05 10:56:23 +02:00
Strata Contributor 1d78e57bd0 cache: preserve checkpoints when split allocation fails 2026-10-05 10:00:33 +02:00
sergiywith f6601bf83f #606: the fused SwiGLU q8_1 quantizers keep the block's scale and sum finite too
0.1.39 clamped two q8_1 activation quantizers (#606: an fp16 sum or scale past 65504 rounds to inf, the dot products
then read inf * 0 = NaN, and a NaN in the residual ends as one token repeated). Two more store the block unclamped:
- native_swiglu_quantize_q8_1_kernel (native_mmvq.cu), the shared expert's fused SwiGLU + q8_1, on by default
  (STRATA_FUSED_SWIGLU_Q81) in decode and the multi-token pass;
- native_gu_fused_kernel (iq_kernels.cu), the routed experts' fused SwiGLU + q8_1 (gfx906 builds, STRATA_EXP_MODE=8).
Both now go through q8_1_finite / q8_1_quant / q8_1_ds as their siblings do: every finite block is stored bit for bit
as before. iq_multi_parity's #606 check covers the shared expert's one (gate = 32, up = x / 32: its activations are x
exactly). rwkeyes found the first one (#606).
2026-10-05 10:00:29 +02:00
Niko1221 203ec5cdf7 prefill: STRATA_HCD_EXACT=1 - the HC down projection by a WMMA kernel with hipBLASLt solution 1176/1177 k order (StaggerU included), bitwise 2026-10-05 03:52:38 +00:00
Niko1221 e58d7fc849 tests: gdn_rec_head also compares the PP (variant 4) and two-column (variant 5) recurrence kernels, bitwise
T = 1, 7, 8, 9, 17, 100, 2048, 2051 (full and partial chunks), y, its FP16 bits and the final state: 0 values differ.
2026-10-04 20:26:01 +00:00
Niko1221 a4c30dc5c6 prefill: STRATA_GDN_PP=2 - the GDN recurrence with two columns per lane, bitwise
gdn_rec_quad_kernel's limit was shared memory: every wave re-read the 64 k / q floats of its row group per token
and the shuffles went through the same unit (probe: no k / q reads 9.8 ms, no shuffles 16.9 ms, of 21 ms).  A lane
now carries two columns (8 lanes x 2 columns x 4 row groups = 16 columns a wave, 64 a block, 96 blocks, 384 waves
in one round at 230 VGPRs): the two columns share the k / q reads, and each wave has two independent dependency
chains.  Per column the same arithmetic in the same order.  One 16K chunk-layer 25.4 -> 16.9 ms (gdn_probe14.hip,
bitwise vs the old kernel); 64K prompt 1232.4 -> 1267.6 tok/s (-1.47 s, interleaved, 2 runs), ids identical at 8K
and 64K.  STRATA_GDN_PP=1 is the one-column kernel with the register fixes only (-0.66 s).
2026-10-04 20:13:02 +00:00
Niko1221 2f510316ec prefill: STRATA_GDN_PP=1 - the GDN recurrence at 10 waves a SIMD, no state copies, bitwise
gdn_rec_quad_kernel needed 196 VGPRs = 7 waves a SIMD, but its 192 blocks of 4 waves need 9.6: a second round of
blocks ran after the first (half the heads took exactly half the time).  Cause: the 32 state-row addresses of the
first loads were CSE'd with the final stores and kept live across the whole walk (64 VGPRs); the stores' lane offset
now goes through an empty asm, so the addresses are made again - 133 VGPRs.  A full chunk's four tokens also run
with the state alternating between two register sets (the update writes the other one), which drops the 32
register copies the compiler made after each update (131 VALU a token instead of ~178).  The same arithmetic in
the same order.  One 16K chunk-layer 25.4 -> 20.5 ms (gdn_probe12.hip, bitwise vs the old kernel); 64K prompt
1233.7 -> 1249.0 tok/s, -0.66 s; engine output ids identical at 8K and 64K.
2026-10-04 19:23:07 +00:00
Niko1221 9668e32400 prefill: STRATA_GDN_CONVL2=1 (conv + q/k L2 norm in one kernel) and STRATA_GDN_NOY=1 (no dead FP32 write), bitwise
STRATA_GDN_CONVL2: gdn_conv_tiled_kernel with gdn_l2_kernel folded in (a block's 128 threads are one head's channels):
the tile's conv loop verbatim, then the sum of squares spelled out as the compiler built the l2 kernel (first xor step
an fma on the partner's rounded square, then adds), the four partials in the same order, the same v * rsqrtf(ss+eps).
16K chunk, per layer: 9.13 ms -> 6.95 ms.  A first version keeping the values in registers differed by 1 ulp in the
silu (different code shape); this one keeps the tiled loop as is.

STRATA_GDN_NOY: the norm kernel does not store the FP32 y nothing reads (403 MB per 16K chunk and layer); the FP16
value is made with v_fma_mixlo_f16, the instruction the compiler used next to the store (otherwise a mul and a
convert round twice and 1 ulp of the FP16 results differ).

Engine ids match the unchanged binary at 8K (acd9c8328408).
2026-10-04 17:38:17 +00:00
Niko1221 ebfebd23e2 Merge branch 'f-halo-int-a' into f-halo-int 2026-10-04 15:32:04 +00:00
Niko1221 04e1e11afb HIP: grouped decode experts for UD-Q4_K_XL (Q4_K/Q5_K gate/up, Q5_1/Q8_0 down), bitwise, opt-in STRATA_EXPERT_V2K=1
S26 structure (4 rows per warp, activations in LDS, fused SwiGLU+q8, optional TSUM) over S27<TY>::load/apply splits of the existing vec_dot functions.
2026-10-04 15:31:16 +00:00
Niko1221 9d9338dd1b Merge f-halo-int (7440806) into the stream A branch 2026-10-04 14:40:32 +00:00
Niko1221 d385870f77 prefill: gfx11 native fused experts - sign-table decode and a flat weight buffer (down kernel 18% faster), bitwise
native_w11_kernel: the sign bytes of the IQ2 / IQ3 grids come from a shared-memory table of byte masks
((g ^ m) + c per 4 bytes; no zero byte in any grid) instead of the compare / subtract emulation; the decoded
weight stages are one flat shared buffer addressed through a pointer, which changes the code the compiler makes
for the down kernel (7.5 -> 6.2 ms per 128-expert launch, IQ3_S / IQ4_NL, 16384 tokens).  One layer of 16384
tokens, fused experts: IQ2_S 83.8 -> 76.3 ms, IQ2_XXS 74.4 -> 68.9, IQ3_S 78.5 -> 73.8, IQ3_XXS 77.6 -> 71.8.
Outputs are bit for bit the same (the test now prints a hash of the fused output in pair order).
2026-10-04 14:39:57 +00:00
Niko1221 609ce5a645 qsa_prompt_attn_parity: print a hash of the new kernel output bits 2026-10-04 14:33:01 +00:00
Niko1221 48994ee7a4 qsa_prompt_attn_parity: probe modes (QA_SAME, QA_DUMP) for the K/V sharing question 2026-10-04 14:27:56 +00:00
Niko1221 7440806987 prefill: STRATA_PF_FUSED_KQ=1 - the fused native-expert path takes UD-Q4_K_XL (gfx11): gate/up Q4_K / Q5_K, down Q5_1 / Q8_0
The formats carry a minimum, so the activation blocks' spare bytes now hold the int8 code sums per 32 values (written by
every path, read by none else). Layers MMQ does not cover on HIP (Q4_K) join the fused layout through MmqPlan::fo.
Opt-in (default output bit-identical). Test pairs plus an int8-rounded model check; QA_SAME probe in the attention harness.
2026-10-04 14:16:58 +00:00
Niko1221 a1641e9f77 setup: a tip (not a change) for images on a 12 GB card: --vram-reserve-mib 1000 if a request stalls
With the image encoder on the GPU, the 700 MiB reserve can leave ~200 MiB free on a 12 GB card (UD-IQ4_XS
end-to-end check on an RTX 5070). Setup prints a one-line recommendation; the written config is unchanged.
2026-10-04 14:16:36 +02:00
Niko1221 91b0f53659 setup: Unsloth's UD-IQ4_XS is a regular choice (no longer experimental); UD-Q4_K_XL stays experimental
- "experimental" is per model now: MODELS["UD-Q4_K_XL"] keeps it, the unsloth family and UD-IQ4_XS drop it. The
  first menu lists the family without [experimental], with a one-line about (UD-IQ4_XS: a 94 GB download; under
  ~80 GB of RAM part of its experts are read from the SSD). Its size menu lists UD-IQ4_XS first (the default) and
  UD-Q4_K_XL second; an experimental size always sorts last. --model names are unchanged.
- UD-IQ4_XS: no EXPERIMENTAL warning, --check verdict without it. Images are asked (off by default), per model: the
  engine's image path has no restriction for this pack, which uses the original model's image encoder; not yet run
  with images. UD-Q4_K_XL keeps images off.
- MCP: the fallback tables mirror setup's (per-model experimental/vision), sizes in setup's order, per-size
  "experimental" and "images", the vision check per model.
- Docs: README and its six translations, MODELS, UNSLOTH_Q4, AI_SETUP, MCP_SERVER.
- Tests: the menus, the default, --model by name, images, --check, the pack/tokenizer paths; the golden configs
  are unchanged.
2026-10-04 13:24:29 +02:00
Niko1221 11212e08f8 docs: README refined + six translations 2026-10-04 13:17:54 +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 ad33b80748 Merge branch 'f-139b-arc' into f-139-rel 2026-10-04 10:12:13 +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 0918cba0a5 Merge branch 'f-139b-cpu' into f-139-rel 2026-10-04 10:11:54 +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 65a48f0d57 prefill: STRATA_PF_PAD with a weight that cannot be padded (an i-quant dense tensor) - X read at its stride
The padded activation of ssm_out / attn_output was handed to Gemm::native, whose padded-weight path needs a
strided dequant; the GSQ-RCO IQ3_XXS model has IQ3_S ssm_out tensors (no strided dequant for the i-quants), so
the engine exited at the first 64K chunk ("a padded X needs STRATA_PF_PAD's path"). Now such a weight is
dequantized unpadded and strata_pf_gemm_f16_ld reads X at its stride: the same kernel and products as with
PF_PAD off, the same bits.
2026-10-04 09:24:45 +02:00
Niko1221 39cd7fbef4 setup: the CUDA 12 engine's driver floor per OS (Windows 527.41 -> 528, Linux 525); the wheel pins match the 12.9.1 toolkit 2026-10-04 08:01:18 +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 18d853ab4c setup: --remote-expert-opt recommended for 2+ GPU configs (#578)
Added where a config goes to several GPUs (setup --gpus / a multi-GPU install, start --gpus, "use both" at
start). Single-GPU configs are not touched. Opt-out: setup --no-remote-expert-opt, or "remote_expert_opt": false
in the config (a user key, kept on re-runs). The engine uses it only with a helper cache (--expert-cache-device1..3),
so a layer split runs as before. tools/test_setup_remote_opt.py; the 2x16 GB golden configs gain the flag.
2026-10-04 07:57:23 +02:00
Niko1221 5eecc5dee6 prefill: STRATA_PF_HCDOWN=1 - the HC read's down and inject projections as one WMMA GEMM over a padded xn16
The hyper-connection read ran two hipBLASLt BF16 GEMMs over the same T x 10240 xn16: down (N 320, 6.78 ms per
16K-token half, 15.8 TFLOPS - the 4 KB-multiple row stride) and inject (N 4, 1.81 ms). strata_pf_hcdown_bf16 does
both in one 128 x 128 BK-64 WMMA GEMM: weight rows [w_down; w_inject] read from the two tensors, the epilogue
splitting lo (T x 320) and inj (T x 4), so xn16 is read once; with the flag, xn16 is written with token stride
10240 + 64 (gr_norm_rs / gr_write_norm_rs / gr_write_cvec_norm_rs take an ldx; unchanged and bitwise when off).
s23/hcdown_probe.hip, 16K tokens: 3.60 / 3.77 ms for both (unpadded 5.75).

Opt-in, on chunks of STRATA_PF_SWITCH_MIN_T+ tokens. FP32 accumulation in another k order than hipBLASLt:
rounding-level. Aurora, c-best3 v3.2: 64K timing hc read 7234 / 7253 -> 6439 / 6175 ms; prompt 8K 1048 -> 1075 /
1036, 64K 1053 / 1056 -> 1067 / 1068 tok/s. Teacher-forced KL (8192-token prefix, 2001 positions) vs the default,
next to the accepted MMQ-vs-fused pair: offset 0 0.162 (mmq 0.158), 30000 0.110 (0.126), 70000 0.031 (0.033);
ledger {32,17,130}, 64K retrieval 3/3.
2026-10-04 06:17:29 +02:00
Niko1221 2bb11b52a6 hip_compat: cudaFuncAttributePreferredSharedMemoryCarveout (#646's fused_gr carveout on HIP) 2026-10-04 04:53:45 +02:00
Niko1221 6448f0f7ad prefill: STRATA_CVEC_FUSE=1 - a steered layer's write, control vector and next norm in one pass over R, bitwise
With the speed-projection control vector on, every steered layer's FFN write-back took three passes over the
T x 10240 FP32 residual (gr_write, cvec_apply, the next half's gr_norm_rs) instead of the one gr_write_norm_rs of
the other layers. gr_write_cvec_norm_rs does the three in one: gr_write's arithmetic, cvec_kernel's dot order and
two-level reduction, gr_norm_rs's sum order and block_sum - the same bits. STRATA_CVEC_FUSE_CHECK=N runs the
three-kernel path on a copy of R on the first N fused writes and compares R, the row scales and the BF16 image
(Aurora, 8K prompt, layers 4-11: all identical). cvec_tables() hands out the current device's vector tables.

c-best3 v3.1 on Aurora, one 64K prompt with STRATA_PREFILL_TIMING (x2): hc read + combine 13.0 / 13.1 ->
10.8 / 11.2 s, GPU timeline 64.6 / 64.9 -> 60.6 / 63.0 s; benches interleaved, same ids: 8K prompt
971.5 / 1019.9 -> 998.8 / 1045.1, 64K 1025.3 / 1012.8 -> 1039.2 / 1014.4 tok/s.
2026-10-04 04:50:20 +02:00
Niko1221 002f64da50 docs: STRATA_RING_BYTES=1 (#583, opt-in) with its RTX 5070 numbers and quality check 2026-10-04 04:41:35 +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
Niko1221 20f471be73 prefill: the STRATA_PF_GEMM 128 x 256 kernel with a 64-k LDS tile, bitwise (STRATA_PF_BK64=0 keeps BK 32)
With global loads removed, the BK 32 double-buffered kernel still stopped at 37-39 TFLOPS of the ~50 WMMA peak:
the K loop itself (fragment reads, one barrier per 32 k) was the limiter. pfg::kernel_bk64 stages a 64-k tile in
one LDS buffer (55 KB) from 12 named registers with 2 barriers per tile, half the loop trips. Every accumulator
takes the same WMMAs in the same k order: the same bits (s23/gemm_probe11.hip, memcmp vs the BK 32 code).

gfx1151, M 16384, both operands padded (2 runs): N 2560 K 6144 38.0 -> 41.3 TFLOPS, N 12288 K 2560 38.9 -> 40.9,
N 10240 K 2560 40.3 -> 40.4. Also probed and rejected (same bits, slower): A rows read under the previous row's
WMMAs (36.7-37.9; 8-16 B spills), half-wave 16 B reads + v_permlanex16 (31.7-32.4), both (32.8-33.5).

Used for N >= 1024 and K % 64 == 0. c-best3 v3 on Aurora, BK 32 -> BK 64, interleaved, same ids: prompt
8K 959.8 / 947.8 -> 968.0 / 1001.9, 64K 1007.9 / 1004.0 -> 1028.0 / 1025.4 tok/s.
2026-10-04 03:55:08 +02:00
Niko1221 0c4d1fef09 #583 on 0.1.39b: the ring's byte budget and the loan's corrected count opt-in (STRATA_RING_BYTES=1)
Default: 0.1.39's 384/1024-slot ring and bytes_needed count, so the default loan and output stay as they were.
The auto chunk scan (the PR's second commit) is not taken.
2026-10-04 03:53:44 +02:00
Niko1221 e0446f394c #465 setup: recommend "parallel" only where the experts mostly fit in VRAM
On a card whose experts mostly run on the CPU the slots cost speed per request (RTX 5070, Q2_0: 11-24 %), so setup
now recommends slots only when the expert cache (every card of a split counted) still holds half of the model's
experts beside them (Q2_0: 24 GB and up, or a split); elsewhere it stays at one at a time and says "parallel N
reduces waiting for several users but costs about 10-25% speed per request on this card". --parallel N is
honoured as asked, as before. BATCHING.md: the table with the change against one at a time.
2026-10-04 03:36:22 +02:00
Niko1221 2cc21100b4 #465 serve/test_parallel: the first request decodes long enough to overlap the others on a busy PC 2026-10-04 03:34:23 +02:00
Niko1221 1c219eb984 #465 docs: the final build's RTX 5070 numbers; the turn-checkpoint exactness check 2026-10-04 03:14:23 +02:00
Niko1221 de19c046f0 Merge S26 f-halo-int (STRATA_LFUSE, b08cf3e) with S23 STRATA_PF_PAD (bedb808): c-best3 v3 2026-10-04 01:13:49 +00: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
Niko1221 bedb808741 prefill: STRATA_PF_PAD=1 - the S23 prompt GEMM rows padded to stride K + 64, bitwise
A 4 KB-multiple row stride (K 2048 / 4096 / 6144 halves) camps on the memory channels: the gfx1151 WMMA prompt
GEMM ran ssm_out (N 2560, K 6144) at 19 TFLOPS against 31-32 for K 2560. Padding the rows by 64 halves (gemm_probe8,
M 16384): K 6144 19.1 -> 35.0 TFLOPS (W alone 29.0, X alone 23.7), K 2560 31.7 -> 36.6.

With STRATA_PF_GEMM=1 on chunks of STRATA_PF_SWITCH_MIN_T+ tokens: Gemm::native dequantizes the weight into the
scratch with stride K + 64 (dequant_f16_ld, non-i-quant types) and runs strata_pf_gemm_f16_ld; the FP16
activations of the two K 6144 projections (gdn_recurrence's y16 for ssm_out, gate_attn's out16 for attn_output)
are written with stride 6144 + 64. The same products in the same order: the same bits.

c-best3 v2 + STRATA_PF_PAD=1 (Aurora, interleaved, same generated ids): prompt 8K 954.2 / 930.7 -> 983.7 / 940.8,
64K 947.0 / 946.2 -> 974.8 / 1014.4 tok/s.
2026-10-04 03:00:48 +02:00
Niko1221 b08cf3e160 verify: fewer launches around the shared expert and the KV append, bitwise (opt-in STRATA_LFUSE=1)
Per MoE layer of a verify window (2-8 rows, resident combine path):
- the shared expert's scalar gate (a 1-row BF16 GEMV, one latency-bound
  workgroup) rides as one more block of the router GEMV launch
  (bf16_gemv_fp32_mmvf_multi_aux; the rows kernel's per-output code is the
  multi kernel's - checked bitwise for 2-8 rows, with and without TSUM)
- its sigmoid + row scale move into the combine (combine<true, true>: the same
  sigmoid expression, the product rounded on its own with contraction off in
  that block only, then added as before)
- shared gate and up projections in one launch (native_mmvq_pair: blocks
  n_out.. take the second matrix; Q8_0, exact multi layout, not packed)
Per attention layer: the window's int8 KV appends in one launch
(kv_append_q8_steps, blockIdx.z = token x K/V; distinct cells).
-4 launches per MoE layer, -(n-1) per attention layer; STRATA_LFUSE_GATE=0 /
STRATA_LFUSE_PAIR=0 turn the gate / pair parts off.

Engine (gfx1151, UD-IQ4_XS, c-best3 + EXPERT_V2 + Q8_PACKED + TSUM, S25's 18
prompts, same binary): 51.29 -> 50.31 ms per verify window, 54.33 -> 55.12
tok/s; 59K prompts 54.19 -> 53.17 ms, 42.97 -> 43.79 tok/s; all outputs identical.
2026-10-04 00:41:01 +00:00
Niko1221 b4ad373266 #465 engine: INFO slot_cache=0|1 beside batch_slots (the server sends a lone slot request back to the solo path only when the slots keep their conversations) 2026-10-04 02:38:22 +02:00
Niko1221 038430ff20 #465 a request left alone in a slot goes back to the solo path; docs: BATCHING.md for one GPU, measured on the RTX 5070
server: a request decoding in a slot whose neighbours finished (no other slot busy, nobody waiting, at least 32
tokens allowed) is BSTOPped and continued on the solo path with its MTP drafts - the engine copies the slot's
sessions back (INFO slot_cache=1; at most twice per request; STRATA_PARALLEL_SOLO=0 keeps it in the slot).
engine: INFO batch_slots=N slot_cache=0|1.
tools/batch_interleave_test.py: the back-to-solo path, and a next turn from a slot's turn checkpoint (a client
that drops the reply's thinking).
docs: "parallel": N, what a slot costs and what setup recommends, the server's handling (interleaved prompt reads,
BYIELD, slot conversation cache, back to solo), exactness and its settings, the RTX 5070 measurements (the slots
buy waiting time, not speed, on a 12 GB card), the protocol; DETAILS / README / AI_SETUP mention the option.
2026-10-04 02:38:16 +02:00
Niko1221 a184ac914e docs: Older CPUs (experimental) - what setup does on a CPU without AVX2, what was measured
INSTALL.md gets the section (the build setup compiles, what differs, the forced-path numbers from a
Ryzen 5 7600 + RTX 5070, what is untested); the CPU rows of INSTALL.md / DETAILS.md and AI_SETUP.md
point to it.
2026-10-04 02:13:51 +02:00
Niko1221 b6b7a87d84 prefill: STRATA_PF_SWITCH_MIN_T=N - the S23 rounding-level prompt switches only on chunks of N+ tokens
STRATA_HC_UPMIX, STRATA_PA_FAST and STRATA_PF_GEMM change the prompt path's rounding; on short prompts their time
gain is negligible but they change the generated text.  With N set, chunks shorter than N keep the default
arithmetic (and outputs) while long prompts keep the gain.  Unset or 0: every chunk, as before.

gfx1151, c-best3 + STRATA_PF_SWITCH_MIN_T=4096: S25's 18 prompts (1.0-1.9K tokens) give outputs identical to
c-best2 v2 on 18 of 18 (49.28 tok/s; c-best3 without the gate 48.83 / 48.67, other texts).
2026-10-03 23:45:54 +00:00
Niko1221 94e4b623c0 #465 tools: batch_interleave_test.py (a prompt read beside decoding slots, BYIELD, a next turn from its slot); batch_test.py on Windows
batch_test.py: the engine log in the temp folder, the CUDA DLLs on PATH, ASCII output, --mt-min (empty: the
engine's default kernels, for the invariance check without the exact settings).  server: /metrics live.waiting
and live.outside_slots instead of a guessed count.
2026-10-04 01:32:46 +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 d0f2900027 #465 setup: --parallel N, and the count it recommends for the card
The slots' sessions may take a fifth of the expert cache (~VRAM less 5 GB), up to 4: 2 on 12 GB, 3 on 16 GB, 4
from 24 GB, none below. Without --parallel setup only says it once; with it the config gets "parallel": N and a
count above the recommendation is kept with a note.
2026-10-04 01:18:02 +02:00
Niko1221 3afe0e5092 #465 server: "parallel": N, slots in /metrics, one status per request, a long prompt gives way to a short one
- "parallel": N in the run config passes --batch N (a value the engine cannot use is said and passed on: the
  engine warns and adjusts); the server uses the slot count the engine reports (INFO batch_slots)
- every running request keeps its own status, rate window and engine figures (history rows are their own);
  /metrics live.slots shows each slot (idle with the tokens it holds, reading, decoding with tok/s),
  /v1/status concurrency.serving says N
- a conversation's next turn goes to the slot that holds its start, a new one to an empty or the least recently
  used slot; a slot left at a stop token keeps what it holds
- a long prompt read (solo or an admission) gives way (BYIELD) to a waiting request with a prompt under half as
  long, at most twice; the part read waits in a reserved slot and the request goes on once the other is in
- the engine start and POST /v1/vram are serialized against parallel requests
- serve/test_parallel.py: a scripted engine behind the real StrataEngine and Service
2026-10-04 01:18:02 +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 005f7c7185 gfx906 build on 0.1.39: the segmented expert cache (#533) is CUDA-only there too; pinned.cu's std::max beside windows.h
PR #638 was cut from 0.1.38. 0.1.39's segmented expert cache (#533) keeps its cuMem* code out of HIP builds through
STRATA_USE_HIP, which the gfx906 compat build does not define, so it included <cuda.h>. Its virtual-memory blocks are
now behind STRATA_EC_NO_VMM (STRATA_USE_HIP or STRATA_HIP_GFX906); the CUDA and RDNA preprocessor output is unchanged.
(std::max) keeps windows.h's max macro out of two Windows-only lines (found by a Windows gfx906 compile check).
2026-10-04 01:01:09 +02:00
Niko1221 75c9fc6baa HIP: STRATA_TSUM also spreads the expert down rows' items evenly over the lanes, bitwise
s26_down_l_kernel (STRATA_EXPERT_V2): a warp's 4 rows have 40 (block, half)
items each; lane-strided per row, lanes 0-7 took 2 items of every row (8
loads) and lanes 8-31 one (4). Under STRATA_TSUM=1 the 160 items are spread
5 per lane (item p = 32 i + lane is row p / 40), all 5 loads issued before the
dots. A lane's items of a row are then exactly those of the per-row layout's
lane (l - 40 q) mod 32, in the same order, and an xor butterfly gives the same
bits under any rotation of the lanes (each level's partner differs by +-2^j,
which is +2^j modulo 2^(j+1)): every output is bitwise the same (ws3 harness,
memcmp, T 1-3: 1.09 / 1.11 / 1.12x for gate/up + down with TSUM, vs 1.03 / 1.05
/ 1.06x with the butterfly alone). The same spread for gate/up (80 items x 4
rows, 10 per lane in 1-3 load groups) was 0.6-0.8x and is not used.
2026-10-03 22:59:47 +00:00
Niko1221 2b115071ed docs/INTEL_ARC.md: the experimental Intel Arc engine - what you need, the build, what is tested and what is not 2026-10-04 00:50:48 +02:00
Niko1221 bd8371adcd prefill gemm: STRATA_BF16_TC=2, a test mode that runs the FP16 path on any card (gemm_bf16_parity on sm_80+) 2026-10-04 00:49:02 +02:00
Niko1221 2a1c54e7da HIP: several warp sums as one transposed butterfly, bitwise (opt-in STRATA_TSUM=1)
A warp that ends with P independent warp sums (P tokens of a row, or rows x
tokens) used P xor butterflies of 5 shuffles each. s26_tsum.cuh does them in
one transposed butterfly: at offset 16 a lane keeps half of its values and adds
the partner's copy of that half, at 8 half again, ... then plain xor steps.
Every level adds the same pair of values the butterfly adds (fp add commutes),
so each sum is bitwise the same; P - 1 + (5 - log2 P) moves instead of 5 P, as
DPP row_xmask / permlanex16 on AMD (__shfl_xor_sync elsewhere).

Applied under STRATA_TSUM=1 to the decode kernels that end in several sums:
- HC read (STRATA_HC_Q8): gr_down_q8 / gr_up_q8 (ws4 harness, every output
  equal, T 1-4: 1.00 / 1.03 / 1.03 / 1.15x per read)
- STRATA_EXPERT_V2 gate/up and down (ws3 harness, memcmp, T 1-3:
  1.03 / 1.05 / 1.06x)
- native_mmvq_multi_kernel (exact layout) and the Q8_0 / Q6_K packed kernels
- bf16_f32_mmvf_rows_kernel (STRATA_MMVF_ROWS), both reduction stages

Engine (gfx1151, UD-IQ4_XS, c-best3 + EXPERT_V2 + Q8_PACKED, S25's 18
prompts): 54.21 -> 52.10 ms per verify window, 51.31 -> 53.73 tok/s, all 18
outputs identical.
2026-10-03 22:47:42 +00:00
Niko1221 3984194646 #646: the single-matrix mmvq does not stage the IQ grids (10-20% slower on the RTX 5070 at 3-8 columns)
The grouped expert kernels keep the staging (they gain). STRATA_IQ_STAGE_GRID_MMVQ=1 stages mmvq too; the
results are bitwise the same either way.
2026-10-04 00:36:02 +02:00
Niko1221 5ff6ae7530 setup: --backend sycl tests; Windows points at Linux (setup_intel reads sysfs, which WSL has not) 2026-10-04 00:34:13 +02:00
Niko1221 1330ed56d4 #646 on 0.1.39b: the layer split and the CPU path as before, the resident plan for 8-token windows
- layer split: commit stays synchronous on a split (set_commit_async(!multi_gpu), the `|| next_ != nullptr`),
  no pre-staging of the next stage's inputs, kDrafterMib 1000 - the split's sizing and order as in 0.1.39
  (untested here: one GPU);
- the CPU expert kernels keep cpu_avx512_ok() (F/BW/VL/VNNI/VBMI): #646's AVX-512BW-only check would send a
  CPU without VNNI/VBMI to kernels that use them;
- resident_plan_kernel: one thread per window entry up to 128 (64 lost a 7- or 8-token window's last entries
  on the all-resident path);
- the host keeps zeroing the GPU's rows under STRATA_VERIFY_DEVICE_PLAN=1 (copy_or_zero_from_mapped copies
  every row there).
2026-10-04 00:22:59 +02:00
Niko1221 de231f52d2 sycl: strata-sycl.sh passes ONEAPI_DEVICE_SELECTOR into the container
The image pins level_zero:0, so a two-card layer split could not see the second
Arc (#423, tmking01). Set ONEAPI_DEVICE_SELECTOR=level_zero:* for it.
2026-10-04 00:08:16 +02:00
tmking01 f420ee2dd1 sycl: stage_room reads the free VRAM (a layer split's later GPUs had none)
dpct left the DPCT1106 note but dropped cudaMemGetInfo in generate.cpp's
stage_room, so every later stage of --layer-split sized its expert cache from
0 bytes free and the second card crashed with 'expert cache: no room'.
Reported with the fix by tmking01 in #423 (2x Arc Pro B70); also a fixups.py
entry so a re-migration keeps it.
2026-10-04 00:08:16 +02:00
Niko1221 5ffcce4382 sycl: port 0.1.39 (re-migration with dpct 2025.3, 3-way merge per docs/INTEL.md)
The SYCL port of PR #423 tracked 0.1.38; 13 of its migrated copies were behind
0.1.39's sources. Re-migrated 0.1.38 and 0.1.39 with sycl/tools/migrate.sh,
normalized, merged with sycl/tools/merge_upstream.py:

- clean merges: expert_cache, expert_source, native_dense, pinned, elementwise,
  kv_stream, iq_multi_parity, prefill, verify.hpp; q8_1_finite.hpp new (#606)
- verify.cpp: the #649 trace kept with the port's queue handling
  (STRATA_VERIFY_TRACE; the HIP-only coherent/doorbell switches stay HIP-only)
- generate.cpp: the port's --prefill-borrow beside --vram-elastic/--vram-segment-mib
- iq_kernels / native_mmvq: the port's kernels kept, #606's finite q8_1 scale
  and sum applied by hand (dpct's conflicts there were kernel-name hashes)
- expert_cache: --vram-elastic (#533) is CUDA-only on the port too (no driver
  VMM on Level Zero through dpct); it reports so instead of failing to build
- version 0.1.39

Compiled here with icpx 2026.1.1 (SPIR-V, no Intel GPU on this PC): strata
builds. Not run on an Arc.
2026-10-04 00:08:15 +02:00
Niko1221 335f742228 setup: the offer to share a model across two cards checks the model's own engine (CUDA 12 or 13) 2026-10-03 23:46:27 +02:00
Niko1221 7990455358 docs: Older GPUs (experimental) - the support matrix, the CUDA 12 engine and how to opt in
docs/OLDER_GPUS.md: Pascal / Volta through the CUDA 12 engine (#395 #600 #540 #655), RTX 20's STRATA_BF16_TC opt-in,
AMD gfx1031 (#524), gfx1012 (#442) and gfx906 (#638, #677) with the reporters' numbers; README, AI_SETUP, AMD_HIP and
NVIDIA_V100 point to it.
2026-10-03 23:44:14 +02:00
Niko1221 7c5a71adf7 router_dot_parity: the router lookahead's AVX2 and AVX1 dots against a double reference
Random BF16 rows of the router's shape [512 x 2560], 1..8 tokens: both within 3.3e-7 of the double
sums on a Ryzen 5 7600 (0.53 ms and 0.50 ms per layer for 4 tokens). Two ctest entries, the second with
STRATA_FORCE_ISA=avx (the AVX-only CPU's path). Test program only.
2026-10-03 23:44:03 +02:00
Niko1221 6234f26783 setup: older NVIDIA GPUs through a second, experimental CUDA 12 engine; --cuda 12|13 (#395 #627 #540 #600 #601)
CUDA 13 dropped Pascal and Volta. A model whose GPUs include one (its oldest card decides: one engine runs per model)
now runs the experimental CUDA 12 engine in engine-cuda12/ - the ready-made strata-windows-x64-cuda12.zip with NVIDIA's
CUDA 12 pip libraries, or compiled here with a CUDA 12.x toolkit (Linux; STRATA_NVCC picks one, #601) - and setup says
why. engine/ and every model on it stay as they were.
- the cards are an opt-in: naming one (--gpu / --gpus), --cuda 12, STRATA_EXPERIMENTAL_SM60=1, or a PC with no newer
  NVIDIA card and no supported AMD card; a PC with a newer card keeps recommending it
- --cuda 12|13 (or STRATA_CUDA) overrides the choice and is kept, with a warning where it cannot work (recommend,
  never force); --cuda 12 also runs with a driver older than 580 (525+), which the stop now mentions
- at a start, a Pascal / Volta card added to a CUDA 13 model moves that model's config to the CUDA 12 engine
- updates look at both engine folders; a compiled CUDA 12 engine builds in build-cuda12/
- an RTX 50 card in a CUDA 12 engine is a warning (CUDA 12.8+ builds it; #220 crashed with 12.8), no longer a stop
The setup golden test (every recommended config on five PCs) is unchanged; tools/test_setup_older_gpus.py covers the rest.
2026-10-03 23:42:23 +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
Niko1221 f4dc113ee1 iq_avx2: STRATA_IQ256_GATHER=0 keeps the scalar grid assembly (parse the value) 2026-10-03 23:34:41 +02:00
Yannick Loth 0736f05d8a iq: gather the AVX2 codebook from the grid table (STRATA_IQ256_GATHER)
The two IQ3 gate/up formats decode each 32-value half with eight scalar loads
assembled by _mm256_set_epi32.  A scratch build that substitutes a constant for
the grid vector shows that assembly is ~half the single-core kernel (IQ3_XXS
248 -> 130 us, IQ3_S 357 -> 140 us), and the two formats are ~58% of the
gate/up lookup work on the Swift 1.5 IQ3_XXS pack.

Add an opt-in AVX2 gather for IQ3_XXS (iq3xxs_grid) and IQ3_S (iq3s_grid),
the mirror of the AVX-512 kernels' STRATA_IQ_GATHER.  On the i7-12850HX
(Alder Lake, no AVX-512) it is bit-identical - a direct scalar-vs-gather output
dump matches for all five gate/up formats, native_expert_parity stays green -
and 1.32-1.34x on the two formats isolated, +1.0% serve end to end (on >= off in
5 of 5 interleaved pairs).  The pool is flat in the deterministic --stats (the
15-worker pool is not decode-bound), so it is a per-core win that compounds.

Default off, like the AVX-512 one: AVX2 gather throughput is microarchitecture-
dependent.  IQ2_XXS / IQ2_XS / IQ2_S use 64-bit grids, where an AVX2 i64 gather
measured a wash, and are unchanged.  The AVX-512 path is untouched.
2026-10-03 23:34:26 +02:00
Anon 8ca4e160c2 pinned: back the Linux expert arena with transparent huge pages
Without a hugetlb pool the Linux arena fell back to a plain anonymous
mapping with 4 KB pages. The CPU expert pool streams whole experts (about
3 MB each for the 4-5 bit GGUFs) out of that arena, so every expert costs
~750 TLB misses.

Map the fallback 2 MiB-aligned and madvise(MADV_HUGEPAGE), so THP in
"madvise" mode (the common distro default) backs it. STRATA_NO_LARGEPAGES=1
keeps 4 KB pages, as on the hugetlb path; the log note says which one it got.
2026-10-03 23:34:13 +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
JeanP00l dc0f2f7916 bench: community report, 2x AMD Instinct MI50 (gfx906), Coder IQ1_M, 4K to 128K prompt tokens 2026-10-03 23:33:55 +02:00
David ff12cbe2b9 hip: retain native wave synchronization on HIP 7 2026-10-03 23:33:46 +02:00
David e9a5f8d2b8 hip: preserve Q2_0 signed zero on gfx1012 with legacy HIP 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 0afba54b4c fused_gr: the gfx906 fast norm/up launches only in the gfx906 build (CUDA and RDNA did not compile) 2026-10-03 23:33:00 +02:00
JeanP00l c330ec76cf docs: gfx906 verify-window routing in the kernel table 2026-10-03 23:33:00 +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 6b8b54963b gfx906: K-quant/Q5_1/Q8_0 experts take the CUDA layout; gr_down K-split opt-in; gfx906-aware ctest
- native_expert_grouped: the AMD layouts handle the IQ packs' gate/up and down types only; Unsloth's Q4_K / Q5_K
  gate/up and Q5_1 / Q8_0 down now take the CUDA layout instead of exiting (native_grouped_parity found it).
- fused_gr_read_multi: the K-split gr_down (fixed-order finish) sums in another order than the single-token
  fused_gr_read, so it broke gr_parity's multi == single contract; it is now opt-in (STRATA_GR_SPLIT=1). The
  latency-hidden norm/up (same sums, same order) run in the default read (STRATA_GR_FAST=0: off).
- mmvq_multi_parity: on gfx906 the wave64 MMVQ is the same exact kernel for every column count, so the negative
  control has nothing to find; a second ctest entry runs it on the CUDA layout (STRATA_MMVQ_WAVE=0), where it has
  power.
- kv_hybrid_parity: the tensor-core prompt attention refuses on gfx906 as on the RDNA backend.

ctest on 2x MI50 (gfx906): 34 of 34 pass (iq_parity and ple_parity need fixture files and are not counted).
2026-10-03 23:33:00 +02:00
JeanP00l 8e8e641245 docs: gfx906 (Instinct MI50 / MI60, Radeon VII) build, kernels and measurements in AMD_HIP.md 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 bbb0650993 QSA attention: PR #540's kernel below sm_75 in the experimental build only
#540's attention kernel (bit-exact, +7% prompt on a V100) uses about twice the registers and also runs decode, so it
is not taken for every card: the kernel the ready-made engine runs is restored as it was, and #540's version lives
beside it as attn_chunk_kernel_pre75, compiled only with -DSTRATA_EXPERIMENTAL_SM60=ON and launched on a card below
sm_75 (per device; STRATA_ATTN_PRE75=0 is the A/B arm).  The launches pass cap/stride explicitly (the same 0, 0 as
the defaults) so either kernel can be picked through one pointer.
2026-10-03 23:30:33 +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
Károly Sárdi 00e56870b4 perf(volta): address review - keep BF16X2 remainder GEMMs on cuBLAS, keep Inf, share sq with spt, atomic cc cache 2026-10-03 23:29:14 +02:00
Károly Sárdi 6a4b1265c6 perf(qsa): cut the attention kernel's shuffles and q shared-memory traffic, bit-exact 2026-10-03 23:29:12 +02:00
Niko1221 ef18ede418 cmake: STRATA_ENABLE_SYCL, the experimental Intel Arc engine (sycl/, off by default) 2026-10-03 23:29:11 +02:00
Niko1221 f13b869b6b Volta prompt attention and the Pascal BF16 path: compiled into the experimental build only (PR #600, #395 follow-up)
STRATA_EXPERIMENTAL_SM60 now reaches every target (add_compile_definitions). prompt_attn_v70_kernel with its launcher,
and Gemm::bf16's fp32 path for Pascal, exist only under it: the ready-made engine (sm_75+) loads no extra kernels for
them, while the experimental build's sm_60/61/70 cards get them.
2026-10-03 23:29:00 +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
Klaus Friedel 174ce924de docs: point the BF16 note at #540 (the BF16-via-FP16 PR #593 was closed in its favor) 2026-10-03 23:28:14 +02:00
Klaus Friedel 5e1926070c bench: distribution check of the Volta prompt attention (teacher-forced KL, controls)
Old path against new kernel on 8.5K / 33K / 99K-token chats, 617 scored positions each, with two controls that change only the rounding
order (BF16 GEMMs without the FP16 detour, another prefill chunk), reruns of both paths (bit-identical), and the kernel's own error on the
model's data (relative 4.4e-7 mean, 4.8e-6 worst row over ~5 M rows).
2026-10-03 23:28:14 +02:00
Klaus Friedel de9a78eed4 docs: NVIDIA_V100.md and the measurements of the Volta prompt attention
The experimental sm_70 build (STRATA_EXPERIMENTAL_SM60, #236) had no page. NVIDIA_V100.md says how to build it, which kernel each
part of the prompt path uses on Volta and what the prompt attention kernel was measured at.

bench/results/2026-10-03-v100-prompt-attn: prompt speed with and without the kernel (7K / 28K / 114K tokens), the nsys share of the
attention before and after, the parity numbers, needle recall (15 of 15 with either path) and the greedy-output comparison.
2026-10-03 23:28:14 +02:00
Klaus Friedel 649fc7fcad qsa_prompt_attn: tensor-core kernel for Volta (sm_70, mma.m8n8k4)
On sm_70 qsa_prompt_attn_batch refused the device and the prompt path fell
back to the decode kernel (FP32 FMAs, one query at a time): 19-25% of the GPU
time of a 32K prompt on a V100.

prompt_attn_v70_kernel is the v1 kernel's structure with the two matrix
products on m8n8k4 (FP16 in, FP32 accumulate): q.k with one dim group (64 dims,
= one int8 scale group) per warp and 8 cells per quad-pair, p.v with 16 dims per
quad-pair. q and p keep their hi + lo FP16 split, the four dim groups' partials
are added in a fixed order (deterministic). Int8, FP16 and K8V4 KV are handled;
Q4_0 KV (mode 4, sm_80+ only upstream) keeps the old kernel. The fragment layout
was measured on a V100 and matches turbomind's SM70_MMA_884.

qsa_prompt_attn_parity (V100): error against FP64 ~5e-6 (output scale ~3.6),
int8 3.0x and fp16 1.3x faster than the FP32 kernel per 2048-query chunk. The
test now skips its Q4_0 cases below sm_80 instead of exiting 2 there.
End to end, 32K prompt, one V100, greedy output identical:
26 s -> 22 s on current main (STRATA_PROMPT_ATTN_OLD=1 is the old path).
2026-10-03 23:28:14 +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
Demetree Kallergis 0f476b4553 native experts: gate the AVX2 multi-token kernels on cpu_avx2_ok() (#394)
native_gu_rows() tested cpu_avx512_ok() before the AVX-512 kernel but not cpu_avx2_ok() before the
AVX2 one, and native_down_rows() had the same asymmetry for its two multi-token kernels (kq256 and
the IQ4_NL one). On a CPU without AVX2 a verify window of two or more tokens took an /arch:AVX2 path
and died with 0xC000001D. With the gates it falls through to ggml-cpu's vec_dot, which covers every
native type for any nt. A CPU with AVX2 takes exactly the paths it took before.

(Cherry-picked from #394's 0083bb7 without its generate.cpp part; the startup gate is reworked for the
opt-in STRATA_ISA_FLOOR build in a later commit.)
2026-10-03 23:27:51 +02:00
Niko1221 d072203bf9 iq_avx2: the IQ2 sign table at compile time (no static constructor in an AVX2 file)
#391 removed MSVC's BMI2 shlx from the table's constructor; GCC still vectorised the same loop into
AVX2 (vpbroadcastb) and ran it before main() on an AVX-only Xeon E5 (found by rwkeyes' Strata_Dirigo
fork, 6dd37a2, which made it constexpr). constexpr here as well, deriving ksigns_iq2xs[i] (the 7 bits
plus an even-parity bit) so the constant expression does not read a non-constexpr array. Same 128
values; the file now has no runtime initializer.
2026-10-03 23:27:37 +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
Niko1221 241dcab7ac setup: --backend sycl, the Intel Arc engine (experimental, built from source; sycl/setup_intel.py) 2026-10-03 23:27:21 +02:00
Demetree Kallergis 30d2b5f07b iq_avx2: build the IQ2 sign table without a variable shift
A namespace-scope static initializer shifted a byte into place with << (8 * k), which is a
variable shift.  MSVC compiling this file with /arch:AVX2 emits BMI2 shlx for that - it assumes
anything with AVX2 also has BMI2, true from Haswell on and false of everything before it.

Because the table is static, the constructor runs before main(), so this faulted at PROCESS START:
strata.exe exited 0xC000001D with no output at all, not even for --help.  One shlx in the whole
linked binary was enough to make the engine unstartable on any pre-Haswell CPU under MSVC.

Storing the byte directly is the same 64-bit value on the little-endian targets this file is for,
and leaves no variable shift for the compiler to widen.

Found by building the engine for sm_61 on a Sandy Bridge-E (Xeon E5-2680) with
STRATA_EXPERIMENTAL_SM60=ON, which 0.1.31 added (#236).  Verified by disassembly: no shlx/shrx/sarx/
mulx/andn/bzhi/pext/pdep anywhere in strata.exe afterwards, and --help now runs.

This is independent of any AVX1 work - it is a plain startup crash on the CPU this was built on.

(cherry picked from commit 3fcaf03840)
2026-10-03 23:27:07 +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
Niko1221 37769d3aa1 setup: gfx1031 (RX 6700 XT) wording and a test (PR #524 follow-up) 2026-10-03 23:26:57 +02:00
Matt 94db2c877d forgot one spot to add in setup.py 2026-10-03 23:26:24 +02:00
Matt c198d8239a Added gfx1031 experimental support 2026-10-03 23:26:24 +02:00
Niko1221 088937b978 Merge PR #423 (maxfridbe): Intel Arc SYCL port of the engine, experimental 2026-10-03 23:26:15 +02:00
maxfridbe 63024cd848 sycl: the stream-all prompt walk by default when the VRAM holds >90% of the experts
It hung on the B70 until the stager/PLE event-wait fixes; it runs since and reads
Coder prompts faster (40K 1,106 -> 1,160, 128K 897 -> 920, 128K k8v4 1,006 ->
1,041, 256K 760 -> 780 tok/s; same output). It copies every non-resident expert,
so with a big host tier it loses (IQ2_XS, a quarter in the RAM mirror: 2,184
tokens ~560 -> 254 tok/s): generate.cpp hands the non-resident share to the
prompt path, which walks only below 10%. STRATA_PREFILL_STREAM_ALL=1/0 forces it.

Docs: the 40K prompt breakdown, and why the cluster kernels and Q4_0 XMX prompt
attention are not worth porting here; the 2K "regression" was session variance
(old and new builds A/B'd: 784-799 tok/s both).

Tested on the B70: Coder 2,184-token and 40K, IQ2_XS 19- and 2,184-token
outputs identical; IQ2_XS 562 tok/s (stream-all off by its share), Coder 40K
1,160 tok/s.
2026-10-03 16:32:22 -05: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
maxfridbe b7760573ec sycl: port upstream 0.1.38
3-way merge from the port's "main + PRs" migration to dpct of upstream 0.1.38
(prefill.cpp re-merged with the histogram diff: upstream's peer-device and fused
restructure kept, the port's edits carried over - the stager's GGUF job, the
stream-all gate, the fp16 NaN checks; generate.cpp: the pipelined profile fill
beside upstream's prefetching serial fill; mtp/verify: the SYCL host mapping).

- the PRs' header forks dropped: upstream has them now
- stubs: --peer-device (peer_experts.cpp; open refuses) and the fused int8 prompt
  kernels (moe_fused_stub.cpp; MMQ is not built here)
- off on SYCL: the sm_90 cluster greedy sampler and QSA top-k (fixups.py)
- fused_gr: dpct's undeduced kernel names filled in (fixups.py)
- STRATA_GDN_KEYHEAD stays opt-in on the port (upstream's gate is NVIDIA SM counts)
- version 0.1.38

Tested on the B70: build clean; native_grouped, iq_multi, gr, qsa, kv_hybrid,
quantize_act parity pass, qsa_prompt_attn's int8/fp16 pass (its new Q4_0
tensor-core cases are refused: not in the port); Coder short/long, IQ2_XS, 40K
outputs identical; 128K int8 KV streaming 892 / 66.8 tok/s; GR_DOWN_MAX4 neutral,
GDN_KEYHEAD 8% slower (stays off).
2026-10-03 15:13:06 -05:00
maxfridbe 733590721d Merge upstream main (0.1.38) into b70 2026-10-03 14:39:37 -05:00
benoit lange dbbc0d600e Layer split comment: KV streaming and images do run across a split
Reported on 2 x TITAN RTX (split 25, KV streaming, GPU image encoder, parking, --batch 4 --batch-groups 2).
2026-10-03 20:54:02 +02:00
Niko1221 73e71f40e0 Engine 0.1.39: the #577 file-tier regression fixed, the OpenAI Responses API (#451), #606 #620 #458 #533, PR batch; setup requires it 2026-10-03 20:05:44 +02:00
Niko1221 5fc49bdcac layer split trim: opt-in on HIP too (STRATA_STAGE_TRIM=1, PR #639)
The trim was on by default in the HIP builds. It now waits, as on CUDA, for the PR author's re-run of the release
branch on his cards; the default flips after that. --layer-split auto and one GPU were never affected.
2026-10-03 20:05:31 +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 46a4ef5cb2 Merge branch 'f-139-resp' into f-139-rel
# Conflicts:
#	serve/server.py
2026-10-03 20:04:17 +02:00
Niko1221 92ca0c7fbd Merge branch 'f-139-ux' into f-139-rel
# Conflicts:
#	docs/DETAILS.md
#	serve/server.py
2026-10-03 20:03:53 +02:00
Niko1221 ed3bd791ff Merge branch 'f-139-eng' into f-139-rel 2026-10-03 20:03:37 +02:00
Niko1221 19ff212d1c serve: a checkpoint that fails on the GPU sync says the GPU stopped responding (#613)
"saving a conversation checkpoint failed" was the only text when the checkpoint's
device sync failed - a GPU hang that Windows then resets (VIDEO_ENGINE_TIMEOUT), not a
checkpoint problem. The error now names it: "...: the GPU stopped responding before it
(<error>) - a GPU hang; on Windows the driver is then reset". Error text only.
2026-10-03 19:36:12 +02:00
Niko1221 fd5d665225 tools: conversation_restore_speed.py, decode after a conversation-cache restore vs live (#528)
Two private engines, the same conversation: the baseline keeps it live, the candidate
parks it (an unrelated request in between, a re-rendering client, or two long
conversations taking turns) and restores it. Each turn's greedy tokens must match and
its decode speed must be within --tolerance (15%) of the live turn's. Pure logic
tested without a GPU (tools/test_conversation_restore_speed.py).

#528 did not reproduce on the RTX 5070 with it: Q2_0 37K tokens (--kv-resident 16384)
and IQ3_XXS 50-75K tokens (--kv-resident 32768) decoded at the live turns' speed after
a restore, with the same tokens.
2026-10-03 19:29:26 +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 fc38595c65 setup: recommend the hybrid worker count only with more E-cores than P-cores (#642)
docs/AMD_HIP.md's gfx1030 report measured an i7-13700KF (8P + 8E) at 38-42 tok/s with
all 15 workers against 36 with 8, so a CPU with no more E-cores than P-cores keeps the
engine's own count; the E-heavy chips the fork measured (i9-14900KF: 15 of 23 faster)
get the recommendation.
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 1b3eac7f8a serve tests: the long fixture answer alternates two tokens (one repeated token is now ended at 256, #606) 2026-10-03 18:50:36 +02:00
Niko1221 0b48acea33 docs, setup help: --draft-vocab fr and its measured acceptance (#597)
IQ3_XXS on the RTX 5070, temperature 0, 256 tokens, 8 French prompts (topics outside the corpus), 2 English and 2 code
prompts, arms alternating, 2 passes each: drafts accepted in French 0.505 (en) / 0.507 (cjk) -> 0.597 (fr), higher on
all 8 prompts and in each pass; English 0.610 -> 0.629, code 0.772 -> 0.779. Decode speed was not measurable here
(builds ran beside it); the reporter measured 141 -> 158 tok/s in French on an RTX 5090.
2026-10-03 18:45:50 +02:00
Niko1221 a20a5d480e strata_mcp: UD-IQ4_XS in the built-in model table, as in setup.py (#621) 2026-10-03 18:40:42 +02:00
Niko1221 4e413b344e File tier: the #577 re-check's log line on Windows only (the unbuffered reads exist there alone) 2026-10-03 18:37:11 +02:00
Niko1221 5b735eccb2 setup: Unsloth's UD-IQ4_XS as an experimental choice (#621)
UD-IQ4_XS sits between IQ3_S and UD-Q4_K_XL: IQ3_S gate/up experts (IQ4_XS in one layer) with IQ4_NL downs (Q8_0 in
five layers), 59.5 GB of them, and UD-Q4_K_XL's dense side. The engine's kernels already take these formats; it was
packed (iq_pack.py --compat-bf16) and run on the Strix Halo, not yet on NVIDIA.

- MODELS["UD-IQ4_XS"] in the unsloth family: three shards pinned at the same revision 38bb39e with their sizes and
  SHA-256 (the Hub's LFS pointers), the RAM budget like UD-Q4_K_XL (budget, one GPU, no images, --compat-bf16 pack,
  no experts.bin), engine 0.1.38 or newer (the fused-layout fix for packs not every layer of which is covered).
- A model can have its own shard pattern and count (model_file / model_shards); budget_model() tells which Unsloth
  model a config runs, for the #498 multi-GPU messages and RAM need.
- The AMD question (#429) stays for UD-Q4_K_XL only (nvidia_only): UD-IQ4_XS's formats have AMD prompt kernels.
- --gguf-dir accepts UD-IQ4_XS files (#444's list says so).
- UNSLOTH_Q4.md: its files, what setup does, what is known (Strix Halo), and the BF16 rounding of the hyper-connection
  projections that every --compat-bf16 pack has (not changed here). MODELS.md: a paragraph.
- test_setup_unsloth: pins, budget, a mocked install, the engine check, no AMD question, the config read back.

UD-Q4_K_XL's install, its tests and every other model's config are unchanged.
2026-10-03 18:36:44 +02:00
Niko1221 8cd360707c web: the Model settings card's Save is the primary button (#564) 2026-10-03 18:35:47 +02:00
Niko1221andDavid 0703ec90e0 serve: the OpenAI Responses API, POST /v1/responses (#451)
Stateless, on the chat path: a Responses request becomes the chat template's
messages, tools and kwargs, runs through Service.run, and its reasoning, text
and tool calls come back as output items and the official typed SSE events
(response.created ... response.completed / incomplete / failed).

- input as a string or items (messages, reasoning, function_call,
  function_call_output, custom tool calls); instructions and leading developer
  messages make one system message; tool results are put in call order
- function, namespace (Codex's MCP/agent tools) and custom tools; hosted tools
  are left out with a log line
- reasoning.effort -> reasoning_effort, max_output_tokens, text.format
  json_schema/json_object through the structured output path
- reasoning returned as reasoning_text (and as opaque encrypted_content when
  asked) and replayed into the prompt, so Codex's next turn reuses the cache
- errors in the Responses format; same API key, Host and Origin checks as /v1
- a response.in_progress event every 15 s while a long prompt is read
  (Codex's idle timeout ignores SSE comments)
- docs: DETAILS.md section with a Codex config.toml

Based on #630 by CC-David-CC (the event assembler, namespace tools, call-id
ordering of results), without its opt-in flag, replay key and crypto
dependency, strict-function and JSON-format refusals, or the second
summary generation pass. Tested with Codex CLI 0.160.0 and openai 2.15.0.

Co-authored-by: David <[email protected]>
2026-10-03 18:35:35 +02:00
Niko1221 4546bf7c03 web: a Model settings view for documented run config keys (#564)
The About tab's Model settings card reads GET /config and saves through
POST /config: the sampling defaults, reasoning_budget_tokens,
fit_max_tokens, anthropic_thinking, effort_position, aliases,
idle_unload_s, lazy_load, engine_silence_s, api_monitor, open_browser
and --vram-reserve-mib (serve/runconfig.py). Every value is checked
first (one bad value changes nothing), only the named keys change, every
other key of strata-<model>.json stays as it is (as setup run again
keeps them, #629), the earlier file is kept as .bak, and the change is
used from the next start. POST takes JSON from Strata's own page only,
like /settings, and the API key when one is set; the network, key, MCP
and program keys are not editable there.
2026-10-03 18:35:02 +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 13d0e2f4a3 SECURITY.md: how to report privately, and what the server exposes (#544)
Private reports through GitHub's private vulnerability reporting; the
server's protections as of 0.1.38 (127.0.0.1 by default, the API key,
the Host and Origin checks, CORS, MCP tools and the request monitor
opt-in), with DETAILS.md for every setting. README links it.
2026-10-03 18:31:56 +02:00
Niko1221 a9fe9236bf web: a Conversation cache card in the Monitor tab (#596)
/metrics gains "conversation_cache": the parked conversations, their
bytes, parks/restores/evictions and the last switch, read from the
engine's own log lines ("conversation cache: parked/restored ...") from
the start of its current run on; the budget and slots from its INFO line;
and the prompt tokens reused in the last request and since the start.
The card shows the parked slots and memory when --conversation-cache-mib
is on, the reuse lines always, and how to turn parking on when it is off.
2026-10-03 18:31:20 +02:00
Niko1221 a74c61f608 Document the decode window profiler (STRATA_VERIFY_PROFILE + STRATA_DECODE_TIMING); --gpu-stages points to it on a native pack (#610)
--gpu-stages replays per-layer graphs, which a native (IQ) pack does not capture, and refused with "not captured
with the split". The serve path already times a native window's stages: STRATA_DECODE_TIMING=1 prints the
per-request decode split and STRATA_VERIFY_PROFILE=1 adds the GPU stages. DETAILS.md now says how to read them, and
--gpu-stages on a native pack says to use them.
2026-10-03 18:31:07 +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 29181ca203 docs: the layer split's per-card dense weights (PR #639), STRATA_ARENA_MMAP (PR #640), make_profile --reorder (#587) 2026-10-03 18:27:29 +02:00
Niko1221 c9dbd8603c Windows Pascal/Volta source build: link the shared CUDA runtime only (#585)
CMake links CUDA code with cudart_static by default and the targets also link CUDA::cudart (cudart.lib); MSVC with
CUDA 12.x (the STRATA_EXPERIMENTAL_SM60 build for sm_60/sm_70) then fails with LNK1169. That build (MSVC +
STRATA_EXPERIMENTAL_SM60, no CMAKE_CUDA_RUNTIME_LIBRARY given) now sets CMAKE_CUDA_RUNTIME_LIBRARY Shared, the
reporter's verified fix. The ready-made engine and every other build are unchanged. INSTALL.md: where github.com
cannot be reached, third_party/llama.cpp at the pinned commit can be put there by hand.
2026-10-03 18:26:04 +02:00
Niko1221 9eceb375ab serve + engine: opt-in "effort_position": "end" keeps the cached conversation when the effort changes (#458)
The effort's instruction is the first thing the template renders, so a
request that only changes reasoning_effort (or turns thinking off) read
the whole prompt again. With "effort_position": "end" in the run config
the server renders every prompt's start as the default effort's and puts
a low/medium effort in a short system turn right before the answer
(thinking off: just the template's empty thinking block); the rendering
is template-agnostic (history + turn + the generation prompt).

Engine: --tail-role-token ID (default -1, off) moves the turn checkpoint
in front of such a trailing turn, so it holds the conversation and not the
effort text; INFO reports tail_role_token. The server passes the id of
"system" only when the engine binary knows the flag, and says so when it
does not. Without the key nothing changes: the same prompt ids, no flag.
2026-10-03 18:26:01 +02:00
Niko1221 1d789a14c0 STRATA_ARENA_MMAP: write experts.bin only when the drive has room (PR #640 follow-up)
The first mapped start writes the whole arena (25 GB for the Coder) next to the pack. A drive without that much free
space filled up before the write failed. Now the start checks the free space first (the arena plus 2 GiB to spare);
without it, it says so and keeps the arena in RAM for this start. Linux opt-in only; nothing changes without the env.
2026-10-03 18:25:56 +02:00
Niko1221 f1281f7fd7 layer split trim: on by default in the HIP builds, opt-in on CUDA (PR #639 follow-up)
The trim was measured on 2x MI50 (gfx906) only; nobody has run it on NVIDIA multi-GPU, where CUDA-only paths could
still read another stage's dense weights on a card that no longer holds them. The HIP builds trim with explicit split
points as the PR does (STRATA_STAGE_TRIM=0 keeps the full copies); the CUDA build keeps the full copies unless
STRATA_STAGE_TRIM=1. --layer-split auto and one GPU are unchanged everywhere.
2026-10-03 18:25:56 +02:00
Niko1221 64b8d5dd31 setup: STRATA_NVCC picks the CUDA toolkit; CUDA_HOME is looked at; glibc 2.43 + CUDA 12.9 and gcc 15 documented (#601)
find_nvcc takes the newest toolkit it finds, so with CUDA 12.8 and 12.9 installed, 12.9 was always used - and 12.9's
math headers do not compile with glibc 2.43 (Ubuntu 26.04). STRATA_NVCC=<nvcc> is now the only one considered (a
warning when the file is missing, or when it is too new for the Pascal/Volta build). CUDA_HOME is a candidate like
CUDA_PATH. TROUBLESHOOTING.md: the two build errors and what to do (g++-14 for gcc > 14; CUDA 12.8 or 13.x for glibc
2.43). Without STRATA_NVCC nothing changes.
2026-10-03 18:25:30 +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 0d6e03fa0b docs: AMD_HIP.md - a calibration run by hand on Linux HIP is kept for its card (#566) 2026-10-03 18:24:06 +02:00
Niko1221 935ac87a54 --ple-io direct on a rotational disk: warn at start, setup keeps the table in RAM, the stall report counts disk waits (#605)
On a 7200 RPM disk the default --ple-io direct's random reads of the n-gram table stalled prompts past the 60 s
watchdog, and the stall report looked like any other engine stall.

- Engine (Linux): at start with --ple-io direct, the table's disk is looked up in sysfs (queue/rotational); on a
  rotational one it warns and recommends --ple-io ram or an SSD (not on WSL, whose virtual disk says rotational).
- Stall report (Linux): the engine's threads in state D and their wchan ("threads waiting on the disk (state D): 16
  of 35 (blk_io_schedule x16)"), with what that means.
- Setup (Linux): a model on a rotational disk gets --ple-io ram when the RAM holds the table beside the model,
  otherwise a warning that recommends an SSD. Nothing changes on an SSD.
- DETAILS.md: --ple-io direct is for SSDs (it reads unbuffered, not through the OS cache), and a troubleshooting row.
2026-10-03 18:22:16 +02:00
Niko1221 c16f31f850 serve: a literal <think> / </think> in a message is encoded as text (#537)
The tokenizer matches <think> and </think> as their special tokens even
in plain text (type 4, as llama.cpp does), so a user quoting "</think>"
handed the model a real end-of-reasoning token and the answer came back
empty. Before the template is rendered the tags in message text (user,
tool, system, assistant history, tool call arguments, tool descriptions)
are swapped for private-use marks; after it, those spans are encoded as
ordinary text (Tokenizer.encode's new plain= spans), the template's own
markers stay special. An assistant turn that opens with its own
<think>...</think> block keeps that block. A prompt without the tags is
encoded exactly as before; count_tokens uses the same path.
2026-10-03 18:21:55 +02:00
Niko1221 19dbffde75 setup: a calibration on Linux HIP names the AMD card and is reused for it (#566)
hardware_key, the settings file's key for a saved calibration, read the cards with nvidia-smi's list: on an AMD PC
every card was "?|0GB" (two different cards shared one calibration), and on a PC with an NVIDIA card too a HIP index
named that NVIDIA card or stopped setup ("there is no GPU N"). A HIP config's cards now come from amd_gpus, in HIP's
numbering, with the architecture in the name (an RX 7900 XTX and an RX 7900 XT are both gfx1100; a card without a
product name is known by its arch). Setup reuses a saved calibration on Linux HIP as on NVIDIA (setup_calibration);
Windows HIP keeps the defaults for now. The tuning is still offered on NVIDIA only - a `./setup.sh --calibrate` on
HIP runs it by hand, and each control is verified on HIP before setup offers it.

The NVIDIA key and the settings file's format are unchanged. Tests: tools/test_setup_amd.py CalibrationKey (7).
2026-10-03 18:20:33 +02:00
Niko1221 5c50fbbcf2 setup: --draft-vocab fr - the English/code subset plus the tokens of French text (#597)
French answers drafted poorly with the shipped subsets: 23.6% of the token occurrences of French text fall outside
draft_vocab_en.bin, and the draft head cannot propose those (no subset adds the accented words and word pieces).

- tools/draft_vocab.py: a coverage mode. --corpus FILE... adds to the base the most frequent tokens of the corpus that
  together cover --coverage (default 0.99) of its occurrences, and prints the share outside the base and the new list.
- data/draft_vocab_fr.bin: draft_vocab_en.bin plus 5,686 tokens (46,211 ids), from 10 French Wikipedia articles
  (France, Paris, Napoleon Ier, Langue francaise, Union europeenne, Litterature francaise, Cuisine francaise,
  Economie de la France, Revolution francaise, Seconde Guerre mondiale; 1.24 M characters, 326,926 tokens) at 0.99:
    python tools/draft_vocab.py --gguf <IQ3_XXS shard 1> --base data/draft_vocab_en.bin --corpus <the articles> \
        --coverage 0.99 --out data/draft_vocab_fr.bin
  Outside the list on that corpus: 0.74% (23.65% with en).
- setup.py: "fr" in DRAFT_VOCABS / --draft-vocab (~151 MiB head on IQ3_S, between en and cyrillic). The sha256 check
  knows it as a shipped subset, so a later --draft-vocab cjk/en replaces it and a subset made by hand stays.
- tests: tools/test_draft_vocab.py (the coverage mode), tools/test_setup_draft_vocab.py (fr replaces a shipped subset
  and back, keeps the en ids first in their order, its MiB estimate).

The default subset (cjk) is unchanged.
2026-10-03 18:19:49 +02:00
Niko1221 bb162e2edf serve: end a reply that repeats one token 256 times in a row (#606)
A broken state (or a model in a loop) can answer one token forever: #606 streamed 36,689 tokens of "!" for four
minutes, because a request without max_tokens gets the whole remaining context. The server now ends such a reply
at 256 identical tokens in a row with finish "length" and a line in its window saying what to do if it repeats on
a new request. "repeat_stop_tokens": N in strata-<model>.json sets the run (0 = off). Nothing changes for a reply
without such a run.
2026-10-03 18:18:49 +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
Niko1221andHardin22 ad0df801ae setup: recommend P-cores + half the E-cores as CPU expert workers on hybrid CPUs (#642)
On a hybrid CPU (Intel 12th gen+, AMD Zen 5 + Zen 5c) setup writes --pool-workers
N into the run config: the performance cores but the host loop's one, plus half
of the efficiency cores. An E-core runs the expert kernels ~2.2x slower and each
layer waits for its slowest part; Hardin22 measured 15 workers at 165 / 116 tok/s
against 106 / 84 with all 23 on an i9-14900KF (#642). A recommendation only: the
line is in the config, a saved calibration's measured count wins, and a CPU whose
cores are all alike gets the same config as before.

The engine's pool, with every core allowed (the default affinity), now orders the
P-cores first on a hybrid CPU, so a smaller pool runs on the P-cores and the first
E-cores rather than on whatever the OS numbered first (from Hardin22's fork).
Non-hybrid CPUs: unchanged.

Co-Authored-By: Hardin22 <[email protected]>
2026-10-03 18:17:18 +02:00
Niko1221 021f544c60 setup: --vision-tokens N, the most image tokens a picture becomes (#625)
Kept in the run config's vision.max_tokens (the server already passes it
to the encoder); a setup run again keeps it for the same encoder device.
More than the default is allowed with a note on the encode time. Without
the flag the config is as before. DETAILS.md: vision.mmproj can point to
a Q8_0 mmproj of this model.
2026-10-03 18:15:55 +02:00
benoit lange 2871ff4b68 Parking with a layer split: the draft ring with the last stage, on its GPU; guards; trim log
- The draft layer's K/V lives on the last stage's GPU (the drafter is loaded there); its save, ring restore and
  verification now run with that stage's image under that device, instead of with stage 0's image on GPU 0, where
  the ring restore kernel ran against another GPU's memory (seen in Niko1221/Strata#653, which places it the same way).
- Parking with a layer split on one GPU (--split-device 0) is refused at start: untested.
- The whole-session validate refuses an image with stage images; clearer message for stage parts in live.
- --trim-stage-weights says which layers each GPU loads, and fails when index.txt cannot be read.
- conversation_snapshot_test: the whole-session refusal; parking_test.py runs with STRATA_SNAPSHOT_VERIFY=1.
2026-10-03 18:14:59 +02:00
Yannick Loth 90d3251790 make_profile: --reorder so a trace can rank ahead of a complete base
tools/make_profile.py builds the ranking as the base's order, then the
trace's pairs, then the fill. The base is only appended to, so --base can
never move a pair the base already ranks - and the shipped
data/expert-profile.bin ranks all 24,576 pairs, which makes a trace a
no-op:

  wrote ...: 24576 ranked pairs (24576 from the base, 0 from the traces, ...)

So the documented way to seed a profile from --dump-routing (rank the
pairs your workload routes most) silently did nothing with the shipped
base; only --no-base changed anything, and that drops the base's ranking
entirely.

Add --reorder: rank the trace's pairs first (most frequent first), then
let the base fill the rest, then the fill. The default is unchanged. The
ranking moves into a rank_profile() so it can be tested without a model;
tools/test_make_profile.py covers the no-op, --reorder, --no-base, the
frequency/tie order and an end-to-end run over a complete base. On this
host's coding trace the same command now puts 5,808 trace pairs first.
2026-10-03 18:14:52 +02:00
Niko1221 9b6515b37c setup: --no-browser / "open_browser": false keeps the chat page from opening (#609, #631)
--no-browser (on a start or at setup) saves "open_browser": false in the
model's run config; start() and run-<model>.bat/.sh then leave --open
out, and the server ignores --open when the config says so (a start
script written earlier still has it). --browser undoes it. Without the
flag the config, the command and the script are as before.
2026-10-03 18:14:39 +02:00
Niko1221 35c2bf9d9d adaptive swaps: gather the pin spans only when they are pinned (PR #640 follow-up)
The adaptive swap loops called srcp->blob() for every swap to collect the spans pin_blobs locks, on every build -
pin_blobs is a no-op except on Linux AMD with STRATA_ARENA_MMAP=1. blob() is not free: it counts reads and the file
tier (--mmap-experts) may assemble the blob, a second time for the copy. pin_blobs_on() now guards the gathering, so
CUDA and the runs without STRATA_ARENA_MMAP do exactly what 0.1.38 did.
2026-10-03 18:14:32 +02:00
Niko1221 5fc2afa9ce layer_split: check it before the start, accept a JSON list, say the format with an example (#644)
"layer_split" is the first layer of each later GPU ("24,36,42" for 4 GPUs), not a count of layers per card. A
value such as "24,16,12,12" failed the engine's check after the start began, and a JSON list reached the engine as
"[24, 16, 12, 12]".

- server.py layer_split_value: "auto" (default), a comma string or a JSON list of one rising number per GPU after the
  first, from 2; anything else stops the start with the format and an example for that many GPUs.
- The engine's message says the same, with the value it got.
- MULTI_GPU.md and setup's --layer-split help say it.
2026-10-03 18:14:23 +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
Niko1221 464ca57d7f layer split trim: CUDA0 keeps every layer's router with --mmap-experts (PR #639 follow-up)
The file tier's routing-aware prefetch (RouterLookahead) copies all layers' ffn_gate_inp.weight from CUDA0's arena.
With CUDA0 trimmed to its own layers it found them missing and turned itself off ("the routers are not BF16 in the
arena"). CUDA0 now keeps the routers (~2.5 MiB a layer) when --mmap-experts is on; the other stages and the runs
without --mmap-experts trim as before.
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 e294e0109b setup: running it again keeps the run config's keys it does not write (#629)
A re-run for an installed model (another --context, say) rewrote
strata-<model>.json from scratch and dropped the user's sampling,
mcp_servers, mcp and other blocks. Setup now carries over every key it
does not write (and env/vision entries of the user's own, such as a Q8_0
mmproj), keeps the earlier file as strata-<model>.json.bak and says what
it kept and which hand-added engine options it did not. A copy set up
like an earlier install carries them over from that install's config.
A first install writes the same config as before.
2026-10-03 18:13:28 +02:00
Niko1221 502c7296c9 Load the native head and the logits before the expert arena; name the free VRAM when they fail (#620)
The native head's cudaMalloc ran after the expert arena had registered ~45 GiB of host pages. WDDM then refused it
on a 16 GB card with the desktop on the iGPU ("native head upload: out of memory" with GiBs free) - the drafter is
loaded before the arena for the same reason. Now the head and the logits buffer load right after the drafter, still
before the expert cache is sized, so the slots are the same. The head's, the drafter's and a split stage's head
failures print "(N MiB of M MiB VRAM free on this GPU)".

setup: a context of 64K or more whose KV streaming does not fit the RAM test now says so (it was silent, and a
regenerated config lost --kv-resident without a word).
2026-10-03 18:12:58 +02:00
David b6d60f01b8 serve: read the /load and /unload body before replying (#630)
The server replied to /load and /unload without reading the request body; on
Windows, closing a socket with unread data sends a TCP reset, so the client saw
a connection reset instead of the answer. Read the (bounded) body first.

Taken from #630 by CC-David-CC.
2026-10-03 18:12: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
Niko1221 b92b78b092 serve: a malformed "tools" value is a 400, not a dropped connection (#592)
openai_to_messages / anthropic_to_messages check that tools is a list of
named tool objects (a JSON-string list is decoded, as for messages) and
raise ValueError otherwise; the MCP name set skips non-objects too.
2026-10-03 18:09:27 +02:00
Niko1221 99fe8b2437 serve: restart() keeps 0.1.38's close() (PR #637 follow-up)
0.1.38's close() already ends the old engine and waits for it (QUIT, terminate, kill, 20 s each) and raises
EngineStuck when it cannot. The PR's second kill and 120 s wait, after which a new engine was started next to one
still alive, is left out; the start retry and the last known context stay.
2026-10-03 18:09:15 +02:00
JeanP00l 67f4dc72bf serve: restart a dead engine for real - wait for the old process, retry the start, keep the last known context
Seen in long agent sessions (Claude Code, many turns at ~20K tokens): when the engine was ended mid-request (the
60 s watchdog), restart() started the new engine while the old process still held its VRAM. The new one ran out
of memory and exited before READY, `max_context` stayed 0, and from then on every request failed with
"400 ... exceeds the context (0)" until the server was restarted by hand.

- restart() ends the old process first (close(); kill and wait if it is stuck), so its GPU memory is free;
- a start that exits before READY is tried again, 3 times, RESTART_RETRY_S (15 s) apart; `starting` is set
  meanwhile, so a request gets the 503 "starting" of #344;
- `known_ctx` keeps the last context the engine reported: after a restart that failed for good, a request is
  checked against it and reaches run(), which starts the engine again, instead of a 400 about context 0.

Two tests in RestartWindow (both fail without this change): a start that fails once is retried and the engine
comes up; a request after a failed restart gets 200, not a 400. `python -m unittest serve.test_server`: 120 OK.
2026-10-03 18:07:54 +02:00
Niko1221 59a7e22fc2 HIP: the grouped decode experts, IQ3_S gate/up + IQ4_NL down, faster and bitwise (opt-in STRATA_EXPERT_V2=1)
native_expert_grouped for the UD-IQ4_XS shapes (n_embd 2560, n_ff 640): s26_gu_l_kernel / s26_down_l_kernel keep
every row's lane-strided k order, Split<TY>::apply expression and warp_sum, so every output is bitwise the old one's;
a warp interleaves 4 rows (more loads in flight), the chunk's activation q8_1 rows are read from LDS, and for IQ3_S
the 512-entry grid table too (the same values); SwiGLU and its q8_1 run as one launch (contraction off in the
quantizer). Other formats / shapes keep native_gu_multi_kernel / native_down_multi_kernel.

gfx1151 harness (S26 ws3: real expert blobs, 32 experts x 4 layers = 298 MB, decode-style windows planned as
resident_plan does, memcmp of every output, paired HIP events): one layer's experts T=1 140.3 -> 133.3 us, T=2
225.1 -> 204.1, T=3 300.2 -> 265.9 (166 -> 175-185 GB/s). Tried and rejected: non-temporal loads (no gain), whole
rows staged in LDS (0.6-0.8x: occupancy), 8 rows per warp for down.
Engine (UD-IQ4_XS, c-best3 env + the switch, same binary, S25's 18 prompts): outputs 18/18 identical; 56.6 / 57.0
-> 55.65 / 55.27 (and 55.61 with the fused SwiGLU q8_1) ms per verify window, 48.6-48.9 -> 50.3 tok/s;
59K prompts 2/2 identical, 58.65 -> 56.77 / 56.66 ms per window, 39.70 -> 41.01 / 41.09 tok/s.
2026-10-03 15:33:07 +00: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
benoit lange 953203be43 Tests and docs: parking with a layer split, tools/parking_test.py, a Testing section
- tools/parking_test.py: a follow-up decodes the same tokens whether its conversation stayed live or came back
  from the parking cache (every stage's image with a layer split). 4-GPU split, IQ3_S: 3,786 and 9,276-token
  conversations restored in 42 / 51 ms, follow-ups identical; without parking the same follow-ups took 3.4 / 6.8 s.
- tools/batch_test.py: --keys to send sampling keys (sampled rows in batch windows).
- docs/BATCHING.md: parking together with batching, the Testing section; docs/DETAILS.md: parking now works with
  --layer-split.
2026-10-03 16:11:57 +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
Niko1221 db43f526d0 Merge S23 f-halo (STRATA_HC_UPMIX, STRATA_PA_FAST, STRATA_PF_GEMM prompt switches) into f-halo-int 2026-10-03 11:12:41 +00:00
Niko1221 f2afdfc244 Merge branch 's25-tps2' into f-halo-int 2026-10-03 08:20:18 +00:00
Niko1221 22abb92dff GDN decode step and activation quantizes: bitwise faster paths (STRATA_GDN_SPLIT=1, opt-in STRATA_QFUSE=1)
GDN step (verify_kernels.cu, S25's STRATA_GDN_SPLIT=1 split, handed to S26):
- the commit's replay writes no outputs (t_out_begin = MT): y_dummy_ was never read; the state is unchanged;
- the commit runs gdn_state_replay_kernel: S25's column split (4 blocks of 32 columns per head) with gate, beta and the
  v column of every row prefetched and the next row's k / q loaded while the current one computes;
- the window keeps the old kernel for one row (the split did not pay there) and runs the split for 2+ rows with
  gdn_out_norm_kernel rewritten in the old kernel's code shape: S25's 128-thread norm differed from the old kernel by
  1 ulp in ~7% of the outputs (405 of 6144 at T=1); the S x RG version is bitwise equal.
gfx1151 harness (h_v 48, 36 layers rotating, paired, every layer memcmp-equal): commit 44.1 -> 39.1 us at 1 row,
50.7 -> 41.8 at 3, 60.8 -> 46.1 at 6; window 23.8 -> 23.2 us at 2 rows, 30.1 -> 26.7 at 4.

STRATA_QFUSE=1: q8_1 images written by their producers instead of a separate native_quantize_q8_1 launch:
- the GDN output norm (both window kernels) quantizes its outputs (warp = one 32-column q8_1 block), contraction
  off in the quantizer so fma(a, b, shfl) cannot round the first sum differently (that made ds.sum differ by 1 ulp);
- the hyper-connection read (gr_up_multi_kernel, gr_up_q8_kernel): the second of the two 16-column blocks of each
  32-column group (a per-caller counter, reset by that block) reads the 32 `mixed` values back and quantizes them -
  attention side into xq_, FFN side into nat_xq_; fused_gr_read_multi returns whether it wrote them (the v3 read
  does not). gr_parity: new check, T 1..8, direct and graph replays, q8_1 bytes equal to native_quantize_q8_1's.
Harness: GDN step + quantize 23.1 -> 20.5 us (1 row), 26.4 -> 23.3 (2), 32.6 -> 27.0 (4), q8_1 bytes equal.

Engine (UD-IQ4_XS, c-best2 + the switch, S25's 18 prompts, 256 new tokens): 18/18 identical in every run;
c-best2 49.46 / 49.44 tok/s, + STRATA_GDN_SPLIT=1 50.13 / 50.23 / 49.99, + STRATA_QFUSE=1 50.10 / 49.98 / 49.95.
2026-10-03 08:19:26 +00:00
Niko1221 4c00d2e7a3 prefill: opt-in STRATA_PF_GEMM=1 - a gfx11 WMMA GEMM for the FP16 prompt projections
strata_pf_gemm_f16 (wmma_gemm.cu), tried in Gemm::f16 before hipBLASLt for T >= 64, N >= 512, K % 32 == 0, beta
0 / 1: block 128 x 256 (128 x 128 below N 1024), BK 32, 8 waves of 64 x 64, LDS double buffer with the next stage
prefetched into named registers, grouped tile order (8 row tiles sweep all column tiles, so the weights stream
from DRAM once per group).  FP32 accumulation, another K order than hipBLASLt: rounding-level.

gfx1151, M 16384 (TFLOPS, tuned hipBLASLt in brackets): 12288 x 2560 33.0 (27.4), 10240 x 2560 31.9 (26.8), 6144 x
2560 31.1 (25.9), 2560 x 2560 30.1 (24.9), 640 x 2560 30.2 (25.3), 2560 x 6144 19.4 (19.1); the register-only WMMA
ceiling is ~50.  Engine 8K: gdn 1122 / 1112 -> 1083 / 1071 ms, qsa proj 813 / 716 -> 692 / 676, gdn out proj
753 -> 645 / 665.  Teacher-forced KL 0.099 / 0.028 on two text offsets (accepted pair: 0.117 / 0.030).  Off by
default.  With STRATA_HC_UPMIX + STRATA_PA_FAST, UD-IQ4_XS 64K prompt 872.8 / 880.6 -> 942.4 / 937.8 tok/s.
2026-10-03 07:32:53 +00:00
Niko1221 7aaf466d18 qsa: opt-in STRATA_PA_FAST=1 - the gfx11 prompt attention with single FP16 q and p
prompt_attn_wmma_kernel carries q and p as FP16 hi + lo pairs (two WMMAs each, FP32-level).  SPLIT = false drops
the lo halves: FP16 attention as the usual flash-attention kernels, half the matrix work.

gfx1151, real 64K selections (16383 queries x 2051 cells, one QSA layer): 138.7 -> 117.8 ms; rel RMS vs FP64
4.8e-7 -> 2.7e-4.  Engine 8K "qsa attn" 718 / 712 -> 589 / 590 ms.  Teacher-forced KL on three text offsets 0.153 /
0.087 / 0.027 (the accepted MMQ-vs-fused pair: 0.153 / 0.117 / 0.030).  Off by default.
2026-10-03 07:32:53 +00:00
Niko1221 31b2472a3f prefill: opt-in STRATA_HC_UPMIX=1 - the hyper-connection up projection with gr_mix_r as its epilogue (gfx11)
The read's up GEMM wrote `gated` (T x 10240 FP32: 671 MB per 16K chunk) for gr_mix_r to read back.  gr_upmix
computes a tile of 128 tokens x 16 columns for the 4 streams on the matrix cores (BF16 in, FP32 accumulate, the
block's 64 w_up rows staged in LDS once) and runs the mix in registers, in gr_mix_r's order.  The grid is ordered
column-tile fastest: the blocks in flight share one token tile, so the R reads of the epilogue stay on few pages
(token-tile fastest touched 512 rows x 40 KB per block: 28 ms instead of 3.7 at 8K - address translation on this
box goes through the IOMMU).  STRATA_HC_UPMIX_CHECK=N runs the default pair on the first N reads and prints the
difference.

gfx1151, UD-IQ4_XS: `mixed` vs the default rel RMS 1.2-3.6e-7 on every read of an 8K prompt; prompt-timing "hc
read" 1352 / 1313 -> 1080 / 1212 ms at 8K; 64K prompt 874.3 / 876.1 -> 902.7 / 903.0 tok/s.  Rounding-level (the K
order of the GEMM): teacher-forced KL on three text offsets 0.170 / 0.085 / 0.032 vs the accepted MMQ-vs-fused
pair's 0.153 / 0.117 / 0.030; ledger and 8K / 64K retrieval pass.  Off by default.
2026-10-03 07:32:53 +00:00
benoit lange db90f0b816 server: a client that stops reading early keeps the engine in step
With --batch, a consumer that left mid-answer (a client disconnect, a tool call or a stop string ending the
response) left the engine writing the abandoned answer's tokens, and the next request read them as its own:
it answered the previous question again. Now the solo request is STOPped and read to its DONE, an admission
to its BADM, and a slot is BSTOPped and freed only at its BDONE. STRATA_BATCH_TRACE=1 logs the control lines.
tools/early_close_test.py reproduces it (solo and with a second request running).
2026-10-03 08:04:53 +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 48d10a4996 Prompt path: STRATA_PREFILL_DUMP_R_ALL=FILE, every position's final residual as BF16 (opt-in, for distillation data)
Appends [n][hc*n_embd] BF16 rows (round-to-nearest-even) in position order for every batched prompt chunk: the
input the MTP draft layer reads, for training it against this trunk. Off unless the variable is set.
2026-10-03 05:35:31 +00:00
Niko1221 d92c9feb78 Verify PLE block: the window's key and value projections at once (opt-in STRATA_PLE_BATCH=1, bit-identical)
Layer 1's PLE block ran its key (10240 x 2560) and value (2560 x 2560) GEMVs once per window row, reading ~40 MB of
weights per row. Both depend only on the row's n-gram embedding, so with the switch the group's rows go through
the multi-row forms of the same kernels (native MMVQ over n columns / bf16_gemv_fp32_mmvf_multi, each row's
arithmetic the single-row call's) and ple_block copies its row's results instead of recomputing them. The
history-dependent rest of the block stays row by row.
2026-10-03 05:10:15 +00:00
Niko1221 58729711d9 STRATA_HC_Q8=1: inject rows as the exact BF16, the final mixer from Q8_0 too
The GGUF's F32 inject rows hold BF16-exact values (conversions.json: "the values are BF16's"), so quantizing them
to Q8_0 only added error: the Q8_0 read now takes the pack's BF16 inject rows (STRATA_HC_Q8_INJECT=1 keeps the
Q8_0 copy). The final mixer before the head (output_hc_down/up, also BF16-rounded in the pack) now reads the
GGUF's Q8_0 values through the same multi read.
2026-10-03 04:11:11 +00:00
Niko1221 2f6c70f0d1 Merge S25 f-halo-tps (8 commits) into S26 f-halo-s26-ovh: f-halo-int
Conflicts: mtp.cpp (the draft head: S25 converts it with --mtp-q4 first, then STRATA_Q6_PACKED packs it only
while it is still Q6_K), native_dense.cpp (includes).
2026-10-03 03:47:13 +00:00
Niko1221 e3c3d6bade GDN verify step: each head's columns over 4 blocks (opt-in STRATA_GDN_SPLIT=1, bit-identical by construction)
gdn_step_norm_multi_kernel ran one 512-thread block per head - 32 blocks for a layer's 2 MB state, ~36 us per
launch at T=1 on gfx1151 (~60 GB/s) and +4 us per window row; it runs twice per GDN layer and window (the window
and the commit's replay). With the switch the recurrence runs on (head, 32-column) blocks with the same
(column, row group) threads and state rows, so every column's arithmetic is unchanged; the output norm, which
needs a head's 128 columns, follows in gdn_out_norm_kernel with the same warps and the same sum order.
2026-10-03 03:46:23 +00:00
Niko1221 5cf0079bcc HIP: the packed Q6_K heads (opt-in STRATA_Q6_PACKED=1), bitwise
The main output head (248320 x 2560) and the MTP draft head (its 106299-row subset) are Q6_K: 210-byte blocks, so
every int load is 2-byte aligned. STRATA_Q6_PACKED=1 builds a packed copy of each at load - the same bytes as ql /
qh / scales / d planes (+497 MiB, +213 MiB) - and native_q6_k_mmvq on a registered pointer runs
native_q6_k_packed_kernel: aligned non-temporal dword loads, 640 persistent workgroups, and the thread ownership,
q6_q8_dot_impl expression, accumulation order and reduction of native_q6_k_mmvq_kernel<false> /
native_mmvq_multi_kernel<Q6KTraits, NC, 4, 1>, so every output is bitwise equal (exact multi-column layout only).
STRATA_Q6P_SELFTEST=1 checks it at load (1-4 columns, memcmp) and times it paired; =2 also sweeps the grid.

gfx1151 load-time self-test (11 paired samples, medians): main head 1 column 2420 -> 2300 us, 2 columns 2563 ->
2455, 4 columns 2973 -> 2975 (no gain: not bandwidth-bound there); draft head 1 column 1319-1351 -> 946-961 us,
2 columns 1420-1459 -> 1064-1090, 4 columns 1629-1678 -> 1320-1365. Outputs bitwise equal. Grid 320-20480 flat.
2026-10-03 03:28:31 +00:00
Niko1221 be7889ed84 verify: a fully resident decode window without the host round trip (opt-in), and fewer quantize launches
STRATA_VERIFY_RESIDENT=1: when every expert of every layer is resident in the VRAM/UMA tier (checked once at init;
a full cache never evicts; one stage only), the window graph is recorded without the per-layer host step: no
doorbell_publish, no wait_flag_ge (3 per layer), no plan copy from mapped memory, no PCIe/CPU share (fetch_blobs,
rebase_ptrs, its empty grouped launches, copy_rows_from_mapped) and no moe_hit_add: the device plans each group
(resident_plan, the host's exact loop) and the combine reads the hit rows as 0.0f + hit (combine<true>), which is
what the zeroed part rows plus parts += hit gave. run() then skips the per-layer spin. Same tokens.

resident_plan: the plan in one block of 128 threads (entry i per thread; groups in first-occurrence order, entries
in increasing i, the same arrays) instead of one thread - 44 us -> a few us per call on gfx1151, 48 per window.
Used by STRATA_VERIFY_DEVICE_PLAN=1 too.

STRATA_VERIFY_QDEDUP=1: the experts' q8_1 image of the MoE input is made before the shared expert, whose gate/up
read it instead of quantizing the same rows again (quantize_q8_1_rows and native_quantize_q8_1 write the same
bytes); the shared expert's SwiGLU and its q8_1 run as one launch (native_swiglu_q8_1_kernel: the same expression,
then native_quantize_q8_1_kernel's block quantizer); the BF16 copy of the input is skipped when the shared expert's
native BF16 gate (which never reads it) is on. Same bytes, same tokens.

Aurora (gfx1151, UD-IQ4_XS, S23's c-u4 config, one binary, env-only arms): output ids identical in every run.
Per verify window (rocprofv3 trace, 8K prompt + 300 outputs, 114 windows): see aurora_s23.md (S26, overhead).
2026-10-03 03:28:31 +00:00
Niko1221 c637ed5a6e bf16 multi-row GEMV: 4 output rows per block (opt-in STRATA_MMVF_ROWS=1, bit-identical by construction)
The router (512 x 2560) and the indexer projections run bf16_f32_mmvf_multi_kernel: one block per output row, so
the window's T activation rows were read again by every block (+3.3 us per row per launch on gfx1151, ~0.2 ms per
window row). With the switch a block computes 4 output rows from one read of the activations; per output each
thread keeps the same pairs, FMA order and reductions.
2026-10-03 03:17:34 +00:00
Niko1221 8b30b1d587 Decode attention: the score phase one cell per thread (opt-in STRATA_ATTN_LANECELL=1, meant bit-identical)
attn_chunk_kernel scored a chunk's 64 cells warp by warp: each lane 8 of the 256 dimensions, then one warp_sum
per query head - 12 shuffle reductions per cell, which is what made the kernel ~53 us per layer at T=1 and
+32 us per window row on gfx1151 (it scales linearly with T). With the switch, thread t scores cell t % 64 for
3 of the 12 heads alone: the same 8-dimension partial expression per former lane, then lane 0's butterfly order
(a[l] += a[l + o], o = 16 .. 1), so each score has the same operations in the same order. Softmax, values and the
merge are unchanged.
2026-10-03 03:17:34 +00:00
Niko1221 348abafdd9 STRATA_HC_Q8=1: the F32 inject rows as Q8_0 too, so the Q8_0 read is taken
The read takes the Q8_0 kernels only when the inject rows have a Q8_0 copy as well, and the GGUF stores them as
F32: with only down/up loaded (0.62 GiB) every window still ran the BF16 read (Aurora: 18/18 outputs identical
to the default, verify 61.4 vs 60.9 ms). The inject rows (4 x 10240 per projection) are now quantized at load
with ggml's Q8_0 reference rounding.
2026-10-03 03:17:34 +00:00
Niko1221 fe6c5260ec Verify: the hyper-connection read from the GGUF's Q8_0 projections (opt-in STRATA_HC_Q8=1; S23's parked patch)
NativeDense keeps the six hc_* projections per layer as stored (Q8_0, 0.65 GiB) beside the pack's BF16 copy
(1.20 GiB, --compat-bf16's rounding); the verify window's fused_gr_read_multi then reads them with split-K
kernels: gr_down_q8 (10 row groups + the inject rows x 16 chunks of 640 columns, the norm folded in as
unscaled partials + per-chunk sums of squares) and gr_up_q8. Every other path keeps BF16. Another summation
order and the GGUF's own values: output-changing, so opt-in and quality-checked separately. Also the
STRATA_GR_V3=2 knob (the two-half v3 split where only 64 KiB of LDS is available). Taken over from S23
(aurora_s23/hc_q8_grv3s2.diff) unchanged.
2026-10-03 03:17:34 +00:00
Niko1221 2c697fdace --mtp-q4 all|proj|head: the projections and the draft head separately
On Strix Halo (UD-IQ4_XS, head Q6_K) the 4-bit head costs acceptance (tokens per step 2.81 -> 2.72 with both)
while the smaller 40.5K-token subset (--mtp-draft-vocab data/draft_vocab_en.bin) costs none, so the two 4-bit
copies are now separate choices.
2026-10-03 03:17:34 +00:00
Niko1221 5e0c43bacc --lookup-chain: the draft policy keeps only the chained tokens that pay
Measured on Strix Halo (UD-IQ4_XS, 1-2K contexts) a verify row costs ~9 ms on a ~61 ms window, so chaining
every 3+-token match (49% of chained drafts accepted) gave +6.4% tokens per step for +7% window time. The
policy now picks the number of chained tokens k with the best expected tokens per ms: the MTP window's
measured tokens + p_mtp (c + c^2 + .. + c^k) at the measured cost of the longer window, where p_mtp is the
product of the draft layer's own draft probabilities and c the chained drafts' acceptance by match length
(their own decayed counts, learned only from rounds that reached the chain). Unmeasured sizes are probed a
few times when p_mtp * c >= 0.6. STRATA_LOOKUP_CHAIN_FIXED=1 takes every proposal as before.
2026-10-03 03:17:34 +00:00
Niko1221 e35a061f1d MTP draft head: --mtp-q4 (4-bit projections and draft head) and --mtp-draft-vocab FILE
Both opt-in; drafts only, the verify window decides every token (18/18 prompts identical on IQ3_XXS).

- --mtp-q4: the draft layer's ten Q8_0 projections (fc_embedding/fc_hidden, q/k/v/o, the shared expert) and
  the draft head's subset rows are requantized to Q4_0 at load/bind with ggml's reference quantizer
  (native-experts builds). The prompt path's batched K/V pass keeps the Q8_0 originals.
- --mtp-draft-vocab FILE: the draft head's token subset from FILE instead of <mtp>/draft_vocab.bin
  (data/draft_vocab_en.bin: 40,525 tokens instead of 106,299).

RTX 5070, IQ3_XXS (head Q5_K), 18 prompts: draft ms per window 2.55 -> 2.23 (q4), 2.13 (en), 1.91 (both);
tokens per step 2.64 -> 2.63 / 2.67 / 2.57.
2026-10-03 03:17:34 +00:00
Niko1221 2312c83bfd Speculation: --lookup-chain (prompt lookup chained after the MTP drafts), --mtp-hnorm stream, draft sources
All opt-in; with none of them given the engine runs exactly as before.

- --lookup-chain K [--lookup-chain-min M]: after the MTP's drafts, the suffix drafter matches the context
  plus those drafts and appends up to K of the tokens that followed the match to the same verify window
  (the window grows to at most 8). Greedy verification keeps the output. Own counts: a stderr line per
  request and five more DONE fields when on.
- include/strata/spec/draft_source.hpp: a DraftSource interface (the request's own text is
  PromptLookupSource; other sources register in extra_draft_sources() and the longest match wins).
- --mtp-hnorm stream: the draft layer's pre_fc_norm_hidden as one RMS per hyper-connection stream
  (llama.cpp's qwen4exp MTP graph) instead of one over all four (vLLM's qwen4_exp MTP, the default).
  New kernel native_qsa_rms_norm_grouped; the batched prompt K/V pass follows the switch.
- Diagnostics: STRATA_MTP_FULL_HEAD=1 drafts over the whole vocabulary; STRATA_MTP_TOP2=1 counts how often
  the draft layer's runner-up is the token verify picked where a draft was rejected.
2026-10-03 03:17:34 +00:00
Niko1221 96f4065aae HIP: the packed Q8_0 decode layout (opt-in STRATA_Q8_PACKED=1), bitwise
A lossless load-time repack of the native dense Q8_0 matrices with 1024 <= n_in <= 4096 and n_out >= 2048 (on
UD-IQ4_XS: attn_qkv, attn_gate, attn_q; 84 matrices, +1.87 GiB): the same int8 values in a qs plane and the same
fp16 scales in a d plane, 34 bytes per 32 values as before. native_q8_0_mmvq finds a registered matrix by its
GGUF-layout pointer (that copy stays: the prompt GEMMs read it) and runs native_q8_0_packed_kernel: 640 persistent
workgroups; each thread owns the bytes of the exact layout (block tid/4 + 32 i, ints 2 (tid%4) and +1), reads them as
one aligned 8-byte run with streaming loads, and accumulates and reduces in the same order, so every output is
bitwise equal to native_small_mmvq_kernel / native_mmvq_multi_kernel (exact layout; with the upstream multi-column
layout the GGUF kernels run).

gfx1151, S26 standalone harness (8 real UD-IQ4_XS layers per shape rotating, paired HIP events, 15 samples, every
layer x 1-5 columns memcmp-equal): attn_qkv +10-12%, attn_gate +11-12%, attn_q +10-11% per call. 6144-wide rows
(ssm_out, attn_output) and rows < 2048 are not packed (slower in the engine / standalone).
Engine (UD-IQ4_XS, S23's c-u4 config, same binary, env only): output ids identical in all 10 runs; steady output
8K 40.06 vs 39.77 tok/s (3 runs each), 64K 30.94 vs 30.34 (2 each).
2026-10-03 01:26:09 +00:00
Niko1221 7e363dbebb prefill: the gfx11 fused prompt-expert grids at 2 blocks per WGP
HIP counts a gfx11 WGP (two CUs, which the WMMA prompt-expert kernels use as one in
the default WGP mode) as one multiprocessor, and the occupancy query answers 1 block
for these kernels where 2 run side by side - so every fused launch (native IQ and
Q2_0) ran a 20-block grid on gfx1151, 2 waves per SIMD.  wgp_blocks() doubles the
query's answer (STRATA_PF_OCC=N sets it, an experiment knob).

The same work items and arithmetic: parity passes, fused-vs-MMQ rel RMS unchanged,
and the engine's outputs are the same tokens.

gfx1151, one layer, 8192 tokens x top 10, IQ3_S / IQ4_NL: blocks per WGP 1: 60.7 ms,
2: 44.7, 3: 49.9, 4: 45.2.  Q2_0 (2048 tokens): 13.3 -> 11.4 ms.
Engine, UD-IQ4_XS (MTP + vision + speed projection): 64K prompt 819.8 / 823.9 ->
892.4 / 878.8 tok/s, 8K 825.5 / 820.8 -> 874.5 (the other 8K run overlapped a
download: 758); outputs identical (drafts 292/516 at 64K, 343/468 at 8K); ledger
{32,17,130}, 8K retrieval 3/3.
2026-10-03 00:33:19 +00:00
Niko1221 55b9f8346f prefill: the fused layout's smaller MoE buffers only when the native kernels cover EVERY layer
fused_layout() (STRATA_PF_FUSED=1) shrinks GU / H / Xq to the fused kernels' needs whenever
fused_ring() is true, and fused_ring() was true when ANY layer of a native pack was covered. A pack
with an uncovered layer - Unsloth UD-IQ4_XS: 5 layers with Q8_0 down projections - then ran MMQ for
those layers at the full chunk in buffers sized for stream_all_min() - 1 tokens: on gfx1151 an 8K
prompt answered "!!!!..." and a 64K prompt hung (--prefill 2048: an illegal memory access). Such a
pack now keeps MMQ's buffers; its covered layers still take the fused kernels. UD-IQ4_XS 8K
retrieval after the fix: 3/3 correct, 837.9 prompt tok/s. (The CUDA path shares this code.)
2026-10-02 22:27:33 +00:00
Niko1221 aa5754fa06 hip: gfx1151 hipBLASLt table - the prompt GEMM shapes at --prefill 16384 too (T 16383 / 16384)
30 more rows from tools/hip/tune_hipblaslt.cpp on Aurora (90 in all). IQ3_XXS, 64K prompt, the
gfx11 fused experts: --prefill 16384 with them 849.2 / 847.0 tok/s vs 8192 803.1 / 824.0 / 829.3.
2026-10-02 21:31:01 +00:00
Niko1221 004b15cd4d core: #409 unified-memory free-memory accounting (device_free_bytes), its integrated-GPU sizing part
From PR #409 (af404df), only device_free_bytes(): on an integrated GPU (cudaDevAttrIntegrated, on HIP
hipDeviceAttributeIntegrated; the Ryzen AI Max 8060S) the expert cache sizing and checks count
/proc/meminfo MemAvailable less STRATA_UMA_HEADROOM_GIB (6) when that is more than cudaMemGetInfo:
the page cache of experts read from the GGUF in place gives way to a device allocation. The PR's
aarch64 / DGX Spark CPU parts are not taken.
2026-10-02 20:13:42 +00:00
Niko1221 41c89b1708 prefill: #313 RDNA3 WMMA dense GEMMs (opt-in STRATA_WMMA_GEMM=1), its GEMM part, on gfx1151 too
From PR #313 (1e42cf3): src/prefill/wmma_gemm.cu/.h, the Gemm::f16/bf16 hooks before hipBLASLt, the
build macro STRATA_WMMA_GFX11 and its parity test; not its prompt attention (STRATA_HIP_WMMA covers it).
Fix: the intrinsics guard named only gfx1100/1101/1102 while the runtime gate takes any gfx11, so on
gfx1150/gfx1151 mma() compiled to "return c" (silently wrong); it now names gfx1150/gfx1151 too.
hip_prefill_wmma_gemm_parity on gfx1151: 3520 cases pass (f16 max_abs 2.4e-7, bf16 0).
2026-10-02 20:12:38 +00:00
sergiywith b3096357c1 Prompt path: an MMQ group's experts gathered in one launch, after one wait, released by one event
In the streamed walk every expert put a cross-stream wait (its copy) and an event record (its ring slot back) on the
compute stream around a ~3 us gather. Under WDDM each pair left ~10 us of GPU idle even when the copy had long
landed: an nsys trace of a 32K prompt showed 226 ms of idle between back-to-back gathers while the host was 3-11 ms
ahead of the GPU. Now a group of 16 is gathered by one launch (copy16_group_kernel) after one wait on its last
streamed copy - the copy stream is in order - and one event releases all of its ring slots: the issuer waits on
used[used_of[slot]]. A ring entry the routing skipped inside an open group first gathers what the group holds so
far, so no more than a group's entries are ever held back from the issuer. The same bytes in the same group slots.
STRATA_PREFILL_GROUP_GATHER=0: one at a time, as before.

IQ2_XS, RTX 5090, 262K context: 32K prompt -8.6%, 8K -10%, 2K -12.6%; first-token logits bit-identical at 2K, 8K,
16K and 32K and with a 96-slot ring.
2026-10-02 20:10:40 +00:00
Niko1221 4cca9043e0 HIP: the native packs' fused prompt experts on gfx11 matrix cores (opt-in STRATA_PF_FUSED=1)
native_w11_kernel: moe_fused_iq.cu's decode (load_unit / convert: IQ2_XXS IQ2_XS IQ2_S IQ3_XXS IQ3_S
IQ4_XS gate/up, Q2_0 IQ4_NL down) into double-buffered LDS, v_wmma_i32_16x16x16_iu8 from a MAGIC C
operand (the CUDA kernel's arithmetic), 128 weight rows x 64-row tile per item, 8 waves of 2x2 tiles.
hip_prefill_fused_iq (tests/cuda/prefill_fused_iq_test.cpp) on gfx1151: every pair passes against the
FP32 reference (= MMQ's error) and MMQ (rel RMS 3e-5..3e-4); one layer, IQ3_XXS / IQ4_NL: 2048 tokens
31.1 -> 22.6 ms, 8192 tokens 67.2 -> 62.0 ms (MMQ path -> fused).
2026-10-02 20:10:40 +00:00
maxfridbe 835d30b186 sycl: OpenAI logprobs on /v1/chat/completions (engine LP lines + server_intel)
- engine (serve mode): GEN key logprobs=K (0..20) writes "LP logprob id:lp ..."
  after each "T id", from the verify window's head logits (Verifier::copy_logits;
  before temperature, penalties and sampling); absent key, no LP lines
- server_intel.py: pairs LP lines with their tokens (StrataEngine._pump), passes
  logprobs/top_logprobs to the engine, and fills OpenAI's choices[].logprobs
  (content / reasoning_content) in streamed chunks and the collected reply; the
  stop token is not listed. serve/ itself is unchanged.

Tested: a fake engine on hardac (streamed, not streamed, without logprobs,
stop token excluded); on the B70 with the IQ2_XS: same answer with and without,
"Paris" -0.0019 with top-1 equal to the chosen token, no measurable time cost.
2026-10-02 14:35:20 -05:00
Niko1221 5b8c84e7da HIP: the fused Q2_0 prompt experts on gfx11 matrix cores (opt-in STRATA_PF_FUSED=1)
expert_w11_kernel: moe_fused.cu's design (GPU grouping, experts read in place, gate/up + SwiGLU +
int8 H, then down) on v_wmma_i32_16x16x16_iu8. A work item = 128 weight rows x a 64-row tile, 8
waves of 2x2 16x16 tiles; operands straight from global one block ahead, weight scales in LDS.
The activations take gfx11's K order (act_pos); per 32-value half the integer dot is exact and the
epilogue is the CUDA kernel's. hip_prefill_fused_moe (tests/cuda/prefill_fused_moe_test.cpp) on
gfx1151: vs FP32 reference rel RMS 1.070e-2 (= MMQ's), vs MMQ 3.0e-5; one 2048-token layer 23.2 ms
(MMQ path) -> 11.3 ms. The native packs' fused kernels stay CUDA-only (stubs on HIP).
2026-10-02 19:21:35 +00:00
Niko1221 a138f63067 prefill: the GDN recurrence with four lanes per column (no barrier per token) + a grid-stride norm
Replaces the per-head kernel of the previous commit (VGPR-bound, spilled). Same column-split grid
as gdn_rec_cols_pipe_kernel (4 x 32-column blocks per head), but lane g of a column's four holds
exactly the old thread's row group g; kv / o partials meet through wave shuffles and are added in
the old order, so no __syncthreads per token; q/k/v/gate/beta staged 4 tokens at a time (~10 KB
LDS: all 192 blocks resident). The output norm runs the old kernel's body over a grid-stride walk
(T x 48 blocks of 128 threads before). tests/hip/gdn_rec_head.cpp: y, y16 and state bitwise equal
to the old kernels for T 1..2051 (variant 3 = new recurrence + old norm, a diagnostic); 2048 tokens
9.8 -> 4.1 ms on gfx1151 at the test's 600-880 MHz.
2026-10-02 19:21:35 +00:00
Niko1221 9a42723283 prefill: the GDN recurrence per head, a lane pair per column, the output norm fused (same bits)
One block per value head; each column's 128 state rows in the registers of two lanes of one wave
(the 32-row group partials swapped within the wave and added in the old order); q/k/v/gate/beta
staged in LDS 8 tokens at a time with the next chunk prefetched; the RMS norm + gate of
gdn_out_norm_kernel fused per chunk. tests/hip/gdn_rec_head.cpp: y, y16 and the state bitwise
equal to the column-split + norm kernels for T 1..2051; 2048 tokens 9.8 -> 6.7 ms on gfx1151
(at the 600-880 MHz the GPU's DPM runs this test at). STRATA_GDN_HEAD=0 keeps the old kernels.
2026-10-02 19:21:35 +00:00
Niko1221 92dde2f72e HIP: the WMMA QSA block scorer (STRATA_SELECT_WMMA=1) on gfx11 (RDNA3 / RDNA3.5) too
gfx11's bf16 WMMA layout: 16 k per lane, C rows 2i + lane/16. On gfx1151 (qsa_select_bench, 131624
capacity): ctx 32768 x 2048 queries scores 14.2 -> 3.5 ms (4.1x), selections identical 2047/2048;
ctx 65536 x 8192 queries 110.1 -> 25.9 ms (4.3x), identical 8192/8192. Still opt-in, as on gfx12.
2026-10-02 19:21:35 +00:00
Niko1221 41dbcdff32 hip: hipBLASLt tuning table for gfx1151 (Strix Halo, ROCm 7.14.1 / hipBLASLt 100401)
All 15 prompt GEMM shapes of Qwen3.8-Flash-Next at chunks 2048 and 8192 (T = 2047/2048/8191/8192),
tuned on Aurora (Ryzen AI Max+ 395) with tools/hip/tune_hipblaslt.cpp.
2026-10-02 18:13:09 +00:00
maxfridbeandsergqwer, constantindjonkam, architectds, BlueKingMuch (upstream PR authors) fd3214f52e sycl: port open upstream PRs #385 #463 #453 #374 #363 #407 #413 onto 0.1.35
One dpct run of upstream main + the seven PRs, 3-way merged into the files they
touch (sycl/tools/merge_upstream.py); their header changes are forked into
sycl/include until upstream merges them.

- #385 stager waits for a buffer's previous DMA on every job (port: polled seq)
- #463 decode waits for an adaptive swap before reading residency
- #453 drafter's batched K/V on the KV-streaming ring
- #374 first chunk's PLE rows read beside layer 0; port fixes: the next read
  starts at c0 + T (the port's first chunk is shorter), and the upload is
  marked by a polled sequence number instead of a host event wait, which
  deadlocked prompts past ~4K tokens under the Level Zero v2 adapter
- #363 group stride + fused SwiGLU/q8_1 for the PCIe expert call, re-applied
  to the port's lane kernels (fp contract off, as upstream's HIP form)
- #407 --adapt-tuned (opt-in), #413 key-head DeltaNet recurrence (opt-in via
  STRATA_GDN_KEYHEAD=1: 8% slower on the B70; its NVIDIA gate and SM-holding
  parity bench not ported)

Tested on the B70: native_grouped_parity, iq_multi, gr, qsa, kv_hybrid,
quantize_act, qsa_prompt_attn parity pass; Coder 19/2,184-token, IQ2_XS and
40K outputs identical to 0.1.33; Coder 2,184-token prompt 784 -> 825 tok/s;
128K int8 KV streaming 895 / 67.0 tok/s; IQ2_XS decode +1.7% (#363).

Co-authored-by: sergqwer, constantindjonkam, architectds, BlueKingMuch (upstream PR authors)
2026-10-02 13:12:58 -05:00
Niko1221 c6a0d4033b prefill: MMQ groups of a fully resident layer in row-count order (opt-in STRATA_MMQ_RESIDENT_SORT_NE=1)
Port of the Aurora experiment (mmq-resident-rowcount-sorting-v1, +13% 8K prompt on gfx1151 over
0.1.30 + Lt, identical output ids there): each 16-expert MMQ group's max_rows is close to its
experts' own row counts, so less tile padding. Host-only; slot/src/off rebuilt before the upload.
2026-10-02 18:00:02 +00:00
Niko1221 601b7765b6 HIP: the matrix-core prompt attention (STRATA_HIP_WMMA=1) on gfx11 (RDNA3 / RDNA3.5) too
gfx11's v_wmma_f32_16x16x16_f16 takes all 16 k per lane (lanes 16..31 repeat 0..15) and
returns C rows 2i + lane/16; the kernel loads its fragments in that layout. Opt-in as on gfx12.
2026-10-02 18:00:02 +00:00
Niko1221 6edf8bde75 HIP: admit gfx1151 (Strix Halo, RDNA3.5) as an experimental target, with sudot4 2026-10-02 17:41:28 +00:00
maxfridbe 05c90db586 sycl: port upstream 0.1.35 (re-migration of its changed engine files)
device, expert_source, native_head, prefill gemm, prefill.cpp, generate.cpp
merged from dpct of 8554b6b and 5b734e4 with sycl/tools/merge_upstream.py (the
3-way merge restricted to files upstream changed, dpct kernel-name hashes
canonicalized to the port's). Version 0.1.35.
2026-10-02 09:36:13 -05:00
maxfridbe e26ea97e09 Merge upstream main (0.1.35) into b70 2026-10-02 09:28:29 -05:00
maxfridbe cbe442bfbb sycl: pipeline the expert cache's profile fill (model load 3-4x faster)
The fill from the GGUF (--stream-experts) read each expert's three slices,
copied the blob and waited for the copy before the next read: ~12,000-18,500
experts with neither the SSD nor PCIe kept busy. Now the slots are admitted
in profile order first (same placement), reads go in file order by up to 8
threads into page-locked batches of 64, and a batch's copies overlap the next
batch's reads. STRATA_FILL_SERIAL=1 keeps the serial fill.

Tested on the B70: cold page cache, Coder fill 23.4 GiB 76.2 s -> 18.8 s;
launch to first token Coder 82 s -> 26 s, IQ2_XS 120 s -> 41 s; outputs
identical to the serial fill.
2026-10-02 03:49:48 -05:00
maxfridbe f2f4121a70 sycl: read streamed experts from the right shard; fix IQ2_XS products (Swift 1.5 runs)
- gguf_expert_source: ExpertLayout::gguf_file is per layer and role
  (3 * layer + role) since upstream 0.1.31; the port indexed it by layer, so
  experts in shard 2 were read from shard 1 at shard-2 offsets (Swift 1.5:
  layers 13-47, NaN logits from layer 13). One fd per layer and role now.
- iq_kernels: vec_dot_iq2_xs and Split<17> took their 16-bit codes through
  a uint16_t* to an int2 (type punning the device compiler does not honour);
  by shifts now. iq_multi_parity checks IQ2_XS against a CPU decode from
  ggml's tables (iq2xs_host_tables.inc) and passes.
- prefill: STRATA_DBG_NAN=1 also reports the experts' fp16 GEMM inputs.

Tested on the B70: Swift 1.5 IQ2_XS answers correctly (48 tok/s raw, short
thinking); the original IQ2_XS output is unchanged (65109a, 58 tok/s).
2026-10-01 22:49:04 -05:00
maxfridbe 9ef7a73273 sycl: port upstream 0.1.33 (re-migration of its six engine files)
expert_cache.cpp, expert_source.cpp, fused_gr, qsa_prompt_attn, gr_parity and
generate.cpp merged from a fresh dpct of 2ca1b66 and 8554b6b; files that only
differed by dpct run-to-run noise left alone. Conflicts: --resident-cpu-experts
(upstream's resident_cpu_explicit kept with --stream-experts) and the prompt
attention's capability check (the port's XMX dispatch kept). Version 0.1.33.

Tested on the B70: build clean; gr_parity and qsa_prompt_attn_parity pass;
Coder short/long, IQ2_XS and the 40K prompt (borrowing) outputs identical,
78.3 / 75.7 / 58.0 / 68.9 tok/s. setup_intel.py --check and an install run, and
server_intel.py with the mock engine, against the new setup.py / server.py.
2026-10-01 20:26:24 -05:00
maxfridbe 5da3769523 Merge upstream main (0.1.33) into b70 2026-10-01 19:59:04 -05:00
maxfridbe b5793b7c5d sycl: fix the prompt-slot borrow hang (stager event wait under L0 v2); borrow above 32K again
The hang was in the streamed-expert prompt path, which borrowing is the only
way to reach on the Coder: the stager threads waited on the copy queue's
events to reuse their 16 page-locked buffers, and under the Level Zero v2
adapter that left another queue's barrier on the same event waiting forever
once the ring wrapped. The copy queue now writes a sequence number into
page-locked memory after each DMA and the stager polls it.

- prefill.cpp: Stager::issued_one / buffer_free; STRATA_PREFILL_SYNC=1 debug
  switch (wait and log after every phase)
- generate.cpp: borrowing is the default above a 32K context again

Tested on the B70: 40K prompt with borrowing 1,117-1,144 tok/s, decode 69
(output identical to the run without borrowing); 128K int8/q4_0/k8v4 decode
66.6/62.8/66.8 (from 59.5/53.4/54.4), 256K 55.9/51.7/52.2 (from
48.7/43.2/42.3); Coder and IQ2_XS 32K outputs unchanged.
2026-10-01 19:58:53 -05:00
maxfridbe fddee3a47d sycl: port upstream 0.1.32 (re-migration 3-way merge)
dpct of 6ac1a2c and 2ca1b66, normalized, kernel-name hashes canonicalized to
the port's, then git merge-file into sycl/: 49 files, 10 with real conflicts.

- verify: upstream's set_commit_async/wait_commit on the port's deferred
  commit (wait_commit = commit_finish); copy_logits on the queue
- fused_gr: upstream's split/staged read variants merged but unused by the
  port's read; their per-card self-test segfaults on the B70, so on SYCL it
  runs only with STRATA_HC_CHECK=1. The port's split-norm kernel renamed
  (upstream has one of the same name); two colliding kernel names renamed
- fixups.py: fused_gr's per-block local-memory query
- hand files: mtp.cpp, verify.cpp, ple_reader.cpp (keep-alive reads + trace)
- qsa_prompt_attn_parity turns on the XMX kernel it checks
- version 0.1.32

Tested on the B70: build clean; Coder (19 / 2,184-token prompts) and IQ2_XS
outputs identical to 0.1.31 at the same speed (78.1 / 75.7, 58.5 tok/s);
40K prompt 1,201 tok/s, decode 65.1; kv_hybrid, qsa_prompt_attn, qsa, gr,
kv_q4, kv_q8, rope, router, sampler, quantize_act parity pass.
2026-10-01 16:12:35 -05:00
maxfridbe c9fd9cd1d3 Merge upstream main (0.1.32) into b70; Intel code kept out of shared files
Every file outside sycl/ (and docs/INTEL.md) is now upstream's, byte for byte.
The Intel pieces that used to edit shared files move under sycl/ and wrap
upstream from outside:

- sycl/setup_intel.py: runs setup.py's main() with the GPU check, engine
  step, RAM rule and config writer swapped for the SYCL port's
- sycl/serve/server_intel.py: serve/server.py plus the Arc's Monitor
  readings (sycl/serve/xe_telemetry.py) and the optional model menu
  (sycl/serve/web/switcher.js)
- sycl/tools/gpustat.py (was tools/intel/), sycl/.gitignore; migrate.sh
  writes its compilation database outside the checkout
- removed: the llama.cpp engine path (serve/engine_llama.py), the README box

Engine sources in sycl/ still track 0.1.31; the 0.1.32 kernels are not
migrated yet.
2026-10-01 15:06:34 -05:00
maxfridbe 5b1acc00e4 sycl: parity tests wait like cudaMemcpy; drain the refill ring in long prompts
- fixups.py: default-queue memcpy/memset in the parity tests first waits for
  all device queues (cudaMemcpy's legacy-stream semantics), applied to every
  src/kernels/*_parity.cpp
- generate.cpp: the post-prompt refill loops sync the queued copies every 256
  entries so the ring cannot wrap over in-flight slots
- kv_hybrid_parity: enable STRATA_PROMPT_ATTN_XMX (step 5 needs the XMX path)

Tested on the B70: build clean; qsa_parity, gr_parity OK.
2026-10-01 14:56:20 -05:00
maxfridbe fe95d59e25 docs/INTEL.md: lanes per row of the expert kernels measured per verify round (no difference) 2026-10-01 11:31:00 -05:00
maxfridbe 90ff5da28e sycl: refresh the port to upstream 0.1.31
Re-migrated (dpct of 0.1.29 and 0.1.31, 3-way merge per file): 45 files
changed upstream, 38 conflicts resolved; verify.cpp, mtp.cpp and the new
conversation_state.cpp ported by hand; 7 new upstream files.

Fixed on the way:
- the port's grouped expert kernels stay the default: upstream's new multi
  kernels (8 rows a group) ran on the port's 32-row grid and wrote a quarter
  of each expert (end-of-text output); STRATA_EXPERT_SPLIT=1 runs them with
  their own grid. Lanes per row now selectable (STRATA_GU/DOWN_LANES)
- dpct's unparenthesised __fadd_rn: the native QSA indexer's per-token
  pooled key (decode), the native QSA scores and the s2 activation rounding
  replaced instead of added - from the first migration; fixups.py fixed
- doorbell waits: fixups.py bounds upstream's new spin spelling again
- prompt-slot borrowing hangs since upstream's rework: off by default
- the Turing PTX branch in qsa_prompt_attn, dp4a.hpp's __nanosleep, the
  coupled-draft capture signature, a duplicate kernel name
- gr_parity: the single-token GR read runs the multi kernels (bitwise)
- qsa_parity: a cross-queue copy race in the test itself

Decode 78.2 / 75.7 tok/s (Coder), 58.6 / 64.2 (IQ2_XS); 128K-256K complete.
2026-10-01 11:05:32 -05:00
maxfridbe 2837e43650 PR hygiene: licenses for the vendored code, a generic Intel GPU sampler, no generated files
- sycl/include/dpct/LICENSE.TXT: the SYCLomatic helper headers' license
  (Apache-2.0 WITH LLVM-exception; their SPDX notices already name it)
- sycl/third_party/ggml/LICENSE: ggml's MIT notice beside the migrated copy
- tools/intel/gpustat.py + gpustat.service: the root sampler the Monitor
  tab reads load and VRAM from on xe (finds the card, sizes VRAM from its
  BAR; GPUSTAT_PCI / GPUSTAT_VRAM_MB override)
- dpct reports and .yaml sidecars removed and ignored
- docs/INTEL.md: no references to a private test rig; the sampler and the
  model_switcher setting documented
2026-10-01 07:15:36 -05:00
maxfridbe e533380d98 Merge upstream main (engine 0.1.31) into b70
Conflicts: setup.py (upstream's reworked low-RAM/paging/rope logic kept, the
Intel backends guarded out of the experts-in-RAM rules as before; meta, the
CUDA/HIP build stamp, only read where the resident variant uses it) and
serve/server.py (upstream's /load, /unload and idle-unload beside the port's
/switcher and request logging). The SYCL copies are refreshed next.
2026-10-01 07:11:17 -05:00
maxfridbe 6f12431d53 serve: Intel Arc readings in the Monitor tab; a model menu for hosts that swap models
- telemetry: on an Intel Arc (xe) the GPU readings come from a root
  sampler's /run/gpustat.json (load, VRAM: xe exposes them to root only)
  and sysfs (temperature, power, cap, the PCIe link the card trained at,
  capped by the slot). NVML stays first; nothing changes on NVIDIA.
- a run config may name a model_switcher RPC (one model at a time on a
  shared GPU): GET/POST /switcher report and request a swap, and the web
  app shows a Model menu with the models served on this port; it waits
  for /health to name the new model, then reloads. setup.py keeps the
  setting when it rewrites a config.
2026-10-01 06:55:20 -05:00
maxfridbe c2c6ccf431 docs/INTEL.md: the original Qwen3.8-Flash-Next IQ2_XS runs on the B70 (51-61 tok/s)
6,247 of its 24,576 experts live in the pinned host mirror (8.4 GiB) and
are read over PCIe by the device plan; RAM stays above 12 GB free.
2026-10-01 06:07:55 -05:00
maxfridbe 75d2c94d38 docs/INTEL.md: k8v4 long-context rows and the API figure on the current build
k8v4 (KV in VRAM, no streaming): 995 tok/s prompt / 65.8 decode at 128K,
804 / 51.2 at 256K with 994 experts served from the host mirror.
API: 48-72 tok/s on a short chat answer, prompt included.
2026-10-01 04:52:51 -05:00
maxfridbe c53fe2331d docs/INTEL.md: re-measured on the current build, long contexts included
Summary 77.9 tok/s (19-token prompt) / 74.2 after 2,184 tokens; long
contexts with setup's KV streaming: 66.1 tok/s decode after 128K, 54.1
after 256K (all experts in VRAM). The 'not re-measured' caveat is gone.
2026-10-01 04:23:15 -05:00
maxfridbe e6d5e03006 sycl: GR norm per (token, stream), aligned loads in the IQ3_XXS/IQ3_S/IQ2_S expert dots
- the GR norm ran one work-group per token; one per (token, stream) visits
  every element in the same order, so rs and xn are bitwise unchanged
  (gr_bench checksums equal), GR read 76.7 -> 71.4 us at 6 tokens
- the gate/up expert dots read their 2-byte-aligned weights as aligned
  4-byte loads (page-safe) instead of 16-bit pairs; same ints
- grids in local memory (STRATA_GRID_SLM=1) measured slower: off
Decode 77.9 tok/s (19-token prompt), output tokens unchanged.
2026-10-01 04:22:27 -05:00
maxfridbe 5273a1cfb7 docs/INTEL.md: planned item 9, the original IQ2_XS with the expert overflow in the host mirror 2026-10-01 04:01:03 -05:00
maxfridbe 223c9c3a01 sycl: IQ4_NL expert dots with the codebook in registers and aligned loads (decode 70.8 -> 76.1 tok/s)
The expert down projection (IQ4_NL in 39 of 48 layers) was the largest
expert kernel: ggml reads four uint32 of the codebook from global memory
per lookup and each weight int as two 16-bit loads. Register constants
and aligned 4-byte loads (the third only when unaligned, so page-safe)
produce the same ints: 80.8 -> 43.1 us per call, output tokens identical.
-DSTRATA_IQ4NL_FAST=0 restores ggml's path.
2026-10-01 03:38:25 -05:00
maxfridbe 69b4b98f1b docs/INTEL.md: current numbers up front (SYCL port 70.8 tok/s decode, 799 tok/s prompt)
The summary still described llama.cpp as the Intel engine at 23-25 tok/s.
It now compares NVIDIA, the SYCL port (setup's default once built) and
llama.cpp; the stale 'not ported'/'not done' items, setup's KV choice and
the served speed are corrected, and the long-context table is marked as
measured before the decode work.
2026-10-01 03:35:41 -05:00
maxfridbe 0012c9eb3f serve: log each request's client; SYCL wrapper without the image's health check
A disconnect in the log now names the client that closed it. The
strata-sycl-dev image inherits a health check on port 8080 (llama.cpp's)
that marked the serving container unhealthy forever.
2026-10-01 03:27:17 -05:00
maxfridbe 368ec61aac sycl: GR down projection sliced by columns (decode 65.5 -> 70.6 tok/s)
Each work-group owns a 128-column slice of all 324 rows, stages that
slice of xn once (<= 4 KB SLM) and streams the rows through it, 4 lanes
per row; a reduce kernel sums the 80 slice partials in a fixed order.
GR read 108.6 -> 76.5 us at 6 tokens. STRATA_GR_DOWN_SLICED=0 restores
the direct kernel.
2026-09-30 23:25:20 -05:00
maxfridbe 02013d01c8 sycl: wide Q8_0/IQ4_NL decode kernels, aligned Q6_K activation loads (decode 63.4 -> 65.6 tok/s)
Q8_0 and IQ4_NL blocks (34/18 bytes) are 2-byte aligned: the wide kernels
load them through load16_a2, now page-safe (the second chunk only when the
address is unaligned). Q8_0 4-6x, IQ4_NL 640->2560 4x. Q6_K's Q8_1 activation
int4 loads were 4-byte aligned: four int loads instead.
Output identical for 146 of 256 tokens, then a near-tie flips (summation
order); STRATA_MMVQ_WIDE_32=0 restores the old kernels.
2026-09-30 23:12:42 -05:00
maxfridbe 90aed054ff sycl: decode 54 -> 62.6 tok/s: parallel resident plan, untiled GR down, wide Q4_K/Q5_K/IQ4_XS
- resident_plan groups the entries with one thread per entry (thread 0
  alone took 73 us per layer); the plan is the host loop's exactly
- the GR down kernel reads xn from global memory instead of a 60 KB SLM
  tile per work-group (occupancy); outputs bitwise equal (gr_bench)
- 16-byte-load wide kernels for Q4_K, Q5_K and IQ4_XS (IQ4 codebook in
  registers): IQ4_XS 107 -> 323 GB/s, Q4_K/Q5_K 1.3-1.6x
Output tokens identical in every engine run.
2026-09-30 22:46:56 -05:00
maxfridbe cd83544431 sycl: aligned loads for the Q6_K decode kernel (decode 45 -> 54 tok/s); PLE gather trace
Q6_K blocks are 210 bytes, so the wide kernel's 16-byte ql/qh loads were
2-byte aligned and the B70 split them: 145 GB/s against 640 GB/s for the
same blocks at a 224-byte stride. load16_a2 does two aligned loads and a
shift; with it the wide kernel takes every Q6_K shape and up to 8 columns
(2.6-3.4x in q6k_align_bench). Output tokens identical.

Also a port copy of ple_reader.cpp with STRATA_PLE_TRACE=1, which showed
the 45/39 tok/s two-speed runs are one 226 ms PLE stall landing in either
the prompt or the first decode round (TTFT + decode is the same).
2026-09-30 21:43:56 -05:00
maxfridbe e7c42b62b9 sycl: SIMD16 variants of the decode mmvq kernels (opt-in STRATA_MMVQ_SG), mmvq_sg_bench
SIMD16 wins in isolation on IQ4_XS and short outputs, but no setting beats
the 32-lane default in the engine beyond noise; kept opt-in.
2026-09-30 19:52:32 -05:00
maxfridbe 7becf121d6 sycl: pinned host mirror for experts missing from VRAM, read by the device plan
Experts without a VRAM slot are read once into pinned host memory and the
device-built verify plan points the expert kernels at them over PCIe. The
plan used to drop non-resident experts, so output with misses diverged.
Forced 1,879-expert miss: decode 2.6 -> 40.9 tok/s, output identical.
2026-09-30 18:54:06 -05:00
maxfridbe 21ccac1b01 setup/docs: KV streaming for the SYCL engine from 64K - 256K context decodes at 31 tok/s (from 4-5)
--kv-resident 32768 keeps the whole KV in pinned host memory and only the attended window in VRAM, so every
expert stays in VRAM at long contexts. B70, decode after the prompt: 128K int8 8.5 -> 38.7 tok/s, 128K q4_0
32.7 -> 35.3, 256K int8 3.7 -> 31.3, 256K q4_0 5.0 -> 31.5; the prompt pays 5-13%. setup.py now turns it on
from 64K for the SYCL engine (its RAM check counts only the KV copy and the engine, the experts being in VRAM)
and keeps INT8 KV, which is the faster of the two with streaming.
2026-09-30 17:41:04 -05:00
maxfridbe 29e379e0d2 setup: Strata's SYCL engine on Intel cards when it is built; sycl: clean exit in serve mode
setup.py gets a third backend, sycl: on an Intel Arc with build-sycl-aot/strata and the strata-sycl-dev image
present (or --intel-engine strata; --intel-engine llama keeps llama.cpp), it builds the native pack and the
MTP draft layer (reusing an mtp-q2_0.gguf that is already there), writes the engine's arguments with the
container's paths through sycl/serve/strata-sycl.sh, --stream-experts with a reserve that keeps every expert in
VRAM (1,024 MiB up to 32K, 2,048 MiB and 4,096-token chunks above), Q4_0 KV above 64K, and keeps the sampling
block of an earlier config. start() checks the model files through the config's sycl_root; upgrade_config
leaves the SYCL config alone (the wrapper has no version, which read as 0.1.12 and downgraded --prefill).
Tested on the B70: the Coder's config regenerated by setup serves at 38.9 / 30.3 tok/s (first turn / follow-up)
with all 12,288 experts in VRAM.

The serve loop used to abort at exit (139): the Verifier's destructor waited on its queue after the Level
Zero context had begun tearing down. The destructor is now exception-safe and serve mode exits directly once
its requests are done: exit 0.
2026-09-30 17:11:08 -05:00
maxfridbe 1807ab8f2c sycl: serve the port - strata-sycl.sh runs the engine in the oneAPI image as server.py's exe (36 tok/s sampled)
The Coder on the B70 now runs Strata's own engine behind serve/server.py --engine strata: the wrapper starts
build-sycl-aot/strata inside strata-sycl-dev with the serve pipes attached and the port's run-time switches set.
Through the OpenAI API with temperature 0.6 / top-p 0.95 / top-k 20 / repetition penalty 1.05: 36 tok/s decode
on a first turn, 33 on a follow-up (conversation cache reused), vs 24-26 for llama.cpp on the same card. The
expert reserve has to leave every expert in VRAM (--stream-experts has no host copy): 1,024 MiB at 32K context.
2026-09-30 16:15:45 -05:00
maxfridbe 6b5761d34a sycl: STRATA_DUMP_SEL (the prompt path's selected cells); INTEL.md - XMX v2 in the full matrix, selection overlap
The selection dump measured how much neighbouring prompt positions' QSA selections overlap at 80K tokens: the
union of 8 is 3.3x one selection (16: 5.1x), 12% shared by all 8, so a grouped-gather attention kernel would buy
at most 5-8 s of an 80K prompt. XMX v2 across the 128K/256K matrix: 23-33% slower prompts than the FP32 attention.
2026-09-30 16:02:40 -05:00
maxfridbe 3467cc9d4e sycl: XMX prompt attention v2 (1.5x v1, k8v4 parity complete), long-context x KV-type table
v2: 64-cell chunks, V packed two cells at a time as 32-byte stores, K^T as 4-byte pairs with INT8 converted on
the load, hi+lo Q in one accumulator per scale group, one accumulator update per chunk. qsa_prompt_attn_parity
passes in every mode and kv_hybrid_parity passes completely with it (its last step needs a tensor-core-class
prompt kernel). 13.4 ms per chunk vs v1's 18.7 and the FP32 fallback's 5.5: the attention is gather-bound, so
it stays opt-in (STRATA_PROMPT_ATTN_XMX=1, or =32 for 32-cell chunks).

INTEL.md: 128K and 256K prompts with int8, q4_0 and k8v4 KV on the B70. All complete; q4_0 keeps every expert
in VRAM at 128K (decode 32.7 tok/s after the prompt); at 256K all three evict experts and decode drops to 4-5.
2026-09-30 15:02:17 -05:00
maxfridbe 04e00c08c6 sycl: lend cache slots to the prompt path above a 32K context - 80,000 tokens at 1,062 tok/s, decode 35-40 after
Without borrowing, the VRAM reserve for the KV state and the chunk buffers evicts ~1,900 experts, the prompt
streams them (790 tok/s at 80,000 tokens) and decode after it runs at 2 tok/s. Borrowing lends ~940 slots and
refills them in ~1 s: 1,062 tok/s and 35-40 tok/s decode. It costs ~1 s per prompt at short contexts (2,184
tokens 610 vs 792 tok/s), so the default is by context: borrow above 32K. --prefill-borrow / --no-prefill-borrow
decide explicitly; layer splits keep their own buffers as before.
2026-09-30 14:25:02 -05:00
maxfridbe 14e0c017bb sycl: refresh for upstream 0.1.28-0.1.29, fix the streamed-expert prompt path (stale ring pointers, stream-all hang)
- Re-migration merge of upstream 99b4097..0ea2fc3 (GDN recurrence pipelining, block scores read once per window,
  the GPU-wide sampler, the draft head's VRAM reserve, correctness fixes); verify.cpp/mtp.cpp ported by hand; two
  new fixups (a cudaStream_t cast in the sampler, a const sycl::free in native_head). Parity unchanged (19 of 21).
  B70: 2,184-token prompt 765 -> 792 tok/s, decode at that context 43.1 -> 45.2 tok/s, outputs identical.
- The prompt path's stream plan kept GgufExpertSource::blob() pointers for ~1,900 experts per chunk against a
  512-slot ring, so blobs were overwritten before they were copied (the first 80k measurement was partly wrong
  experts). The stager threads now read (layer, expert) themselves via GgufExpertSource::read_into.
- The stream-all walk hangs in its first large chunk on this card (copy engine stuck on a barrier, any ring size,
  either issuer; the old read path too). The port defaults to the per-layer routed-only walk (upstream e258e6e's
  STRATA_PREFILL_RING=8, included); STRATA_PREFILL_STREAM_ALL=1 restores it for debugging.
- The PLE reader copy keeps its 64-deep queue. 80,000 tokens now complete: 101 s = 790 tok/s.
2026-09-30 14:07:02 -05:00
maxfridbe d62ccc75ff Merge remote-tracking branch 'origin/main' into b70
# Conflicts:
#	setup.py
2026-09-30 11:35:14 -05:00
maxfridbe aeb21b6b11 docs: INTEL.md - 80k tokens at 1,065 tok/s with 12 staging threads; the SSD is the floor of the streamed experts 2026-09-30 09:15:09 -05:00
maxfridbe 74df8331cf docs: INTEL.md - the 80,000-token prompt: the VRAM plan trap, expert streaming per chunk, where the 77 s go 2026-09-30 09:12:52 -05:00
maxfridbe 3b48ee56ce sycl: PLE rows - a 64-deep read queue and a short first prompt chunk (2,184 tokens 711 -> 765 tok/s, 8,000 857 -> 986)
The rows of a 2,184-token prompt are 27k random 4 KB O_DIRECT reads (114 MB for ~7 MB of rows), all before the
first kernel: 466 ms with the reader's 16 blocking threads. 64 threads (the port's default now, a copy of
src/platform/direct_file.cpp; STRATA_IO_THREADS still overrides) give 330 ms and 128 / 256 the same - the drive
tops out near 85k IOPS. A 256-token first chunk (STRATA_PREFILL_FIRST, 0: off) lets the GPU start while the
rest of the rows are read behind it: 2,184 tokens 3,075 -> 2,860 ms, time to first token ~150 ms less; 8,000
tokens 986 tok/s. The chunk boundary moves rounding in the expert GEMMs, so a long greedy continuation can
diverge late (token 45 of 64 on the test prompt); short prompts are bit-identical.
2026-09-30 08:30:18 -05:00
maxfridbe c697b15994 sycl: iq4_nl dequant as runs of 8 (0.024 -> 0.017 ms per expert), host timers around the prompt path's per-layer grouping 2026-09-30 08:05:57 -05:00
maxfridbe 58e1f8d0c9 sycl: the speed round - warm-up captures, commit/draft overlap, vector-store dequant, XMX kernels (opt-in), profiles
Measured on the B70, outputs identical before and after each change (docs/INTEL.md "Speed work"):
- every window graph and the drafter's graphs captured at load (STRATA_WARM_GRAPHS=0 restores first-use capture)
- the commit graph is left running while the drafter's round runs on its own queue: decode 38.3 -> 43.1 tok/s at
  the 2,184-token context, 46.5 -> 49.7 on a short prompt
- the draft layer's batched prompt pass takes prompts under 64 rows (106 -> 16 ms on 19 tokens)
- one device module per kernel: the first launch in the prompt path 245 -> 1 ms
- the expert dequant writes each thread's run as one vector store: 0.085 -> 0.030 ms per expert; the 2,184-token
  prompt 566 -> 682 tok/s, 8,000 tokens 720 -> 841
- SWAR sign compare/subtract in the expert dots, a local-memory resident-plan kernel, a split-K fused down kernel
  (bit-identical to the single-token kernel, gr_parity): parity-clean, no measurable decode change

XMX: oneMKL's FP16 GEMMs already run on the XMX units (30-60 TFLOP/s). Two joint_matrix kernels are in the tree,
correct and opt-in because both lose to the paths they would replace on this card: xmx_gemm_iq (fused dequant +
GEMM from the quantized rows; xmx_gemm_bench) and qsa_prompt_attn_xmx (the mma.sync prompt attention's port;
qsa_prompt_attn_parity passes at 1e-6 of scale; STRATA_PROMPT_ATTN_XMX=1).

Profiling: unitrace -q needs Intel's metrics libraries (Dockerfile.metrics) and dev.xe.observation_paranoid=0;
the per-kernel table is in INTEL.md. A trap recorded there: --prefill-until with a native pack drops the prompt
tail (the token loop is skipped), so a "windows for short prompts" routing was invalid and is not in this commit.
2026-09-30 07:47:11 -05:00
maxfridbe 6396c46971 docs: INTEL.md - the merged port's numbers on the B70 (46.5 / 38.3 tok/s decode, 566 tok/s prompt) 2026-09-30 01:03:42 -05:00
maxfridbe b88eff688b sycl: refresh the port for upstream 0.1.25-0.1.27 (re-migration merge), AOT divide rounding, hybrid K8V4 KV
The merge of upstream main (99b4097) left 28 migrated copies behind. They are refreshed by re-migration:
dpct over the old and the merged tree, sycl/tools/normalize.sh (dpct's names, unchanged files, message
serials, fixups.py) on both, then a 3-way merge per file. Copies dpct never produced (verify.cpp, mtp.cpp)
take upstream's K8V4 diff by hand. The PTX gate fixup follows upstream's new __HIPCC__ guard (#elif).

The AOT build never received -cl-fp32-correctly-rounded-divide-sqrt (it went to the spir64 backend, which
an AOT build does not have): quantize_act_parity had its 303k mismatches back. The option now reaches
ocloc through -device "... -options ...". 19 of 22 parity tests pass again; kv_hybrid_parity (new) passes
all but its tensor-core step and is built, not registered.

B70, merged engine, AOT, --stream-experts --no-prefill-borrow --spec 4 --mtp: 2,184-token prompt 566.5
tok/s (was 475), decode 38.3 tok/s at that context (was 42.7; 77% draft acceptance on this text), MTP
prompt pass 38.6 ms (E-9 batched).
2026-09-30 01:01:25 -05:00
maxfridbe ac14e76bf5 Merge remote-tracking branch 'origin/main' into b70
# Conflicts:
#	setup.py
2026-09-30 00:10:48 -05:00
maxfridbe 828c3be921 docs: 42.7 tok/s decode at the 2,185-token context with MTP (prompt 475 tok/s in the same run) 2026-09-29 23:59:36 -05:00
maxfridbe d49f4b9d0f docs: 45.3 tok/s decode with the base model's MTP draft layer (87% accepted) - 1.8x llama.cpp on the same card 2026-09-29 23:58:26 -05:00
maxfridbe c550299fa8 docs: like-for-like against llama.cpp on the same prompt - prompt 560 vs 138-319 tok/s, decode 20.8 vs 24-26 2026-09-29 23:53:00 -05:00
maxfridbe 0b6624317a sycl: Q6_K 16-byte-load kernel (wide projections only), mmvq bench correctness check; bandwidth probe
bw probe (incompressible data): 4-byte loads stream at 395 GB/s, 16-byte at 596. The Q6_K matvec sits at
130-160 GB/s in every variant tried (lanes per row, rows per warp, unroll, 16-byte loads, software
pipelining): latency-bound on a dependent load->unpack->dp4a chain, not ALU or bandwidth. The 16-byte
kernel is kept for n_out >= 4096 (+13% there, slower on small shapes); rel 5e-8 against the shared one.
Decode 20.80 tok/s (spec 2). The drafter is the lever now: MTP fetch + pack running.
2026-09-29 23:51:36 -05:00
maxfridbe 1a234406dc sycl: long prompts - 693 tok/s at 2k tokens; the streaming source is a thread-safe 512-slot keyed cache
The prompt path collects hundreds of blob pointers per layer and copies from them on other threads later;
the 8-slot ring handed back pointers that were overwritten by then, and its index was not thread-safe.
Slots are now keyed by (layer, expert), reused oldest-first among 512 (~1.4 GB), under a mutex.

Long prompts also hung in the prompt path's streamed-expert handshake (lent cache slots make experts
non-resident, the host supplies them behind a flag the GPU wait kernel does not see). --no-prefill-borrow
keeps every expert resident and removes the handshake: 2,000 tokens 693 tok/s, 2,185 tokens 560 tok/s,
300 tokens 180 tok/s (llama.cpp on the same card: 150 short, 424 at 105k).

Rig: watchdog reports and kills a hung run at the expected time (EXPECT_S/QUIET_S); --spec 2 default
(20.75 tok/s decode vs 16.95 at spec 4: the suffix drafter accepts 9%).
2026-09-29 23:19:30 -05:00
maxfridbe 2eab02c231 sycl: clean exit, per-kernel profiling, Q6_K unpack hoisted; measured: the mmvq kernels run at 110-130 GB/s warm
- exit segfault: dpct's static global_memory/constant_memory objects destructed after the default queue
  (queue::get_device in the core); heap-allocated, never freed. Exit code 0; unitrace can write its table.
- Dockerfile.unitrace (pti-gpu unitrace in the dev image) + sycl/rank_kernels.py: per-kernel device time.
- Q6KTraits: the column-independent unpack (shifts, masks, saturating byte subtract) moved into load();
  acts()/dot() split; a row-blocked kernel (native_mmvq_rowwarp_kernel, STRATA_MMVQ_RPW) kept but off:
  with a 300 ms warm-up the shared kernel already does 2560x2560x4 in 48 us (110 GB/s) and the variants tie.
- mmvq_bench: the dense kernel timing harness (warm-up + 400 iterations; a 5 ms run measures the clock ramp).
2026-09-29 22:35:48 -05:00
maxfridbe 8b804379cc sycl: fused multi-token router for 256 experts (was 512-only); 16.95 tok/s decode
route<NE> templated on the expert count; native_router_top10_multi_ne dispatches 256/512. Both call sites
(the verify window and the per-token moe_route) take it for the Coder. 64 tokens: 16.25 -> 16.95 tok/s,
same output. Also: Dockerfile.unitrace (per-kernel Level Zero profiling image).
2026-09-29 21:53:31 -05:00
maxfridbe 27ca717e75 sycl: multi-entry expert dots - the resident-expert kernels 2.3x faster on iq3_xxs layers
A window routes the same expert for up to 6 tokens; the grouped kernels recomputed the dequantised weights
(grid lookups, sign unpacking, the byte-wise compare/xor/subtract emulations) once per token. Multi<TY>
splits each of the five formats this model uses (iq2_s, iq3_xxs, iq3_s gate/up; iq4_nl, q2_0 down) into
prep (once) and acts + finish (per token); the kernels take entries four at a time. Lanes per row is a
compile-time constant (STRATA_EXPERT_LANES, 8): 32 lanes on an 80-call row was mostly reduction.

Measured with the new NATIVE_BENCH=1 mode of native_expert_parity (10 experts x 4 entries, one layer):
iq3_xxs/iq4_nl 1.467 -> 0.649 ms, iq2_s/q2_0 ~0.68 -> 0.579 ms, iq3_s/iq4_nl 0.750 ms; parity unchanged.
strata::dp4a wraps dpct::dp4a (sycl::ext::oneapi::dot_acc is the same emulation with a header that breaks
the link across translation units).
2026-09-29 18:30:57 -05:00
maxfridbe 46ecab1310 sycl: AOT build (bmg-g31) - 15.2 tok/s decode, 28.8 tok/s prompt, first token in 0.85 s, no JIT
The -Xs device flag was mis-quoted; fixed. Measured on the B70 with the same 19-token prompt, 32 tokens:
decode 15.17 tok/s, prefill 28.79 tok/s, a 4-token window 73 ms on the card. The persistent JIT cache
(SYCL_CACHE_PERSISTENT=1) is the alternative for the JIT build: 15 MB, second start 15.6 tok/s.
2026-09-29 18:14:41 -05:00
maxfridbe 9af6ce12e3 sycl: ~13 tok/s steady state, coherent 64-token output; window timing, eager stage profiler, docs
Measured on the B70: 64 tokens in 52.0 s, of which ~47 s is the first window (the runtime JIT-compiles
every kernel on first use: 6 tokens take 47.5 s, 64 take 52.0 s). The output is the model's real answer.

Debug tooling kept in the port: STRATA_VERIFY_EAGER=1 runs the window on the queue instead of as a graph
and turns the stage stamps into host clocks, so STRATA_VERIFY_PROFILE=1 prints ms per stage without a
device timer; STRATA_VERIFY_DEBUG=1 prints the residual after every layer and the round's phase times.
build.sh takes BUILD_DIR and AOT (an ahead-of-time bmg-g31 build removes the JIT cost).
2026-09-29 18:04:13 -05:00
maxfridbe cf0e9c1e6e sycl: first light - the SYCL engine generates correct text on the Arc Pro B70
--stream-experts (GgufExpertSource): every expert goes straight from the GGUF into the VRAM cache through a
small ring of staging buffers; no resident host arena, so the engine runs on a 23 GiB machine.

What it took to get from "loads" to "<think>\nThe user wants a Python function":
- the doorbell handshake through host-mapped memory: volatile device loads do not bypass the caches on
  Intel; system-scope atomics (strata::sys_load/sys_store) do. A device-side spin is bounded (kSpinMax): an
  unbounded one wedged the GT twice when its process died.
- STRATA_VERIFY_NO_HOST=1 + STRATA_VERIFY_DEVICE_PLAN=1: with every expert resident the GPU plans each
  layer itself and the host waits for the whole window graph; per-layer ring visibility inside a graph is
  not reliable on this platform (measured with sycl/probe).
- -fsycl-default-sub-group-size=32: the kernels are written for 32-lane warps; dpct pinned only 134 of 289.
- cudaMemcpy is synchronous; dpct's default-queue memcpy had no wait, and the streaming ring reuses its
  buffers - the expert cache filled from overwritten memory (non-deterministic residuals, NaN by layer 5).
- native_expert_parity hand-ported (dpct cannot parse it without ggml-cpu.h): the GPU native expert
  kernel matches the float reference on real IQ1_M rows.
- verify.cpp debug readbacks (STRATA_VERIFY_DEBUG): the per-layer residual ladder that found all of it.

Not yet: speed (0.17 tok/s - the spin budgets), the segfault at exit, the tensor-core kernels.
2026-09-29 17:40:54 -05:00
maxfridbe a1c47bc90b sycl: the CUDA engine migrated to SYCL - builds with oneAPI, 19 of 22 kernel parity tests pass on the Arc Pro B70
The port lives in sycl/ and changes no upstream file: sycl/src and sycl/include hold only the files the
migration touched, sycl/CMakeLists.txt takes everything else from the original tree.

How it was made, reproducibly (sycl/tools/): a dev image (the llama.cpp SYCL image + SYCLomatic 2025.3 +
CUDA 12.8 headers from the pip wheels), migrate.sh (compilation database + dpct over 86 translation
units), fixups.py (every hand fix with its reason, idempotent), build.sh (icpx, ninja).

What dpct got wrong and the script repairs: CUDA's null stream became a null sycl::queue* (strata::q_of
maps it to the default queue); __ldg((const float*) p) lost its cast and read one byte; __fadd_rn lost
parentheses; ggml's lookup tables were threaded through kernel parameters with the wrong table per
template; helper headers renamed since 2025.3; cudaGraphUpload, graph introspection and %globaltimer
have no equivalents. Two compiler flags are load-bearing: -fp-model=precise, and the device compiler's
-cl-fp32-correctly-rounded-divide-sqrt - the Arc's fp32 divide is not correctly rounded by default, and
quantize_act_parity went from 303k mismatches to byte-exact with it.

Not ported: the three kernels with inline PTX (mma.sync, ldmatrix, cp.async) take their existing
pre-sm_80 fallback; the launchers refuse the device. The XMX joint_matrix versions are next.

The engine itself starts on the card, loads the pack, the projections, the PLE table and the 23.4 GiB
expert arena, and is then stopped by the test rig's memory guard: Strata keeps the experts as a resident
host copy, and the rig has 23 GiB of RAM (upstream asks for 32). A streaming fill of the VRAM cache
from the GGUF is the next piece; the whole model fits in the 32 GB card.
2026-09-29 14:06:20 -05:00
maxfridbe 39fdc3a56b docs: 128k context proven on the B70 - a 105k-token prompt read at 424 tok/s
Prompt reading scales up with prompt size (150 tok/s at 2.7k, 424 tok/s at 105k), so the full
window costs about five minutes, not fifteen. 30.3 GB in use with 1.5 GB free at 131,072.
2026-09-29 11:36:51 -05:00
maxfridbe 39cc2087e0 docs/INTEL.md: how much context fits on a 32 GB card, measured
About 20 KiB of KV per token (only 12 of 48 layers are full attention). 32k, 64k, 96k and 128k all
load with the whole model on the card, 128k with 1.6 GB to spare; 160k is under the safety margin
and 256k does not fit - asking for it spilled VRAM into host RAM and took the machine down until its
watchdog reset it, so the doc says so in bold.
2026-09-29 11:29:19 -05:00
maxfridbe 4ce698f199 gitignore: start-llama.sh, generated by setup on the Intel path 2026-09-29 11:14:27 -05:00
maxfridbe 3aa60f0e37 README: the Intel Arc port and the rig it was tested on, up top 2026-09-29 11:10:24 -05:00
maxfridbe f6e71c1c80 docs/INTEL.md: --port and --host, learned the hard way (8080 is open-webui here) 2026-09-29 11:09:39 -05:00
maxfridbe ca344a7c90 setup: an Intel Arc path (no CUDA), and the run script starts llama-server itself
Without nvidia-smi, setup now looks for an Intel discrete GPU in sysfs (vendor 0x8086 under xe or
i915, named by PCI id - lspci's database does not know an Arc Pro B70 yet) and takes a path with no
compiler and no CUDA toolkit: download the model, export the tokenizer, find a llama-server
(--llama-server PATH, one on PATH, or the B70 container image via docker), write a config with a
"llama" block and a start-llama.sh, and a run script that uses --engine llama.

The CUDA engine's rules that do not apply are stepped around rather than deleted: the compute
capability and driver-version checks, the experts-in-RAM gates (on llama.cpp the fit is VRAM,
shard 1 = the download minus the 28.8 GB lookup table), the MTP draft download (5 GB the llama
engine cannot use), and the engine's own argument list.

engine_from_config runs the config's start script when nothing answers at the url, so the run
script is one click and closing the server stops the container.

Verified on an Arc Pro B70: setup runs steps 1-7 clean against the downloaded Coder IQ1_M.
docs/INTEL.md has the whole picture and the measurements.
2026-09-29 11:09:39 -05:00
maxfridbe b3470a1722 serve: a llama.cpp engine behind the same boundary (--engine llama)
The Strata engine is CUDA. On a GPU without it - written for and measured on an Intel Arc Pro
B70 - the same GGUF weights run through llama.cpp's SYCL backend. serve/engine_llama.py puts
llama-server behind Engine.generate(ids, max_new, sampling, cancel), so the OpenAI and
Anthropic endpoints, streaming, tool calls, MCP and the web app are unchanged above it.

It sends the prompt as token ids (the server's own tokenizer, from the same GGUF) with
return_tokens, streams ids back with llama-server's timings mapped onto Strata's `last`,
heartbeats while a long prompt is read, and stops the server by closing the connection when a
client goes away. Attach to a running server ("llama": {"url"}) or spawn one ("llama": {"exe"}).

Not yet: images (Strata's Vision produces embeddings for the CUDA engine; llama-server takes
the image itself) - start without "vision" and image requests are refused cleanly.

Tests: a scripted llama-server (the wire format observed on b29c606), plus a live round trip
when STRATA_LLAMA_URL is set. Measured on the B70 with Coder IQ1_M: 23-25 tok/s decode,
~150 tok/s prefill, correct code, GPU-bound.
2026-09-29 11:09:39 -05:00
543 changed files with 174239 additions and 2651 deletions

No files matched your search

+10
View File
@@ -37,17 +37,27 @@ td.log
/strata-*.json
/strata-*.log
/build-strata.bat
/build-strata-cuda12.bat
/build-cuda12/
/.strata-mcp/
# image encoder (setup.py --vision): build output and the generated build script
build-vision*/
build-vision.bat
build-vision-cuda12.bat
# setup.py outputs (engine, downloads) and release builds
/engine/
/engine-cuda12/
/build-release*/
/dist/
*.done
/third_party/*.zip
# IDEs
.vscode/
.idea/
.cursor/
/.ccache/
/.rocm-win/
+374 -18
View File
@@ -8,7 +8,18 @@
# below refuses anything older: silently building for a pre-Ampere arch would produce a binary that runs
# and is wrong about its own performance, which is the worst of both outcomes.
cmake_minimum_required(VERSION 3.24)
project(strata VERSION 0.1.38 LANGUAGES CXX)
project(strata VERSION 0.1.40 LANGUAGES CXX)
# EXPERIMENTAL: the Intel Arc engine, the SYCL port in sycl/ (PR #423; icpx + oneMKL, docs/INTEL_ARC.md). It is its
# own project that takes the unported sources from src/; with this on, nothing below is configured.
option(STRATA_ENABLE_SYCL "EXPERIMENTAL: build the Intel Arc (SYCL) engine from sycl/ - docs/INTEL_ARC.md" OFF)
if(STRATA_ENABLE_SYCL)
if(STRATA_ENABLE_CUDA OR STRATA_ENABLE_HIP)
message(FATAL_ERROR "STRATA_ENABLE_SYCL is a separate backend; leave STRATA_ENABLE_CUDA and STRATA_ENABLE_HIP off")
endif()
message(WARNING "STRATA_ENABLE_SYCL: the Intel Arc engine is experimental (community port, docs/INTEL_ARC.md)")
add_subdirectory(sycl)
return()
endif()
add_compile_definitions(STRATA_VERSION="${PROJECT_VERSION}")
# issue #31: release builds keep a PDB beside the exe (not shipped in it), so a minidump from a stalled engine can
# be read back to the exact source line
@@ -21,6 +32,18 @@ endif()
# instead of this machine's (Strata's own AVX-512 kernels are per-file and chosen at run time either way), and
# the MSVC runtime linked in, so no Visual C++ redistributable is needed.
option(STRATA_PORTABLE "build for other PCs: AVX2 baseline, static MSVC runtime" OFF)
# EXPERIMENTAL, opt-in: an engine for CPUs without AVX2 (#394, #595, #623), compiled on that PC. ggml-cpu, which then
# computes every CPU expert row, is built for this floor instead of AVX2 or the build host: "avx" (Sandy/Ivy Bridge:
# AVX, no FMA/F16C/AVX2) or "none" (SSE4.2 + POPCNT: Nehalem/Westmere). Strata's own AVX2/AVX-512 kernels stay in the
# binary behind their CPU checks; the router lookahead gets an AVX1 kernel. Empty (the default) changes nothing.
set(STRATA_ISA_FLOOR "" CACHE STRING "experimental older-CPU build: empty (default, AVX2 floor), avx, or none (SSE4.2)")
set_property(CACHE STRATA_ISA_FLOOR PROPERTY STRINGS "" avx none)
if(NOT STRATA_ISA_FLOOR STREQUAL "" AND NOT STRATA_ISA_FLOOR STREQUAL "avx" AND NOT STRATA_ISA_FLOOR STREQUAL "none")
message(FATAL_ERROR "STRATA_ISA_FLOOR must be empty, avx or none (got '${STRATA_ISA_FLOOR}')")
endif()
if(STRATA_ISA_FLOOR AND STRATA_PORTABLE)
message(FATAL_ERROR "STRATA_ISA_FLOOR (an older-CPU build) and STRATA_PORTABLE (the AVX2 release build) exclude each other")
endif()
if(STRATA_PORTABLE AND MSVC)
set(CMAKE_MSVC_RUNTIME_LIBRARY "MultiThreaded")
endif()
@@ -33,17 +56,36 @@ if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES)
endif()
option(STRATA_ENABLE_CUDA "Build the CUDA targets (Phase 2+)" OFF)
option(STRATA_ENABLE_HIP "Build the HIP targets for wave32 AMD GPUs (gfx1100, gfx1201; gfx1030 unvalidated)" OFF)
# AMD gfx906 (MI50, wave64): the same .cu sources compiled as HIP through the gfx906 compat layer. The CUDA runtime and
# cuBLAS names are mapped by the headers in include/strata/platform/hip_compat/, which go first on the include path,
# and CUDA::cudart / CUDA::cublas become aliases of HIP's targets, so every CUDA target below stays as it is. This is
# separate from STRATA_ENABLE_HIP (the wave32 RDNA backend): with it on, STRATA_ENABLE_CUDA is on and
# STRATA_ENABLE_HIP stays off, so every "CUDA" branch of the tree is the one taken.
option(STRATA_HIP_GFX906 "Build the CUDA targets for AMD gfx906 through HIP (wave64 compat layer)" OFF)
if(STRATA_HIP_GFX906)
if(STRATA_ENABLE_HIP)
message(FATAL_ERROR "STRATA_HIP_GFX906 and STRATA_ENABLE_HIP are separate backends; enable only one")
endif()
set(STRATA_ENABLE_CUDA ON CACHE BOOL "" FORCE)
endif()
option(STRATA_ENABLE_HIP "Build the HIP targets for wave32 AMD GPUs (gfx1100, gfx1201; gfx1012/gfx1030 unvalidated)" OFF)
option(STRATA_PREFILL_MMQ "Build the optional GGML MMQ prefill path for HIP" OFF)
# CUDA: MMQ prompt kernels for Q4_K / Q5_K / Q5_1 (Unsloth's UD-Q4_K_XL experts). Off by default: the engine loads
# every kernel when it starts (CUDA_MODULE_LOADING=EAGER), so these took ~5 MB of VRAM - 1-3 expert slots - from every
# model, and the UD-Q4_K_XL prompt is SSD-bound with or without them (docs/UNSLOTH_Q4.md)
option(STRATA_MMQ_KQUANTS "Build the CUDA MMQ prompt kernels for Q4_K / Q5_K / Q5_1 experts" OFF)
option(STRATA_MMQ_KQUANTS "Build the MMQ prompt kernels for Q4_K / Q5_K / Q5_1 / Q6_K: the CUDA experts of Unsloth's UD-Q4_K_XL, and on HIP the dense GGUF projections (STRATA_DENSE_MMQ)" OFF)
# CUDA: Q6_K gate/up in the grouped expert kernels (Unsloth's UD-Q6_K_XL, docs/UNSLOTH_Q6.md). Off by default: every
# grouped-kernel instance is loaded when the engine starts (CUDA_MODULE_LOADING=EAGER), so one more costs VRAM that
# every other model would lose as expert slots (PR #860 review)
option(STRATA_Q6K_EXPERTS "Build the Q6_K gate/up grouped expert kernels (UD-Q6_K_XL)" OFF)
if(STRATA_Q6K_EXPERTS)
add_compile_definitions(STRATA_Q6K_EXPERTS=1)
endif()
option(STRATA_ORCA_Q4KS_MMQ "Build the GGML Q5_0 MMQ instance for OrcaRouter Q4_K_S" OFF)
if(STRATA_PREFILL_MMQ AND NOT STRATA_ENABLE_HIP)
message(FATAL_ERROR "STRATA_PREFILL_MMQ requires STRATA_ENABLE_HIP=ON")
endif()
if(STRATA_ENABLE_CUDA AND STRATA_ENABLE_HIP)
if(STRATA_ENABLE_CUDA AND STRATA_ENABLE_HIP AND NOT STRATA_HIP_GFX906)
message(FATAL_ERROR "STRATA_ENABLE_CUDA and STRATA_ENABLE_HIP are separate backends; enable only one")
endif()
if(STRATA_ENABLE_HIP)
@@ -63,12 +105,45 @@ if(STRATA_BUILD_CONVERSATION_TESTS)
add_executable(conversation_cache_test src/core/conversation_cache_test.cpp)
target_include_directories(conversation_cache_test PRIVATE include)
add_test(NAME conversation_cache_test COMMAND conversation_cache_test)
add_executable(conversation_split_failure_test src/core/conversation_split_failure_test.cpp)
target_include_directories(conversation_split_failure_test PRIVATE include)
add_test(NAME conversation_split_failure_test COMMAND conversation_split_failure_test)
add_executable(conversation_memory_test src/core/conversation_memory_test.cpp src/core/conversation_memory.cpp)
target_include_directories(conversation_memory_test PRIVATE include)
add_test(NAME conversation_memory_test COMMAND conversation_memory_test)
add_executable(conversation_file_test src/core/conversation_file_test.cpp src/core/conversation_file.cpp)
target_include_directories(conversation_file_test PRIVATE include)
add_test(NAME conversation_file_test COMMAND conversation_file_test)
endif()
if(STRATA_ENABLE_CUDA)
if(STRATA_HIP_GFX906)
if(NOT DEFINED CMAKE_HIP_ARCHITECTURES)
set(CMAKE_HIP_ARCHITECTURES gfx906 CACHE STRING "HIP architectures" FORCE)
endif()
enable_language(HIP)
find_package(hip REQUIRED)
find_package(hipblas REQUIRED)
add_library(CUDA::cudart INTERFACE IMPORTED)
target_link_libraries(CUDA::cudart INTERFACE hip::host)
target_compile_definitions(CUDA::cudart INTERFACE STRATA_HIP_GFX906=1 __HIP_PLATFORM_AMD__=1)
target_include_directories(CUDA::cudart BEFORE INTERFACE ${CMAKE_CURRENT_SOURCE_DIR}/include/strata/platform/hip_compat)
add_library(CUDA::cublas INTERFACE IMPORTED)
target_link_libraries(CUDA::cublas INTERFACE roc::hipblas CUDA::cudart)
set(CMAKE_CUDA_TOOLKIT_INCLUDE_DIRECTORIES ${CMAKE_CURRENT_SOURCE_DIR}/include/strata/platform/hip_compat)
include_directories(BEFORE ${CMAKE_CURRENT_SOURCE_DIR}/include/strata/platform/hip_compat)
add_compile_definitions(STRATA_HIP_GFX906=1)
message(STATUS "Strata: HIP (gfx906 compat) enabled, arch ${CMAKE_HIP_ARCHITECTURES}")
set(_strata_gpu_runtime_target CUDA::cudart)
set(_strata_gpu_blas_target CUDA::cublas)
set(_strata_gpu_include_directories ${CMAKE_CUDA_TOOLKIT_INCLUDE_DIRECTORIES})
elseif(STRATA_ENABLE_CUDA)
# #585: CMake links CUDA code with the STATIC runtime by default, and the targets below also link CUDA::cudart (the
# DLL's import library). MSVC with a CUDA 12.x toolkit - the experimental Pascal/Volta build - then fails with
# LNK1169 (every cudart symbol defined twice); GNU ld lets it pass. That build links the shared runtime alone. The
# ready-made engine (CUDA 13, no STRATA_EXPERIMENTAL_SM60) is unchanged; -DCMAKE_CUDA_RUNTIME_LIBRARY overrides.
if(MSVC AND STRATA_EXPERIMENTAL_SM60 AND NOT DEFINED CMAKE_CUDA_RUNTIME_LIBRARY)
set(CMAKE_CUDA_RUNTIME_LIBRARY Shared)
endif()
enable_language(CUDA)
# `enable_language(CUDA)` gives the .cu compile rules but NOT the imported targets, so `CUDA::cudart` has to
# be brought in explicitly for the host-side tests that call cudaMalloc themselves.
@@ -107,13 +182,18 @@ if(STRATA_ENABLE_CUDA)
"CUDA 12.8 crashed on long prompts on these cards (issues #220, #224). Use CUDA 13.0 or newer.")
endif()
endforeach()
if(STRATA_EXPERIMENTAL_SM60)
# every target sees it: the Pascal / Volta code paths (the Volta prompt attention, PR #600) are compiled into this
# build only, so the ready-made engine (sm_75+) keeps exactly the code it had
add_compile_definitions(STRATA_EXPERIMENTAL_SM60=1)
endif()
message(STATUS "Strata: CUDA enabled, arch ${CMAKE_CUDA_ARCHITECTURES}")
set(_strata_gpu_runtime_target CUDA::cudart)
set(_strata_gpu_blas_target CUDA::cublas)
set(_strata_gpu_include_directories ${CMAKE_CUDA_TOOLKIT_INCLUDE_DIRECTORIES})
elseif(STRATA_ENABLE_HIP)
set(_strata_gpu_runtime_target strata_hip_runtime)
set(_strata_gpu_blas_target roc::hipblas)
set(_strata_gpu_blas_target ${STRATA_HIP_BLAS_TARGETS})
set(_strata_gpu_include_directories ${STRATA_HIP_COMPAT_INCLUDE_DIR})
endif()
@@ -164,6 +244,12 @@ if(STRATA_BUILD_TESTS)
add_executable(gguf_split_test tests/core/gguf_split_test.cpp)
target_link_libraries(gguf_split_test PRIVATE strata_artifact)
add_test(NAME gguf_split_test COMMAND gguf_split_test)
# the hipBLASLt tuning header's architecture gate: RDNA4 siblings share solution IDs, RDNA3 siblings share
# theirs, cross-family pairs and version mismatches stay rejected (synthetic tables: no model, no GPU)
add_executable(tuning_header_arch_family_test tests/prefill/tuning_header_arch_family_test.cpp)
target_include_directories(tuning_header_arch_family_test PRIVATE "${CMAKE_CURRENT_SOURCE_DIR}/src/prefill")
add_test(NAME tuning_header_arch_family_test COMMAND tuning_header_arch_family_test)
endif()
# ---- strata_plan ----------------------------------------------------------
@@ -309,7 +395,7 @@ if(STRATA_ENABLE_CUDA OR STRATA_ENABLE_HIP)
src/kernels/cuda/native_flash_attn.cu
src/kernels/cuda/native_qsa_indexer.cu
src/kernels/cuda/native_qsa_score.cu
PROPERTIES COMPILE_OPTIONS "--use_fast_math")
PROPERTIES COMPILE_OPTIONS "$<IF:$<BOOL:${STRATA_HIP_GFX906}>,,--use_fast_math>")
elseif(STRATA_ENABLE_HIP)
# Keep Q8_K's multiply and magic-bias rounding as separate FP32 operations on HIP; contraction to an
# FMA changes tie results. The embedding gather also relies on separately rounded operations.
@@ -348,16 +434,30 @@ if(STRATA_ENABLE_CUDA OR STRATA_ENABLE_HIP)
# library is the honest shape of the dependency graph: core knows about devices and weights, kernels know
# about arithmetic, and this one knows the ORDER the arithmetic happens in.
add_library(strata_engine STATIC src/core/layer.cpp src/core/session.cpp src/core/expert_source.cpp src/core/remote_experts.cpp
src/core/remote_expert_opt.cu
src/core/expert_cache.cpp src/core/native_head.cpp src/core/native_dense.cpp src/core/verify.cpp src/core/mtp.cpp
src/core/conversation_snapshot.cpp src/core/conversation_state.cpp src/core/conversation_memory.cpp src/core/peer_experts.cpp)
src/core/conversation_snapshot.cpp src/core/conversation_state.cpp src/core/conversation_memory.cpp src/core/peer_experts.cpp
src/core/arch_defaults.cpp src/core/vmm.cpp src/core/conversation_file.cpp)
target_include_directories(strata_engine PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/include)
target_link_libraries(strata_engine PUBLIC strata_core strata_kernels strata_kernels_cpu ${_strata_gpu_runtime_target})
if(STRATA_BUILD_TESTS AND EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/tests/core/exchange_storage_test.cpp")
add_executable(exchange_storage_test tests/core/exchange_storage_test.cpp)
target_include_directories(exchange_storage_test PRIVATE include)
target_link_libraries(exchange_storage_test PRIVATE Threads::Threads)
add_test(NAME exchange_storage_test COMMAND exchange_storage_test)
set_tests_properties(exchange_storage_test PROPERTIES TIMEOUT 60)
endif()
if(EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/tests/core/file_expert_source_test.cpp")
enable_testing()
add_executable(file_expert_source_test tests/core/file_expert_source_test.cpp)
target_link_libraries(file_expert_source_test PRIVATE strata_engine)
add_test(NAME file_expert_source_test COMMAND file_expert_source_test)
set_tests_properties(file_expert_source_test PROPERTIES TIMEOUT 60)
add_executable(vmm_test tests/core/vmm_test.cpp)
target_link_libraries(vmm_test PRIVATE strata_engine)
add_test(NAME vmm_test COMMAND vmm_test)
set_tests_properties(vmm_test PROPERTIES TIMEOUT 60)
endif()
# #477: the learned expert profile (--expert-profile-save): its order and make_profile.py's file format (CPU only)
if(EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/tests/core/expert_profile_save_test.cpp")
@@ -384,6 +484,10 @@ if(STRATA_ENABLE_CUDA OR STRATA_ENABLE_HIP)
target_link_libraries(expert_cache_per_layer_test PRIVATE strata_engine)
add_test(NAME expert_cache_per_layer_test COMMAND expert_cache_per_layer_test)
set_tests_properties(expert_cache_per_layer_test PROPERTIES SKIP_RETURN_CODE 77 TIMEOUT 60)
add_executable(expert_cache_segmented_test tests/core/expert_cache_segmented_test.cpp)
target_link_libraries(expert_cache_segmented_test PRIVATE strata_engine)
add_test(NAME expert_cache_segmented_test COMMAND expert_cache_segmented_test)
set_tests_properties(expert_cache_segmented_test PROPERTIES SKIP_RETURN_CODE 77 TIMEOUT 60)
endif()
if(STRATA_BUILD_CONVERSATION_TESTS)
add_executable(conversation_snapshot_test src/core/conversation_snapshot_test.cpp)
@@ -410,6 +514,10 @@ if(STRATA_ENABLE_CUDA OR STRATA_ENABLE_HIP)
# against llama.cpp on the SAME ids and a tokenizer on one side of that comparison is a second variable.
# plan v0.3 P5: batched prompt processing (tensor-core GEMMs through cuBLAS).
add_library(strata_prefill STATIC src/prefill/gemm.cu src/prefill/kernels.cu src/prefill/prefill.cpp)
if(STRATA_ENABLE_HIP)
# #313: the RDNA3 WMMA GEMMs include hip_runtime.h: AMD builds only (a CUDA build never sees this file)
target_sources(strata_prefill PRIVATE src/prefill/wmma_gemm.cu)
endif()
target_include_directories(strata_prefill PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/include ${_strata_gpu_include_directories})
target_link_libraries(strata_prefill PUBLIC strata_engine ${_strata_gpu_blas_target} ${_strata_gpu_runtime_target})
if(STRATA_ENABLE_HIP AND STRATA_HIPBLASLT_AVAILABLE)
@@ -427,6 +535,18 @@ if(STRATA_ENABLE_CUDA OR STRATA_ENABLE_HIP)
if(STRATA_ENABLE_HIP)
enable_testing()
set(_strata_hip_tests)
add_executable(hip_prefill_gemm tests/hip/prefill_gemm.cpp)
target_link_libraries(hip_prefill_gemm PRIVATE strata_prefill strata_hip_runtime)
add_test(NAME hip_prefill_gemm COMMAND hip_prefill_gemm)
list(APPEND _strata_hip_tests hip_prefill_gemm)
add_executable(hip_gemm_f16_io_parity tests/hip/gemm_f16_io_parity.cpp)
target_link_libraries(hip_gemm_f16_io_parity PRIVATE strata_prefill strata_hip_runtime)
add_test(NAME hip_gemm_f16_io_parity COMMAND hip_gemm_f16_io_parity)
list(APPEND _strata_hip_tests hip_gemm_f16_io_parity)
add_executable(hip_q2_zero tests/hip/q2_zero.cpp)
target_link_libraries(hip_q2_zero PRIVATE strata_kernels strata_hip_runtime)
add_test(NAME hip_q2_zero COMMAND hip_q2_zero)
list(APPEND _strata_hip_tests hip_q2_zero)
add_executable(hip_expert_cache_staging tests/hip/expert_cache_staging.cpp)
target_link_libraries(hip_expert_cache_staging PRIVATE strata_engine)
add_test(NAME hip_expert_cache_staging COMMAND hip_expert_cache_staging)
@@ -464,6 +584,23 @@ if(STRATA_ENABLE_CUDA OR STRATA_ENABLE_HIP)
add_test(NAME hip_prefill_native_batch COMMAND hip_prefill_native_batch)
list(APPEND _strata_hip_tests hip_prefill_native_batch)
endif()
# Aurora: the prompt GDN recurrence's per-head kernel against the column-split + norm kernels, bitwise
add_executable(hip_gdn_rec_head tests/hip/gdn_rec_head.cpp)
target_link_libraries(hip_gdn_rec_head PRIVATE strata_prefill strata_hip_runtime)
add_test(NAME hip_gdn_rec_head COMMAND hip_gdn_rec_head)
list(APPEND _strata_hip_tests hip_gdn_rec_head)
# #313: the RDNA3 / RDNA3.5 WMMA dense GEMMs (opt-in STRATA_WMMA_GEMM=1) against a reference
add_executable(hip_prefill_wmma_gemm_parity tests/hip/prefill_wmma_gemm_parity.cpp)
target_include_directories(hip_prefill_wmma_gemm_parity PRIVATE "${CMAKE_CURRENT_SOURCE_DIR}/src/prefill")
target_link_libraries(hip_prefill_wmma_gemm_parity PRIVATE strata_prefill strata_hip_runtime)
add_test(NAME hip_prefill_wmma_gemm_parity COMMAND hip_prefill_wmma_gemm_parity)
list(APPEND _strata_hip_tests hip_prefill_wmma_gemm_parity)
# S (STRATA_HCD_EXACT): the exact HC down kernel against hipBLASLt solution 1176 / 1177 (needs STRATA_HIPBLASLT_TUNING; exit 77 else)
add_executable(hip_prefill_hcd_exact_parity tests/hip/prefill_hcd_exact_parity.cpp)
target_link_libraries(hip_prefill_hcd_exact_parity PRIVATE strata_prefill strata_hip_runtime)
add_test(NAME hip_prefill_hcd_exact_parity COMMAND hip_prefill_hcd_exact_parity)
set_tests_properties(hip_prefill_hcd_exact_parity PROPERTIES SKIP_RETURN_CODE 77)
list(APPEND _strata_hip_tests hip_prefill_hcd_exact_parity)
# S6: the AMD fast router against the portable kernel, bitwise
add_executable(hip_router_fast tests/hip/router_fast.cpp)
target_link_libraries(hip_router_fast PRIVATE strata_kernels strata_hip_runtime)
@@ -560,6 +697,10 @@ if(STRATA_ENABLE_CUDA OR STRATA_ENABLE_HIP)
target_include_directories(mmvq_multi_parity PRIVATE ${_strata_gpu_include_directories})
target_link_libraries(mmvq_multi_parity PRIVATE strata_kernels ${_strata_gpu_runtime_target})
add_test(NAME mmvq_multi_parity COMMAND mmvq_multi_parity)
if(STRATA_HIP_GFX906) # the CUDA layout, where the negative control has power (the wave64 one is exact at every T)
add_test(NAME mmvq_multi_parity_cuda_layout COMMAND mmvq_multi_parity)
set_tests_properties(mmvq_multi_parity_cuda_layout PROPERTIES ENVIRONMENT "STRATA_MMVQ_WAVE=0")
endif()
endif()
# the IQ kernels that decode a weight part once for every column/entry, bitwise vs the per-column ones
# (GPU, synthetic, no model; --bench adds old/new timings)
@@ -568,11 +709,26 @@ if(STRATA_ENABLE_CUDA OR STRATA_ENABLE_HIP)
target_link_libraries(iq_multi_parity PRIVATE strata_kernels ${_strata_gpu_runtime_target})
add_test(NAME iq_multi_parity COMMAND iq_multi_parity)
# the prompt path's DeltaNet recurrence: gdn_rec_kh_kernel against gdn_rec_cols_pipe_kernel, bitwise (GPU,
# synthetic, no model; --bench adds timings)
if(STRATA_ENABLE_CUDA)
# synthetic, no model; --bench adds timings). NVIDIA only: cp.async and the CUDA occupancy API (not on gfx906)
if(STRATA_ENABLE_CUDA AND NOT STRATA_HIP_GFX906)
# Independent pinned expert uploads: byte parity and stream/event ordering.
add_executable(dma_batch_parity tests/core/dma_batch_parity.cu)
target_include_directories(dma_batch_parity PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/include)
target_link_libraries(dma_batch_parity PRIVATE ${_strata_gpu_runtime_target})
add_test(NAME dma_batch_parity COMMAND dma_batch_parity)
set_tests_properties(dma_batch_parity PROPERTIES TIMEOUT 120)
if(WIN32)
add_test(NAME dma_batch_registered_parity COMMAND dma_batch_parity --registered)
set_tests_properties(dma_batch_registered_parity PROPERTIES TIMEOUT 120)
endif()
add_executable(gdn_rec_parity src/prefill/gdn_rec_parity.cu)
target_link_libraries(gdn_rec_parity PRIVATE ${_strata_gpu_runtime_target})
add_test(NAME gdn_rec_parity COMMAND gdn_rec_parity)
# the prompt path's BF16 products against cuBLAS's own BF16 GEMM, within fp32 rounding (sm_7x runs them on the
# FP16 tensor cores; GPU, synthetic, no model; --bench adds timings)
add_executable(gemm_bf16_parity src/prefill/gemm_bf16_parity.cu)
target_link_libraries(gemm_bf16_parity PRIVATE strata_prefill )
add_test(NAME gemm_bf16_parity COMMAND gemm_bf16_parity)
endif()
# native_expert_grouped's group stride and fused SwiGLU + q8_1, bitwise vs the launches before them (GPU, synthetic,
# no model; --bench adds v1/new timings of a window's 48 calls in a graph)
@@ -584,7 +740,17 @@ if(STRATA_ENABLE_CUDA OR STRATA_ENABLE_HIP)
target_include_directories(shared_expert_parity PRIVATE ${_strata_gpu_include_directories})
target_link_libraries(shared_expert_parity PRIVATE strata_kernels ${_strata_gpu_runtime_target})
add_test(NAME shared_expert_parity COMMAND shared_expert_parity --selftest)
# the split / batched verify-window kernels (from eddoursul's fork), bitwise vs the kernels they replace; CUDA only
if(STRATA_ENABLE_CUDA)
add_executable(verify_parity src/kernels/verify_parity.cpp)
target_include_directories(verify_parity PRIVATE ${_strata_gpu_include_directories})
target_link_libraries(verify_parity PRIVATE strata_kernels ${_strata_gpu_runtime_target})
add_test(NAME verify_parity COMMAND verify_parity --selftest)
endif()
add_executable(fused_gr_bench src/kernels/fused_gr_bench.cpp)
target_include_directories(fused_gr_bench PRIVATE ${_strata_gpu_include_directories})
target_link_libraries(fused_gr_bench PRIVATE strata_kernels ${_strata_gpu_runtime_target})
add_executable(gr_parity src/kernels/gr_parity.cpp)
target_include_directories(gr_parity PRIVATE ${_strata_gpu_include_directories})
target_link_libraries(gr_parity PRIVATE strata_kernels ${_strata_gpu_runtime_target})
@@ -629,10 +795,42 @@ if(STRATA_ENABLE_CUDA OR STRATA_ENABLE_HIP)
target_link_libraries(quantize_act_parity PRIVATE strata_kernels ${_strata_gpu_runtime_target})
add_test(NAME quantize_act_parity COMMAND quantize_act_parity --selftest)
# the verify window's routing in one multi-column BF16 projection + one top-k, bitwise the per-token calls
add_executable(route_window_parity src/kernels/route_window_parity.cpp)
target_include_directories(route_window_parity PRIVATE ${_strata_gpu_include_directories})
target_link_libraries(route_window_parity PRIVATE strata_kernels ${_strata_gpu_runtime_target})
add_test(NAME route_window_parity COMMAND route_window_parity)
add_executable(router_top10_parity src/kernels/router_top10_parity.cpp)
target_include_directories(router_top10_parity PRIVATE ${_strata_gpu_include_directories})
target_link_libraries(router_top10_parity PRIVATE strata_kernels ${_strata_gpu_runtime_target})
add_test(NAME router_top10_parity COMMAND router_top10_parity --selftest)
# #783 PR-c: the multi-token router and the k = 10 float4 combine, bitwise against the single-token contracts
add_executable(native_multi_parity src/kernels/native_multi_parity.cpp)
target_include_directories(native_multi_parity PRIVATE ${_strata_gpu_include_directories})
target_link_libraries(native_multi_parity PRIVATE strata_kernels ${_strata_gpu_runtime_target})
add_test(NAME native_multi_parity COMMAND native_multi_parity)
# #783 PR-d: the batched K/V append (INT8, Q4_0, the hybrid fold) against the per-cell appends, memcmp
add_executable(kv_steps_parity src/kernels/kv_steps_parity.cpp)
target_include_directories(kv_steps_parity PRIVATE ${_strata_gpu_include_directories})
target_link_libraries(kv_steps_parity PRIVATE strata_kernels ${_strata_gpu_runtime_target})
add_test(NAME kv_steps_parity COMMAND kv_steps_parity)
# #783 PR-e: the device-built expert plan (parallel grouping + prefix scan) against a host replay of its contract
add_executable(resident_plan_parity src/kernels/resident_plan_parity.cpp)
target_include_directories(resident_plan_parity PRIVATE ${_strata_gpu_include_directories})
target_link_libraries(resident_plan_parity PRIVATE strata_kernels ${_strata_gpu_runtime_target})
add_test(NAME resident_plan_parity COMMAND resident_plan_parity)
# #783 PR-g: the multi-token hyper-connection read/write (exact-T projections under them) against the single-token calls
add_executable(gr_multi_parity src/kernels/gr_multi_parity.cpp)
target_include_directories(gr_multi_parity PRIVATE ${_strata_gpu_include_directories})
target_link_libraries(gr_multi_parity PRIVATE strata_kernels ${_strata_gpu_runtime_target})
add_test(NAME gr_multi_parity COMMAND gr_multi_parity)
if(STRATA_HIP_GFX906)
# the S6 router (STRATA_HIP_ROUTER_FAST=1 on gfx906) against the portable kernel, bitwise, on wave64
add_executable(hip_router_fast tests/hip/router_fast.cpp)
target_include_directories(hip_router_fast PRIVATE ${_strata_gpu_include_directories})
target_link_libraries(hip_router_fast PRIVATE strata_kernels ${_strata_gpu_runtime_target})
endif()
add_executable(s_gemv_parity src/kernels/s_gemv_parity.cpp)
target_include_directories(s_gemv_parity PRIVATE ${_strata_gpu_include_directories})
@@ -659,6 +857,13 @@ if(STRATA_ENABLE_CUDA OR STRATA_ENABLE_HIP)
target_link_libraries(qsa_parity PRIVATE strata_kernels ${_strata_gpu_runtime_target})
add_test(NAME qsa_parity COMMAND qsa_parity --selftest)
# the QSA top-k the engine dispatches against the original kernel: identical ids on both sides of the register
# kernel's reach, with tied and with equal scores (synthetic, no model)
add_executable(qsa_topk_parity src/kernels/qsa_topk_parity.cpp)
target_include_directories(qsa_topk_parity PRIVATE ${_strata_gpu_include_directories})
target_link_libraries(qsa_topk_parity PRIVATE strata_kernels ${_strata_gpu_runtime_target})
add_test(NAME qsa_topk_parity COMMAND qsa_topk_parity --selftest)
# ---- P2.S4: the PLE n-gram hash, the IQ4_NL table read, and the PLE block ----
# `ngram.cpp` is host-only but lives in `strata_kernels` because `ple.cu` shares its geometry constants, and
# splitting them across two libraries would only add a layer.
@@ -785,9 +990,13 @@ endif()
# worse opt-out. `cpu_require_expert_support()` therefore refuses at startup on a CPU without the
# features, and the fallback exists for tests, exactly as P2.S3 specifies.
# Plan v0.3 P6: speculation components that need no GPU.
add_library(strata_spec STATIC src/spec/suffix_drafter.cpp src/spec/controller.cpp src/spec/draft_policy.cpp)
add_library(strata_spec STATIC src/spec/suffix_drafter.cpp src/spec/controller.cpp src/spec/draft_policy.cpp
src/spec/draft_source.cpp)
target_include_directories(strata_spec PUBLIC include)
if(STRATA_BUILD_TESTS)
add_executable(arch_defaults_test src/core/arch_defaults_test.cpp src/core/arch_defaults.cpp)
target_include_directories(arch_defaults_test PRIVATE include)
add_test(NAME arch_defaults_test COMMAND arch_defaults_test)
add_executable(suffix_drafter_test src/spec/suffix_drafter_test.cpp)
target_link_libraries(suffix_drafter_test PRIVATE strata_spec)
add_test(NAME suffix_drafter_test COMMAND suffix_drafter_test)
@@ -799,6 +1008,9 @@ if(STRATA_BUILD_TESTS)
add_test(NAME draft_policy_test COMMAND draft_policy_test)
endif()
if(STRATA_BUILD_TESTS OR STRATA_BUILD_CONVERSATION_TESTS)
add_executable(message_boundary_test src/program/message_boundary_test.cpp)
target_include_directories(message_boundary_test PRIVATE include)
add_test(NAME message_boundary_test COMMAND message_boundary_test)
# the serve loop's conversation-cache retention policy (header-only, no library to link)
add_executable(conv_cache_test src/program/conv_cache_test.cpp)
target_include_directories(conv_cache_test PRIVATE include)
@@ -827,6 +1039,34 @@ else()
PROPERTIES COMPILE_OPTIONS "-mavx512f;-mavx512bw;-mavx512vl;-mavx512dq;-mavx512vnni;-mavx512vbmi;-mf16c;-mfma")
set_source_files_properties(src/kernels/cpu/q2_avx2.cpp PROPERTIES COMPILE_OPTIONS "-mavx2;-mfma;-mf16c")
endif()
# AVX-VNNI (Alder Lake, Sapphire Rapids and later): the AVX-2 expert kernels' vpdpwssd / vpdpbusd copies (q2_avx2.cpp,
# iq_avx2.cpp; *_rows.inl), taken at run time where cpu_avxvnni_ok(). GCC and Clang compile only those functions with
# target("avxvnni") - never -mavxvnni on a file, which would let the compiler use it anywhere - and MSVC takes the
# intrinsics under /arch:AVX2. A compiler without them (GCC < 11, Clang < 12) builds the files without the copies.
include(CheckCXXSourceCompiles)
if(MSVC)
set(CMAKE_REQUIRED_FLAGS "/arch:AVX2")
else()
set(CMAKE_REQUIRED_FLAGS "-mavx2 -mfma -mf16c")
endif()
check_cxx_source_compiles([=[
#include <immintrin.h>
#if defined(__GNUC__) || defined(__clang__)
__attribute__((target("avxvnni")))
#endif
static __m256i dots(__m256i a, __m256i b) { return _mm256_dpbusd_avx_epi32(_mm256_dpwssd_avx_epi32(a, a, b), a, b); }
int main() {
volatile int x = 1;
return _mm256_extract_epi32(dots(_mm256_set1_epi32(x), _mm256_set1_epi32(x)), 0) == 0;
}
]=] STRATA_HAVE_AVXVNNI_INTRINSICS)
unset(CMAKE_REQUIRED_FLAGS)
if(STRATA_HAVE_AVXVNNI_INTRINSICS)
set(_strata_avxvnni STRATA_AVXVNNI=1)
else()
set(_strata_avxvnni STRATA_AVXVNNI=0)
endif()
set_property(SOURCE src/kernels/cpu/q2_avx2.cpp APPEND PROPERTY COMPILE_DEFINITIONS ${_strata_avxvnni})
find_package(Threads REQUIRED)
target_link_libraries(strata_kernels_cpu PUBLIC Threads::Threads)
@@ -857,6 +1097,22 @@ if(STRATA_NATIVE_EXPERTS)
foreach(_f GGML_AVX GGML_AVX2 GGML_FMA GGML_F16C GGML_BMI2)
set(${_f} ON CACHE BOOL "" FORCE)
endforeach()
elseif(STRATA_ISA_FLOOR)
# the experimental older-CPU build (STRATA_ISA_FLOOR above): one ggml-cpu for the floor, ggml's "sandybridge"
# feature set (SSE4.2 + AVX) or SSE4.2 alone - not the build host's, which may be newer than the PC it runs on.
# From the Strata_Dirigo fork (rwkeyes). ggml's defaults turn the rest on when GGML_NATIVE is off: all off here.
set(GGML_NATIVE OFF CACHE BOOL "" FORCE)
set(GGML_CPU_ALL_VARIANTS OFF CACHE BOOL "" FORCE)
set(GGML_SSE42 ON CACHE BOOL "" FORCE)
if(STRATA_ISA_FLOOR STREQUAL "avx")
set(GGML_AVX ON CACHE BOOL "" FORCE)
else()
set(GGML_AVX OFF CACHE BOOL "" FORCE)
endif()
foreach(_f GGML_AVX2 GGML_FMA GGML_F16C GGML_BMI2 GGML_AVX_VNNI GGML_AVX512 GGML_AVX512_VBMI GGML_AVX512_VNNI
GGML_AVX512_BF16 GGML_AMX_TILE GGML_AMX_INT8 GGML_AMX_BF16)
set(${_f} OFF CACHE BOOL "" FORCE)
endforeach()
else()
set(GGML_NATIVE ON CACHE BOOL "" FORCE)
endif()
@@ -877,20 +1133,51 @@ if(STRATA_NATIVE_EXPERTS)
# part of strata_kernels_cpu (the pool calls these and they call its CPU probe: one library, so GNU ld's
# one-pass static linking has no cycle to resolve)
target_sources(strata_kernels_cpu PRIVATE src/kernels/cpu/native_expert.cpp src/kernels/cpu/iq_avx512.cpp
src/kernels/cpu/iq_avx2.cpp src/kernels/cpu/kq_avx2.cpp)
src/kernels/cpu/iq_avx2.cpp src/kernels/cpu/kq_avx2.cpp
src/kernels/cpu/kq_avx1.cpp)
target_include_directories(strata_kernels_cpu PRIVATE third_party/ggml)
if(MSVC)
set_source_files_properties(src/kernels/cpu/iq_avx512.cpp PROPERTIES COMPILE_OPTIONS "/arch:AVX512")
set_source_files_properties(src/kernels/cpu/iq_avx2.cpp PROPERTIES COMPILE_OPTIONS "/arch:AVX2")
set_source_files_properties(src/kernels/cpu/kq_avx2.cpp PROPERTIES COMPILE_OPTIONS "/arch:AVX2")
# the router lookahead's AVX1 dot (older CPUs; called behind cpu_avx1_ok): no FMA, no 256-bit integer ops
set_source_files_properties(src/kernels/cpu/kq_avx1.cpp PROPERTIES COMPILE_OPTIONS "/arch:AVX")
else()
set_source_files_properties(src/kernels/cpu/iq_avx512.cpp PROPERTIES COMPILE_OPTIONS
"-mavx512f;-mavx512bw;-mavx512vl;-mavx512dq;-mavx512vnni;-mavx512vbmi;-mf16c;-mfma")
set_source_files_properties(src/kernels/cpu/iq_avx2.cpp PROPERTIES COMPILE_OPTIONS "-mavx2;-mfma;-mf16c")
set_source_files_properties(src/kernels/cpu/kq_avx2.cpp PROPERTIES COMPILE_OPTIONS "-mavx2;-mfma;-mf16c")
set_source_files_properties(src/kernels/cpu/kq_avx1.cpp PROPERTIES COMPILE_OPTIONS "-mavx")
endif()
set_property(SOURCE src/kernels/cpu/iq_avx2.cpp APPEND PROPERTY COMPILE_DEFINITIONS ${_strata_avxvnni})
if(STRATA_ISA_FLOOR STREQUAL "avx")
target_compile_definitions(strata_kernels_cpu PRIVATE STRATA_ISA_FLOOR_AVX=1)
elseif(STRATA_ISA_FLOOR STREQUAL "none")
target_compile_definitions(strata_kernels_cpu PRIVATE STRATA_ISA_FLOOR_NONE=1)
endif()
target_link_libraries(strata_kernels_cpu PUBLIC ggml-cpu ggml-base)
target_compile_definitions(strata_kernels_cpu PUBLIC STRATA_NATIVE_EXPERTS=1)
if(STRATA_BUILD_TESTS)
# the Q5_1 block dequantizer (a Q5_1 PLE table) against ggml's, synthetic blocks: no model, no GPU
add_executable(dequant_q5_1_test tests/core/dequant_q5_1_test.cpp)
target_link_libraries(dequant_q5_1_test PRIVATE strata_artifact ggml-base)
add_test(NAME dequant_q5_1_test COMMAND dequant_q5_1_test)
endif()
# the router lookahead's dot on each rung this CPU has (AVX2, the older CPUs' AVX1) against a double reference
add_executable(router_dot_parity src/kernels/cpu/router_dot_parity.cpp)
target_link_libraries(router_dot_parity PRIVATE strata_kernels_cpu)
if(STRATA_BUILD_TESTS)
add_test(NAME router_dot_parity COMMAND router_dot_parity)
add_test(NAME router_dot_parity_avx COMMAND ${CMAKE_COMMAND} -E env STRATA_FORCE_ISA=avx $<TARGET_FILE:router_dot_parity>)
endif()
# the AVX-2 expert kernels' variants (iq_avx2.cpp, q2_avx2.cpp) bit for bit against each other at 1..8 tokens and
# against ggml-cpu's dot, on random blocks; --bench: ms per expert on one thread, the weights streamed from DRAM
add_executable(iq_avx2_parity src/kernels/cpu/iq_avx2_parity.cpp)
target_link_libraries(iq_avx2_parity PRIVATE strata_kernels_cpu)
if(STRATA_BUILD_TESTS)
add_test(NAME iq_avx2_parity COMMAND iq_avx2_parity)
add_test(NAME iq_avx2_parity_iq3s_mt1 COMMAND ${CMAKE_COMMAND} -E env STRATA_IQ3S_MT1=1 $<TARGET_FILE:iq_avx2_parity>)
endif()
set(_strata_mmq_types q2_0 iq2_xxs iq2_xs iq2_s iq3_xxs iq3_s iq4_nl iq4_xs q8_0)
# #296: OrcaRouter Q4_K_S's Q5_0 experts (Q4_K and Q5_1 come with STRATA_MMQ_KQUANTS on CUDA, S5's branch)
if(STRATA_ORCA_Q4KS_MMQ)
@@ -912,10 +1199,13 @@ if(STRATA_NATIVE_EXPERTS)
src/prefill/ggml_cuda_host.cu ${_strata_mmq}/quantize.cu)
# q8_0: the drafter's matrices (E-9); STRATA_ORCA_Q4KS_MMQ adds q5_0 (#296, in _strata_mmq_types), and
# STRATA_MMQ_KQUANTS q4_k q5_k q5_1, the experts of Unsloth's UD-Q4_K_XL (docs/UNSLOTH_Q4.md; the dispatch
# follows eddoursul/Strata 7706c1b)
# follows eddoursul/Strata 7706c1b); q6_k is HIP only
set(_strata_mmq_cuda_types ${_strata_mmq_types})
if(STRATA_MMQ_KQUANTS)
list(APPEND _strata_mmq_cuda_types q4_k q5_k q5_1)
if(STRATA_Q6K_EXPERTS) # UD-Q6_K_XL gate/up (docs/UNSLOTH_Q6.md)
list(APPEND _strata_mmq_cuda_types q6_k)
endif()
endif()
foreach(_t IN LISTS _strata_mmq_cuda_types)
target_sources(strata_mmq PRIVATE ${_strata_mmq}/template-instances/mmq-instance-${_t}.cu)
@@ -966,25 +1256,52 @@ if(STRATA_NATIVE_EXPERTS)
if(NOT EXISTS "${_strata_mmq}/common.cuh" OR NOT EXISTS "${_strata_mmq}/quantize.cu")
message(FATAL_ERROR "The selected GGML source is missing the ggml-cuda MMQ sources: ${_strata_mmq}")
endif()
# Aurora: moe_fused.cu's and moe_fused_iq.cu's gfx11 WMMA kernels for the prompt experts (opt-in STRATA_PF_FUSED=1)
set(_strata_mmq_sources
src/prefill/moe_mmq.cu
src/prefill/moe_fused.cu
src/prefill/moe_fused_iq.cu
src/prefill/ggml_cuda_host.cu
"${_strata_mmq}/quantize.cu")
if(STRATA_MMQ_KQUANTS)
# the same K-quant instances as the CUDA branch: on HIP they serve the dense GGUF projections
# of Gemm::native (STRATA_DENSE_MMQ=1), the mixed-quant packs' attention/PLE tensors
list(APPEND _strata_mmq_types q4_k q5_k q5_1 q6_k)
endif()
foreach(_t IN LISTS _strata_mmq_types)
list(APPEND _strata_mmq_sources "${_strata_mmq}/template-instances/mmq-instance-${_t}.cu")
endforeach()
set_source_files_properties(${_strata_mmq_sources} PROPERTIES LANGUAGE HIP)
# moe_fused.cu is Strata's own CUDA-shaped source: it takes the HIP compat headers, ggml's MMQ sources do not
set_source_files_properties(src/prefill/moe_fused.cu src/prefill/moe_fused_iq.cu
PROPERTIES INCLUDE_DIRECTORIES "${STRATA_HIP_COMPAT_INCLUDE_DIR}")
add_library(strata_mmq STATIC ${_strata_mmq_sources})
target_include_directories(strata_mmq
PRIVATE "${_strata_gg}/include" "${_strata_gg}/src" "${_strata_mmq}"
PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/include)
target_compile_definitions(strata_mmq PRIVATE GGML_USE_HIP=1)
if(STRATA_MMQ_KQUANTS)
target_compile_definitions(strata_mmq PRIVATE STRATA_MMQ_KQUANTS=1)
endif()
if(STRATA_ORCA_Q4KS_MMQ)
target_compile_definitions(strata_mmq PRIVATE STRATA_ORCA_Q4KS_MMQ=1)
endif()
target_link_libraries(strata_mmq PUBLIC ggml-base hip::host roc::hipblas)
target_link_libraries(strata_prefill PUBLIC strata_mmq)
target_compile_definitions(strata_prefill PUBLIC STRATA_PREFILL_MMQ=1)
target_compile_definitions(strata_prefill PUBLIC STRATA_PREFILL_MMQ=1 STRATA_PREFILL_FUSED=1)
# Aurora: the gfx11 fused experts against the FP64 reference and MMQ (tests/cuda's test; exit 77 off gfx11)
add_executable(hip_prefill_fused_moe tests/cuda/prefill_fused_moe_test.cpp)
target_include_directories(hip_prefill_fused_moe PRIVATE "${_strata_gg}/include")
target_link_libraries(hip_prefill_fused_moe PRIVATE strata_mmq ggml-base strata_hip_runtime)
add_test(NAME hip_prefill_fused_moe COMMAND hip_prefill_fused_moe)
set_tests_properties(hip_prefill_fused_moe PROPERTIES SKIP_RETURN_CODE 77 TIMEOUT 300)
list(APPEND _strata_hip_tests hip_prefill_fused_moe)
add_executable(hip_prefill_fused_iq tests/cuda/prefill_fused_iq_test.cpp)
target_include_directories(hip_prefill_fused_iq PRIVATE "${_strata_gg}/include")
target_link_libraries(hip_prefill_fused_iq PRIVATE strata_mmq ggml-base strata_hip_runtime)
add_test(NAME hip_prefill_fused_iq COMMAND hip_prefill_fused_iq)
set_tests_properties(hip_prefill_fused_iq PROPERTIES SKIP_RETURN_CODE 77 TIMEOUT 600)
list(APPEND _strata_hip_tests hip_prefill_fused_iq)
# Numerical diagnostic: synthetic layouts by default, optional first-expert IQ3_XXS/IQ4_NL check with a pack.
add_executable(hip_prefill_mmq_parity tests/hip/prefill_mmq_parity.cpp)
@@ -1003,6 +1320,9 @@ if(STRATA_NATIVE_EXPERTS)
add_executable(native_expert_parity src/kernels/native_expert_parity.cpp)
target_include_directories(native_expert_parity PRIVATE ${_strata_gpu_include_directories})
target_link_libraries(native_expert_parity PRIVATE strata_kernels strata_kernels_cpu strata_artifact ${_strata_gpu_runtime_target})
add_executable(native_expert_bench src/kernels/native_expert_bench.cpp)
target_include_directories(native_expert_bench PRIVATE ${_strata_gpu_include_directories})
target_link_libraries(native_expert_bench PRIVATE strata_kernels strata_kernels_cpu strata_artifact ${_strata_gpu_runtime_target})
if(STRATA_BUILD_TESTS AND STRATA_ENABLE_CUDA)
# Unsloth UD-Q4_K_XL's three expert pairs (and Q8_0 alone, its embedding) on synthetic ggml-quantized weights
# at 2560/640: GPU and CPU experts against the float reference, GPU dequantizers against to_float.
@@ -1011,7 +1331,17 @@ if(STRATA_NATIVE_EXPERTS)
# kernel for it before; q5_0/q5_0 (both roles Q5_0) is left out of this list since stacking the same
# lower-precision format on both roles pushes the GPU path over this test's 3% threshold (quantization
# noise, not a kernel bug -- see the PR).
foreach(_pair q4_K/q5_1 q4_K/q8_0 q5_K/q8_0 q8_0/q8_0 q4_K/q5_0 q5_0/q8_0)
# q4_0/q4_0 and q4_0/q4_1: a plain llama-quantize Q4_0 file (bartowski's Qwen3.8-Flash-Next-Q4_0: gate/up Q4_0,
# down Q4_0 on 42 layers and Q4_1 on 6)
# q6_K/q8_0: Unsloth UD-Q6_K_XL gate/up Q6_K with Q8_0 down (docs/UNSLOTH_Q6.md). Layer 2 is q8_0/q8_0, already listed.
set(_strata_parity_pairs q4_K/q5_1 q4_K/q8_0 q5_K/q8_0 q8_0/q8_0 q4_K/q5_0 q5_0/q8_0 q4_0/q4_0 q4_0/q4_1)
if(STRATA_Q6K_EXPERTS)
list(APPEND _strata_parity_pairs q6_K/q8_0)
else() # a pair without GPU kernels (Q6_K gate/up) is refused by native_expert_supported, not run
add_test(NAME native_expert_parity_refuses_q6_K COMMAND native_expert_parity --synthetic q6_K/q8_0)
set_tests_properties(native_expert_parity_refuses_q6_K PROPERTIES WILL_FAIL TRUE)
endif()
foreach(_pair ${_strata_parity_pairs})
string(REPLACE "/" "_" _name "${_pair}")
add_test(NAME native_expert_parity_${_name} COMMAND native_expert_parity --synthetic ${_pair})
set_tests_properties(native_expert_parity_${_name} PROPERTIES TIMEOUT 120)
@@ -1019,9 +1349,6 @@ if(STRATA_NATIVE_EXPERTS)
add_test(NAME native_expert_parity_q5_1_min COMMAND native_expert_parity --q5_1-min)
# #290: the BF16 token embedding (--embd-gguf) gathered and dequantized bit for bit
add_test(NAME native_expert_parity_bf16_embd COMMAND native_expert_parity --bf16-embd)
# a pair without GPU kernels (Q6_K gate/up) is refused by native_expert_supported, not run
add_test(NAME native_expert_parity_refuses_q6_K COMMAND native_expert_parity --synthetic q6_K/q8_0)
set_tests_properties(native_expert_parity_refuses_q6_K PROPERTIES WILL_FAIL TRUE)
endif()
endif()
# #296's real-weight checks (OrcaRouter Q4_K_S files; docs/ORCA_Q4_K_S.md) - test programs, not part of a release
@@ -1043,6 +1370,15 @@ if(STRATA_ENABLE_CUDA AND STRATA_BUILD_TESTS AND EXISTS "${CMAKE_CURRENT_SOURCE_
endif()
if(STRATA_BUILD_TESTS)
if(WIN32)
add_executable(pool_affinity_test src/kernels/cpu/pool_affinity_test.cpp)
target_link_libraries(pool_affinity_test PRIVATE strata_kernels_cpu)
add_test(NAME pool_affinity_test COMMAND pool_affinity_test)
elseif(CMAKE_SYSTEM_NAME STREQUAL "Linux")
add_executable(pool_affinity_test src/kernels/cpu/pool_affinity_linux_test.cpp)
target_link_libraries(pool_affinity_test PRIVATE strata_kernels_cpu)
add_test(NAME pool_affinity_test COMMAND pool_affinity_test)
endif()
# Plan v0.3 P6: one expert applied to several tokens, bitwise vs the single-token kernel (synthetic, CPU only).
# plan v0.3 P5: the device dequantizers vs the artifact's CPU dequantizers on the real shards (not a ctest: needs
# the 38 GB model; run it by hand or from tools/gpu_session.py)
@@ -1055,6 +1391,14 @@ if(STRATA_BUILD_TESTS)
target_link_libraries(expert_multi_test PRIVATE strata_kernels_cpu)
add_test(NAME expert_multi_test COMMAND expert_multi_test)
# PR #706: the opt-in AVX-2 Q2_0 bit-plane kernel (STRATA_Q2_BITPLANE=1) against the legacy one, bitwise alone/window
if(STRATA_BUILD_TESTS AND CMAKE_SYSTEM_PROCESSOR MATCHES "AMD64|x86_64|x64|X86")
add_executable(q2_bitplane_parity src/kernels/cpu/q2_bitplane_parity.cpp)
target_link_libraries(q2_bitplane_parity PRIVATE strata_kernels_cpu)
add_test(NAME q2_bitplane_parity COMMAND q2_bitplane_parity)
set_tests_properties(q2_bitplane_parity PROPERTIES ENVIRONMENT "STRATA_Q2_BITPLANE=1")
endif()
add_executable(expert_parity src/kernels/cpu/expert_parity.cpp)
target_link_libraries(expert_parity PRIVATE strata_kernels_cpu)
# The pack is a REQUIRED input, not an optional one: this test compares against a real expert blob, and a
@@ -1085,3 +1429,15 @@ if(STRATA_BUILD_TESTS)
add_subdirectory(tests)
endif()
endif()
# ---- HIP gfx906: every CUDA source of the tree (and the ggml-cuda MMQ sources) is compiled as HIP ----
if(STRATA_HIP_GFX906)
file(GLOB_RECURSE _strata_cu CONFIGURE_DEPENDS ${CMAKE_CURRENT_SOURCE_DIR}/src/*.cu ${CMAKE_CURRENT_SOURCE_DIR}/bench/micro/*.cu
${CMAKE_CURRENT_SOURCE_DIR}/tools/vision/*.cu)
if(_strata_mmq)
file(GLOB _strata_mmq_cu ${_strata_mmq}/*.cu ${_strata_mmq}/template-instances/*.cu)
list(APPEND _strata_cu ${_strata_mmq_cu})
target_compile_definitions(strata_mmq PRIVATE GGML_USE_HIP)
endif()
set_source_files_properties(${_strata_cu} PROPERTIES LANGUAGE HIP)
endif()
+222
View File
@@ -0,0 +1,222 @@
<h1 align="center">Strata</h1>
[English](README.md) · [简体中文](README.zh-CN.md) · [日本語](README.ja.md) · **Deutsch** · [Français](README.fr.md) · [Español](README.es.md) · [Português](README.pt-BR.md)
<p align="center"><b>Ein KI-Modell mit 125 Milliarden Parametern auf deinem eigenen Gaming-PC</b><br>
NVIDIA- oder AMD-Grafikkarte (ab 12 GB) · Windows oder Linux · kostenlos und Open Source</p>
<p align="center"><a href="https://github.com/Niko1221/Strata/releases/download/v0.1.10/Pagoda.mp4"><img src="docs/media/pagoda-preview.webp" width="720" alt="Ein Voxel-Pagodengarten, den das Modell in Strata geschrieben hat, läuft im Browser"></a><br>
<sub>Ein Voxel-Pagodengarten aus einem einzigen Prompt, auf einer RTX 5070 mit Strata (IQ3_S, 128K Kontext) ·
<a href="https://github.com/Niko1221/Strata/releases/download/v0.1.10/Pagoda.mp4">ganzes Video (49 s)</a></sub></p>
Strata lässt **[Qwen3.8-Flash-Next](https://huggingface.co/Qwen/Qwen3.8-Flash-Next)** auf einem normalen PC laufen.
Das ist ein großes, kluges KI-Modell, das sonst meist einen Server braucht. Es chattet, schreibt Code, versteht
Bilder und arbeitet mit deinen Apps und Coding-Agents zusammen. Nichts verlässt deinen PC.
## Wie schnell ist es?
Wir haben es auf zwei ganz normalen Gaming-PCs gemessen. Ein Token ist etwa ¾ eines Wortes.
- **Schreibt Antworten:** wie schnell die Antwort in einem kurzen Chat erscheint. 60 Tokens pro Sekunde sind
schneller, als du lesen kannst.
- **Liest deinen Prompt:** wie schnell es aufnimmt, was du schickst (hier ein Dokument, Code oder Chatverlauf mit
32K Tokens).
<table>
<tr><th>NVIDIA: RTX 5070 (12 GB), Ryzen 5 7600, 64 GB RAM</th><th>AMD: RX 9070 XT (16 GB), Ryzen 9 3900X, 47 GB RAM</th></tr>
<tr><td>
| Größe | Schreibt Antworten | Liest deinen Prompt |
| --- | ---: | ---: |
| **Q2_0** | 94 Tokens/s | 2,650 Tokens/s |
| **IQ2_XS** | 79 Tokens/s | 2,090 Tokens/s |
| **IQ3_XXS** | 62 Tokens/s | 1,750 Tokens/s |
| **IQ3_S** | 53 Tokens/s | 1,620 Tokens/s |
| **Coder** | 55 Tokens/s | 2,180 Tokens/s |
</td><td>
| Größe | Schreibt Antworten | Liest deinen Prompt |
| --- | ---: | ---: |
| **Q2_0** | 60 Tokens/s | 1,160 Tokens/s |
| **IQ2_XS** | 52 Tokens/s | 1,110 Tokens/s |
| **Coder** | 44 Tokens/s | 1,420 Tokens/s |
</td></tr>
</table>
NVIDIA: Q2_0 mit Engine 0.1.36, die anderen Zeilen mit 0.1.26 (4K Antworten, 32K Prompts). Die vollständigen
Tabellen stehen in [DETAILS.md](docs/DETAILS.md#speed-measured). Eine Karte mit mehr VRAM ist schneller: Eine
RTX 3090 (24 GB) sollte etwa 100-140 Tokens pro Sekunde schreiben. Lange Chats und andere Karten:
[Tempo jedes Modells](docs/MODELS.md#how-fast-is-each-size), [Ergebnisse aus der Community](docs/COMMUNITY_BENCHMARKS.md).
<p align="center"><a href="https://buymeacoffee.com/strataengine"><img src="https://cdn.buymeacoffee.com/buttons/v2/default-yellow.png" alt="Buy Me A Coffee" height="50"></a><br>
<sub>Strata ist kostenlos. Wenn es auf deinem PC gut läuft, hält ein Kaffee die Arbeit daran am Laufen.</sub></p>
## Was du brauchst
| | |
| --- | --- |
| **Grafikkarte** | **NVIDIA** GeForce RTX 20, 30, 40 oder 50 Serie, oder **AMD** Radeon RX 7900 XT / XTX, RX 7800 XT / 7700 XT, RX 9060 XT, RX 9070 / 9070 XT, Radeon AI PRO R9700 oder RX 6800 / 6900 Serie. Sie braucht **12 GB VRAM oder mehr**. |
| **RAM** | 32 GB oder mehr. Dein RAM entscheidet, [welches Modell](#welches-modell-soll-ich-nehmen) passt. Mit 64 GB laufen alle Größen. |
| **Festplatte** | Etwa 80 GB frei. Nimm wenn möglich eine SSD: Der erste Start geht dann viel schneller. |
| **System** | Windows 10 / 11 oder Linux und ein aktueller Grafiktreiber von NVIDIA oder AMD. |
Alles andere richtet der Installer ein. Zwei oder drei Karten können sich das Modell teilen ([Multi-GPU](docs/MULTI_GPU.md)).
Experimentell, von Community-Mitgliedern auf ihren eigenen Rechnern geschrieben und getestet:
- **Ältere Grafikkarten** (Tesla P40 / V100, GTX 10, Radeon VII / MI50, RX 6700 XT, RX 5500 XT): [Ältere GPUs](docs/OLDER_GPUS.md).
- **Intel Arc**, unter Linux aus dem Quellcode gebaut: [Intel Arc](docs/INTEL_ARC.md).
- **AMD Ryzen AI Max (Strix Halo)**, unter Linux aus dem Quellcode gebaut: [Strix Halo](docs/STRIX_HALO.md).
- **Ältere Prozessoren ohne AVX2**: Sie funktionieren, aber langsam. [Ältere CPUs](docs/INSTALL.md#older-cpus-experimental).
Die vollständige Liste: [docs/INSTALL.md](docs/INSTALL.md#what-you-need).
## Installieren
### Lass deine KI es einrichten
Nutzt du einen KI-Coding-Assistenten (Claude Code, Cursor, Codex, GitHub Copilot, ...)? Füge dort diesen Text ein:
```text
Set up Strata on this PC for me: https://github.com/Niko1221/Strata - follow docs/AI_SETUP.md in that repository.
```
Er prüft deine Grafikkarte, deinen RAM und deine Festplatte und wählt das passende Modell. Dann installiert und
startet er es und sagt dir, wie du deine Apps verbindest. KI-Tools können Strata auch über seinen
[MCP-Server](docs/MCP_SERVER.md) installieren, starten und stoppen.
### Oder mach es selbst
[Lade Strata herunter](https://github.com/Niko1221/Strata/archive/refs/heads/main.zip) und entpacke es (oder nutze
`git clone`). **Windows:** Doppelklick auf **`START-HERE.bat`**. **Linux:** Führe im Strata-Ordner **`./setup.sh`** aus.
Die Schritte sind für NVIDIA und AMD gleich. Der Installer erkennt deine Karte und richtet die passende Engine
dafür ein. Er stellt dir ein paar Fragen:
- welches Modell und welche Größe,
- wie viel Kontext (wie viel Text sich das Modell merkt),
- ob es Bilder lesen soll.
Drück jedes Mal Enter, um die empfohlene Antwort zu nehmen. Dann lädt er das Modell herunter (etwa 70 GB) und
startet es. Bricht der Download ab, starte ihn einfach neu: Er macht dort weiter, wo er aufgehört hat. Dein Browser
öffnet die Strata-App unter `http://127.0.0.1:8080`.
> **Während das Modell startet, kann dein PC 1-3 Minuten langsam sein oder nicht reagieren** (beim ersten Mal am
> längsten). Strata lädt 35-55 GB in deinen RAM und reserviert einen Teil davon für die Grafikkarte. Das ist normal.
> Warte und schließ das Fenster nicht. Im Fenster siehst du, was Strata gerade tut.
**Beim nächsten Mal** startest du wieder `START-HERE.bat` (oder `./setup.sh`). Es startet sofort und lädt nichts
doppelt herunter. Schließ das Fenster, um das Modell zu stoppen. `UPDATE.bat` (`./update.sh`) aktualisiert Strata,
ohne es zu starten. Updates, Docker, mehrere Karten, wo die Dateien landen und alle Optionen:
[docs/INSTALL.md](docs/INSTALL.md).
## Welches Modell soll ich nehmen?
Der Installer empfiehlt eins passend zu deinem RAM. Dasselbe Modell gibt es in mehreren Größen, mehr oder weniger
stark komprimiert. Kleinere Größen sind schneller. Größere sind etwas klüger.
| Dein RAM | Nimm | Warum |
| --- | --- | --- |
| **32 GB** | **Coder** | passt in 32 GB und ist für Code gemacht (mit einer 24-GB-Karte laufen auch Q2_0 und IQ2_XS) |
| **48 GB** | **IQ2_XS** (oder Q2_0, das schnellste) | die größeren Größen passen nicht |
| **64 GB** | **IQ2_XS** (empfohlen) oder IQ3_XXS / IQ3_S | alle Größen passen; IQ3_S ist das beste und das langsamste |
| **96 GB oder mehr** | **IQ3_S** oder Unsloths UD-IQ4_XS (~4 Bit) | Platz für die größten Größen, auch wenn alles andere offen ist |
- **[Coder](docs/MODELS.md#coder):** eine Version für Code, bei der die Hälfte der Experten entfernt wurde. Sie
erreicht 91 % des SWE-bench-Verified-Werts des vollen Modells (von ihren Autoren gemessen) und passt in 32 GB RAM.
Außerhalb von Code ist sie schwächer, auch bei chinesischem und anderem CJK-Text (#438). Nimm dafür Q2_0, IQ2_XS
oder IQ3_S, die alle Experten behalten.
- **[Swift 1.5](docs/MODELS.md#swift-15):** ein Fine-Tune, das viel kürzer nachdenkt, bevor es antwortet. Du
bekommst die Antwort früher, bei etwa gleicher Qualität.
- **[Unsloth UD-IQ4_XS](docs/MODELS.md#unsloth-ud-iq4_xs):** die ~4-Bit-Version von Unsloth, in der Qualität
zwischen IQ3_S und UD-Q4_K_XL. 94 GB Download. Mit weniger als ~80 GB RAM liest Strata einen Teil davon während
der Antwort von der SSD, dort ist es also langsamer (eine NVMe-SSD hilft).
- **[Unsloth UD-Q4_K_XL](docs/MODELS.md#unsloth-ud-q4_k_xl-experimental)** (experimentell): am nächsten am vollen
Modell. Aber Strata liest das meiste davon während der Antwort von der SSD, deshalb schreibt es auf einem PC mit
64 GB nur 7-8.5 Tokens/s.
- **[OrcaRouter's Uncensored IQ3_XXS](docs/MODELS.md#orcarouter-uncensored-iq3_xxs):** Das richtest du von Hand
ein. Es steht nicht im Menü des Installers.
Größen, Downloads und was wohin passt: [docs/MODELS.md](docs/MODELS.md). Um später ein weiteres Modell
hinzuzufügen, starte `SETUP.bat` (Linux: `./setup.sh --setup`).
## So benutzt du es
<p align="center"><img src="docs/media/runpagoda.png" width="900" alt="Der Monitor-Tab der Strata-App neben einem Coding-Agent"><br>
<sub>Der <b>Monitor</b> der Strata-App (links), während ein Coding-Agent den Pagodengarten aus dem Video schreibt (rechts)</sub></p>
- **Im Browser:** Öffne `http://127.0.0.1:8080`. Dort gibt es **Chat**, einen Live-**Monitor** für das Modell und
deine GPU/CPU/RAM und **About** mit den Einstellungen und Adressen.
- **Deine Apps und Coding-Agents:** Füge einen „OpenAI-kompatiblen“ Anbieter mit der Basis-URL
**`http://127.0.0.1:8080/v1`** hinzu. Jeder API-Key und jeder Modellname funktioniert.
- Apps, die die API von Anthropic nutzen: `http://127.0.0.1:8080/v1/messages` (Claude Code:
`ANTHROPIC_BASE_URL=http://127.0.0.1:8080`).
- Codex CLI und andere Apps, die die OpenAI Responses API nutzen: `/v1/responses`
([Einrichtung](docs/DETAILS.md#the-responses-api-and-codex-cli)).
- **Nachdenken:** Wähle **aus, niedrig, mittel oder hoch** im Chat-Menü oder unter „Reasoning Effort“ in deiner
App. Aus ist am schnellsten. Hoch ist am besten für schwierige Fragen.
- **Bilder:** Antworte im Setup mit Ja auf „Images?“. Klick dann im Chat auf **Picture** oder hänge Bilder in deiner
App an. AMD-Karten lesen Bilder unter Linux über den Prozessor; unter Windows geht das noch nicht.
- **Vom Handy oder einem anderen PC:** `START-HERE.bat --setup --host 0.0.0.0 --api-key <secret>`. Setz immer einen
Key.
- **Eine Anfrage nach der anderen:** Standardmäßig beantwortet Strata eine Anfrage, die anderen warten. Um mehrere
gleichzeitig zu beantworten, setz `"parallel": 2` ([BATCHING.md](docs/BATCHING.md)). Auf einer 12-GB-Karte wird
dadurch jede Antwort langsamer.
- **Lange Prompts:** Strata liest die erste Nachricht eines Chats komplett, etwa 1 Minute pro 30,000 Tokens.
Folgenachrichten starten in Sekunden.
Mehr: [wo deine Chats gespeichert werden](docs/INSTALL.md#where-things-are-stored), [die API](docs/DETAILS.md#using-it).
## Etwas ist schiefgelaufen?
- **Mein PC ist beim ersten Start von Strata eingefroren.** Das ist normal, während das Modell lädt. Warte und
schließ das Fenster nicht. Nach 10 Minuten immer noch eingefroren? Starte den PC neu, schließ andere Programme und
versuch es noch einmal, oder nimm eine kleinere Größe.
- **Es hat beim Herunterladen oder Installieren aufgehört.** Starte `START-HERE.bat` (oder `./setup.sh`) noch
einmal. Es macht dort weiter, wo es aufgehört hat.
- **Es ist sehr langsam und die Festplattenlampe blinkt ständig, oder es meldet „the engine stopped
unexpectedly“.** Dein PC hat nicht genug freien RAM. Schließ andere Programme (Browser brauchen viel) oder nimm
eine kleinere Größe (Q2_0 oder IQ2_XS).
- **Es meldet, dass Port 8080 schon belegt ist.** Strata läuft bereits. Such nach seinem Fenster.
Weitere Probleme und ihre Lösungen: [docs/TROUBLESHOOTING.md](docs/TROUBLESHOOTING.md). Kommst du nicht weiter?
Öffne ein [Issue](https://github.com/Niko1221/Strata/issues) und hänge `strata-<model>.log` aus dem Strata-Ordner
an. Du hast ein Sicherheitsproblem gefunden? Melde es vertraulich: [SECURITY.md](SECURITY.md).
## Wie funktioniert es?
Modelle wie dieses laufen normalerweise auf Servern mit Hunderten Gigabyte Grafikspeicher. Deine Grafikkarte hat
12-24 GB. Strata bringt das Modell trotzdem unter, indem es **die Arbeit auf deinen ganzen PC verteilt**. Stell dir
eine Küche vor: Was du ständig brauchst, bleibt auf der Arbeitsfläche, der Rest wartet in der Vorratskammer.
<p align="center"><img src="docs/media/how-it-works.svg" width="860" alt="Die 24,576 Experten des Modells: die meistgenutzten auf der Grafikkarte, alle im RAM, eine Nachschlagetabelle auf der SSD"></p>
- **Das Modell ist ein Team aus 24,576 kleinen Spezialisten („Experten“).** Jedes Wort braucht nur 10 davon.
- **Deine Grafikkarte** hält die paar tausend Experten, die am häufigsten gebraucht werden. **Dein RAM** hält alle,
und **dein Prozessor** rechnet gleichzeitig am Rest. **Deine SSD** hält eine große Nachschlagetabelle.
<p align="center"><img src="docs/media/guess-and-check.svg" width="860" alt="Ein kleiner Helfer rät die nächsten Wörter; das große Modell prüft alle auf einmal und behält die richtigen"></p>
- **Raten, dann prüfen:** Ein kleiner Helfer rät die nächsten paar Wörter. Das große Modell prüft sie alle auf
einmal. Du bekommst dieselbe Antwort, nur 1.6-1.8x schneller.
- **Lange Texte werden in großen Stücken gelesen** (bis zu 8,192 Tokens auf einmal), mit über 1,000 Tokens pro
Sekunde.
Die längere Erklärung: [docs/HOW_IT_WORKS.md](docs/HOW_IT_WORKS.md). Jeder Teil und seine Zahlen:
[die Details](docs/DETAILS.md#how-it-works) und das [Paper](docs/paper/Strata-Paper.pdf).
## Danksagung und Lizenz
Das Modell ist [Qwen3.8-Flash-Next](https://huggingface.co/Qwen/Qwen3.8-Flash-Next) vom Qwen-Team. Komprimiert
wurde es von [ISTA-DASLab](https://huggingface.co/ISTA-DASLab/Qwen3.8-Flash-Next-GSQ-RCO-GGUF), UkisAI (Swift 1.5)
und Unsloth. Strata nutzt Teile von [llama.cpp / ggml](https://github.com/ggml-org/llama.cpp). Alle Danksagungen:
[docs/HOW_IT_WORKS.md](docs/HOW_IT_WORKS.md#credits). Strata ist Open Source unter der [MIT-Lizenz](LICENSE). Ein
paar Teile und alle Modelle haben eigene Lizenzen ([welche](docs/HOW_IT_WORKS.md#license)).
## Strata unterstützen
Strata ist kostenlos und Open Source. Wenn es dir nützt, kannst du die Entwicklung unterstützen:
<p align="center"><a href="https://buymeacoffee.com/strataengine"><img src="https://cdn.buymeacoffee.com/buttons/v2/default-yellow.png" alt="Buy Me A Coffee" height="50"></a></p>
+216
View File
@@ -0,0 +1,216 @@
<h1 align="center">Strata</h1>
[English](README.md) · [简体中文](README.zh-CN.md) · [日本語](README.ja.md) · [Deutsch](README.de.md) · [Français](README.fr.md) · **Español** · [Português](README.pt-BR.md)
<p align="center"><b>Ejecuta un modelo de IA de 125 mil millones de parámetros en tu propio PC gaming</b><br>
Tarjeta gráfica NVIDIA o AMD (12 GB o más) · Windows o Linux · libre y de código abierto</p>
<p align="center"><a href="https://github.com/Niko1221/Strata/releases/download/v0.1.10/Pagoda.mp4"><img src="docs/media/pagoda-preview.webp" width="720" alt="Un jardín con una pagoda de vóxeles que escribió el modelo de Strata, funcionando en el navegador"></a><br>
<sub>Un jardín con una pagoda de vóxeles, hecho con un solo prompt en una RTX 5070 con Strata (IQ3_S, contexto de 128K) ·
<a href="https://github.com/Niko1221/Strata/releases/download/v0.1.10/Pagoda.mp4">vídeo completo (49 s)</a></sub></p>
Strata ejecuta **[Qwen3.8-Flash-Next](https://huggingface.co/Qwen/Qwen3.8-Flash-Next)** en un PC normal. Es un
modelo de IA grande e inteligente que normalmente necesita un servidor. Conversa, escribe código, entiende imágenes
y funciona con tus aplicaciones y agentes de programación. Nada sale de tu PC.
## ¿Qué velocidad tiene?
Lo medimos en dos PC gaming normales. Un token equivale más o menos a ¾ de una palabra.
- **Escribe respuestas:** lo rápido que aparece la respuesta en un chat corto. 60 tokens por segundo es más rápido de lo que puedes leer.
- **Lee tu prompt:** lo rápido que procesa lo que le envías (aquí, un documento, código o historial de chat de 32K tokens).
<table>
<tr><th>NVIDIA: RTX 5070 (12 GB), Ryzen 5 7600, 64 GB RAM</th><th>AMD: RX 9070 XT (16 GB), Ryzen 9 3900X, 47 GB RAM</th></tr>
<tr><td>
| Tamaño | Escribe respuestas | Lee tu prompt |
| --- | ---: | ---: |
| **Q2_0** | 94 tokens/s | 2,650 tokens/s |
| **IQ2_XS** | 79 tokens/s | 2,090 tokens/s |
| **IQ3_XXS** | 62 tokens/s | 1,750 tokens/s |
| **IQ3_S** | 53 tokens/s | 1,620 tokens/s |
| **Coder** | 55 tokens/s | 2,180 tokens/s |
</td><td>
| Tamaño | Escribe respuestas | Lee tu prompt |
| --- | ---: | ---: |
| **Q2_0** | 60 tokens/s | 1,160 tokens/s |
| **IQ2_XS** | 52 tokens/s | 1,110 tokens/s |
| **Coder** | 44 tokens/s | 1,420 tokens/s |
</td></tr>
</table>
NVIDIA: Q2_0 con el motor 0.1.36, las demás filas con 0.1.26 (respuestas de 4K, prompts de 32K). Las tablas
completas están en [DETAILS.md](docs/DETAILS.md#speed-measured). Una tarjeta con más VRAM es más rápida: una
RTX 3090 (24 GB) debería escribir unos 100-140 tokens por segundo. Chats largos y otras tarjetas:
[velocidad de cada modelo](docs/MODELS.md#how-fast-is-each-size), [resultados de la comunidad](docs/COMMUNITY_BENCHMARKS.md).
<p align="center"><a href="https://buymeacoffee.com/strataengine"><img src="https://cdn.buymeacoffee.com/buttons/v2/default-yellow.png" alt="Buy Me A Coffee" height="50"></a><br>
<sub>Strata es gratis. Si funciona bien en tu PC, un café ayuda a seguir trabajando en él.</sub></p>
## Qué necesitas
| | |
| --- | --- |
| **Tarjeta gráfica** | **NVIDIA** GeForce serie RTX 20, 30, 40 o 50, o **AMD** Radeon RX 7900 XT / XTX, RX 7800 XT / 7700 XT, RX 9060 XT, RX 9070 / 9070 XT, Radeon AI PRO R9700 o serie RX 6800 / 6900. Necesita **12 GB de VRAM o más**. |
| **RAM** | 32 GB o más. Tu RAM decide [qué modelo](#qué-modelo-elijo) cabe. Con 64 GB funcionan todos los tamaños. |
| **Disco** | Unos 80 GB libres. Usa un SSD si puedes: el primer arranque es mucho más rápido. |
| **Sistema** | Windows 10 / 11 o Linux, y un controlador gráfico actual de NVIDIA o AMD. |
El instalador prepara todo lo demás. Dos o tres tarjetas pueden repartirse el modelo ([multi-GPU](docs/MULTI_GPU.md)).
Experimental, escrito y probado por miembros de la comunidad en sus propios equipos:
- **Tarjetas gráficas antiguas** (Tesla P40 / V100, GTX 10, Radeon VII / MI50, RX 6700 XT, RX 5500 XT): [Older GPUs](docs/OLDER_GPUS.md).
- **Intel Arc**, compilado desde el código fuente en Linux: [Intel Arc](docs/INTEL_ARC.md).
- **AMD Ryzen AI Max (Strix Halo)**, compilado desde el código fuente en Linux: [Strix Halo](docs/STRIX_HALO.md).
- **Procesadores antiguos sin AVX2**: funcionan, pero despacio. [Older CPUs](docs/INSTALL.md#older-cpus-experimental).
La lista completa: [docs/INSTALL.md](docs/INSTALL.md#what-you-need).
## Instalación
### Deja que tu IA lo instale
¿Usas un asistente de programación con IA (Claude Code, Cursor, Codex, GitHub Copilot, ...)? Pega esto en él:
```text
Set up Strata on this PC for me: https://github.com/Niko1221/Strata - follow docs/AI_SETUP.md in that repository.
```
Revisa tu tarjeta gráfica, tu RAM y tu disco, y elige el modelo que cabe. Luego lo instala, lo arranca y te explica
cómo conectar tus aplicaciones. Las herramientas de IA también pueden instalar, arrancar y detener Strata con su
[servidor MCP](docs/MCP_SERVER.md).
### O hazlo tú mismo
[Descarga Strata](https://github.com/Niko1221/Strata/archive/refs/heads/main.zip) y descomprímelo (o usa `git clone`).
**Windows:** haz doble clic en **`START-HERE.bat`**. **Linux:** ejecuta **`./setup.sh`** en la carpeta de Strata.
Los pasos son los mismos para NVIDIA y AMD. El instalador detecta tu tarjeta y prepara el motor adecuado para ella.
Te hace unas pocas preguntas:
- qué modelo y qué tamaño,
- cuánto contexto (cuánto texto tiene presente el modelo),
- si debe entender imágenes.
Pulsa Enter cada vez para usar la respuesta recomendada. Después descarga el modelo (unos 70 GB) y lo arranca. Si la
descarga se detiene, vuelve a ejecutarlo: sigue donde lo dejó. Tu navegador abre la aplicación de Strata en
`http://127.0.0.1:8080`.
> **Mientras el modelo arranca, tu PC puede ir lento o dejar de responder durante 1-3 minutos** (más la primera vez).
> Strata carga 35-55 GB en tu RAM y reserva una parte para la tarjeta gráfica. Es normal. Espera y no cierres la
> ventana. La ventana muestra lo que está haciendo Strata.
**La próxima vez**, vuelve a ejecutar `START-HERE.bat` (o `./setup.sh`). Arranca enseguida y no descarga nada dos
veces. Cierra su ventana para detener el modelo. `UPDATE.bat` (`./update.sh`) actualiza Strata sin arrancarlo.
Actualizaciones, Docker, varias tarjetas, dónde se guardan los archivos y todas las opciones: [docs/INSTALL.md](docs/INSTALL.md).
## ¿Qué modelo elijo?
El instalador te recomienda uno según tu RAM. El mismo modelo viene en varios tamaños, más o menos comprimidos. Los
tamaños pequeños son más rápidos. Los grandes son algo más inteligentes.
| Tu RAM | Elige | Por qué |
| --- | --- | --- |
| **32 GB** | **Coder** | cabe en 32 GB y está hecho para programar (con una tarjeta de 24 GB también funcionan Q2_0 e IQ2_XS) |
| **48 GB** | **IQ2_XS** (o Q2_0, el más rápido) | los tamaños más grandes no caben |
| **64 GB** | **IQ2_XS** (recomendado), o IQ3_XXS / IQ3_S | caben todos los tamaños; IQ3_S es el mejor y el más lento |
| **96 GB o más** | **IQ3_S**, o el UD-IQ4_XS de Unsloth (~4 bits) | espacio para los tamaños más grandes con todo lo demás abierto |
- **[Coder](docs/MODELS.md#coder):** una versión para programar a la que se le quitó la mitad de los expertos. Alcanza
el 91% de la puntuación SWE-bench Verified del modelo completo (medido por sus autores) y cabe en 32 GB de RAM. Es
más flojo fuera del código, también con chino y otros textos CJK (#438). Para eso, elige Q2_0, IQ2_XS o IQ3_S, que
conservan todos los expertos.
- **[Swift 1.5](docs/MODELS.md#swift-15):** un ajuste fino que piensa mucho menos tiempo antes de responder. Recibes
la respuesta antes, con más o menos la misma calidad.
- **[Unsloth UD-IQ4_XS](docs/MODELS.md#unsloth-ud-iq4_xs):** la versión de ~4 bits de Unsloth, entre IQ3_S y
UD-Q4_K_XL en calidad. Una descarga de 94 GB. Con menos de ~80 GB de RAM, Strata lee una parte desde el SSD
mientras responde, así que ahí es más lento (un SSD NVMe ayuda).
- **[Unsloth UD-Q4_K_XL](docs/MODELS.md#unsloth-ud-q4_k_xl-experimental)** (experimental): el más parecido al modelo
completo. Pero Strata lee la mayor parte desde el SSD mientras responde, así que solo escribe 7-8.5 tokens/s en un
PC con 64 GB.
- **[OrcaRouter's Uncensored IQ3_XXS](docs/MODELS.md#orcarouter-uncensored-iq3_xxs):** se instala a mano. No está en
el menú del instalador.
Tamaños, descargas y qué cabe dónde: [docs/MODELS.md](docs/MODELS.md). Para añadir otro modelo más adelante, ejecuta
`SETUP.bat` (Linux: `./setup.sh --setup`).
## Cómo usarlo
<p align="center"><img src="docs/media/runpagoda.png" width="900" alt="La pestaña Monitor de la aplicación de Strata junto a un agente de programación"><br>
<sub>El <b>Monitor</b> de la aplicación de Strata (izquierda) mientras un agente de programación escribe el jardín de la pagoda del vídeo (derecha)</sub></p>
- **En el navegador:** abre `http://127.0.0.1:8080`. Tiene **Chat**, un **Monitor** en vivo del modelo y de tu
GPU/CPU/RAM, y **About** con los ajustes y las direcciones.
- **Tus aplicaciones y agentes de programación:** añade un proveedor "compatible con OpenAI" con la URL base
**`http://127.0.0.1:8080/v1`**. Sirve cualquier clave de API y cualquier nombre de modelo.
- Aplicaciones que usan la API de Anthropic: `http://127.0.0.1:8080/v1/messages` (Claude Code:
`ANTHROPIC_BASE_URL=http://127.0.0.1:8080`).
- Codex CLI y otras aplicaciones que usan la API Responses de OpenAI: `/v1/responses`
([configuración](docs/DETAILS.md#the-responses-api-and-codex-cli)).
- **Razonamiento:** elige **off, low, medium o high** en el menú del chat o en el "reasoning effort" de tu aplicación.
Off es lo más rápido. High es lo mejor para preguntas difíciles.
- **Imágenes:** responde que sí a "Images?" en la instalación. Luego haz clic en **Picture** en el chat, o adjunta
imágenes en tu aplicación. Las tarjetas AMD leen imágenes en Linux a través del procesador; en Windows todavía no.
- **Desde tu móvil u otro PC:** `START-HERE.bat --setup --host 0.0.0.0 --api-key <secret>`. Pon siempre una clave.
- **Una petición cada vez:** por defecto Strata responde una petición y las demás esperan. Para responder varias a la
vez, pon `"parallel": 2` ([BATCHING.md](docs/BATCHING.md)). En una tarjeta de 12 GB, cada respuesta va más lenta.
- **Prompts largos:** Strata lee entero el primer mensaje de un chat, más o menos 1 minuto por cada 30,000 tokens. Los
mensajes siguientes empiezan en segundos.
Más: [dónde se guardan tus chats](docs/INSTALL.md#where-things-are-stored), [la API](docs/DETAILS.md#using-it).
## ¿Algo salió mal?
- **Mi PC se congeló la primera vez que arrancó Strata.** Es normal mientras carga el modelo. Espera y no cierres la
ventana. ¿Sigue congelado después de 10 minutos? Reinicia el PC, cierra otros programas y vuelve a intentarlo, o
elige un tamaño más pequeño.
- **Se detuvo durante la descarga o la instalación.** Vuelve a ejecutar `START-HERE.bat` (o `./setup.sh`). Sigue
donde se detuvo.
- **Va muy lento y la luz del disco no deja de parpadear, o dice "the engine stopped unexpectedly".** Tu PC no tiene
suficiente RAM libre. Cierra otros programas (los navegadores usan mucha), o elige un tamaño más pequeño (Q2_0 o
IQ2_XS).
- **Dice que el puerto 8080 ya está en uso.** Strata ya está en marcha. Busca su ventana.
Más problemas y sus soluciones: [docs/TROUBLESHOOTING.md](docs/TROUBLESHOOTING.md). ¿Sigues atascado? Abre un
[issue](https://github.com/Niko1221/Strata/issues) y adjunta `strata-<model>.log` de la carpeta de Strata. ¿Has
encontrado un problema de seguridad? Infórmalo en privado: [SECURITY.md](SECURITY.md).
## ¿Cómo funciona?
Los modelos como este suelen funcionar en servidores con cientos de gigabytes de memoria gráfica. Tu tarjeta gráfica
tiene 12-24 GB. Strata hace que el modelo quepa **repartiendo el trabajo por todo tu PC**. Piensa en una cocina: lo
que usas todo el rato se queda en la encimera, y el resto espera en la despensa.
<p align="center"><img src="docs/media/how-it-works.svg" width="860" alt="Los 24,576 expertos del modelo: los más usados en la tarjeta gráfica, todos en la RAM y una tabla de consulta en el SSD"></p>
- **El modelo es un equipo de 24,576 pequeños especialistas ("expertos").** Cada palabra solo necesita 10 de ellos.
- **Tu tarjeta gráfica** guarda los pocos miles de expertos que más se usan. **Tu RAM** los tiene todos, y **tu
procesador** trabaja con el resto al mismo tiempo. **Tu SSD** guarda una gran tabla de consulta.
<p align="center"><img src="docs/media/guess-and-check.svg" width="860" alt="Un pequeño ayudante adivina las siguientes palabras; el modelo grande las comprueba todas a la vez y se queda con las correctas"></p>
- **Adivinar y comprobar:** un pequeño ayudante adivina las siguientes palabras. El modelo grande las comprueba todas
a la vez. Recibes la misma respuesta, 1.6-1.8x antes.
- **Los textos largos se leen en trozos grandes** (hasta 8,192 tokens cada vez), a más de 1,000 tokens por segundo.
La explicación larga: [docs/HOW_IT_WORKS.md](docs/HOW_IT_WORKS.md). Cada parte y sus números:
[los detalles](docs/DETAILS.md#how-it-works) y el [artículo](docs/paper/Strata-Paper.pdf).
## Créditos y licencia
El modelo es [Qwen3.8-Flash-Next](https://huggingface.co/Qwen/Qwen3.8-Flash-Next), del equipo de Qwen. Lo
comprimieron [ISTA-DASLab](https://huggingface.co/ISTA-DASLab/Qwen3.8-Flash-Next-GSQ-RCO-GGUF), UkisAI (Swift 1.5)
y Unsloth. Strata usa partes de [llama.cpp / ggml](https://github.com/ggml-org/llama.cpp). Todos los créditos:
[docs/HOW_IT_WORKS.md](docs/HOW_IT_WORKS.md#credits). Strata es de código abierto con la [licencia MIT](LICENSE).
Algunas partes y cada modelo tienen sus propias licencias ([cuáles](docs/HOW_IT_WORKS.md#license)).
## Apoya a Strata
Strata es libre y de código abierto. Si te resulta útil, puedes apoyar su desarrollo:
<p align="center"><a href="https://buymeacoffee.com/strataengine"><img src="https://cdn.buymeacoffee.com/buttons/v2/default-yellow.png" alt="Buy Me A Coffee" height="50"></a></p>
+221
View File
@@ -0,0 +1,221 @@
<h1 align="center">Strata</h1>
[English](README.md) · [简体中文](README.zh-CN.md) · [日本語](README.ja.md) · [Deutsch](README.de.md) · **Français** · [Español](README.es.md) · [Português](README.pt-BR.md)
<p align="center"><b>Faites tourner un modèle d'IA de 125 milliards de paramètres sur votre propre PC de jeu</b><br>
Carte graphique NVIDIA ou AMD (12 Go ou plus) · Windows ou Linux · gratuit et open source</p>
<p align="center"><a href="https://github.com/Niko1221/Strata/releases/download/v0.1.10/Pagoda.mp4"><img src="docs/media/pagoda-preview.webp" width="720" alt="Un jardin de pagode en voxels écrit par le modèle de Strata, qui tourne dans le navigateur"></a><br>
<sub>Un jardin de pagode en voxels, généré en une seule requête sur une RTX 5070 avec Strata (IQ3_S, contexte de 128K) ·
<a href="https://github.com/Niko1221/Strata/releases/download/v0.1.10/Pagoda.mp4">vidéo complète (49 s)</a></sub></p>
Strata fait tourner **[Qwen3.8-Flash-Next](https://huggingface.co/Qwen/Qwen3.8-Flash-Next)** sur un PC normal. C'est
un grand modèle d'IA, très capable, qui demande d'habitude un serveur. Il discute, écrit du code, lit des images et
fonctionne avec vos applications et vos agents de code. Rien ne quitte votre PC.
## Quelle est sa vitesse ?
Nous l'avons mesurée sur deux PC de jeu ordinaires. Un token correspond à environ ¾ d'un mot.
- **Écrit les réponses :** la vitesse à laquelle la réponse s'affiche dans une courte discussion. 60 tokens par seconde, c'est plus rapide que votre lecture.
- **Lit votre prompt :** la vitesse à laquelle il absorbe ce que vous envoyez (ici un document, du code ou un historique de discussion de 32K tokens).
<table>
<tr><th>NVIDIA : RTX 5070 (12 Go), Ryzen 5 7600, 64 Go de RAM</th><th>AMD : RX 9070 XT (16 Go), Ryzen 9 3900X, 47 Go de RAM</th></tr>
<tr><td>
| Taille | Écrit les réponses | Lit votre prompt |
| --- | ---: | ---: |
| **Q2_0** | 94 tokens/s | 2,650 tokens/s |
| **IQ2_XS** | 79 tokens/s | 2,090 tokens/s |
| **IQ3_XXS** | 62 tokens/s | 1,750 tokens/s |
| **IQ3_S** | 53 tokens/s | 1,620 tokens/s |
| **Coder** | 55 tokens/s | 2,180 tokens/s |
</td><td>
| Taille | Écrit les réponses | Lit votre prompt |
| --- | ---: | ---: |
| **Q2_0** | 60 tokens/s | 1,160 tokens/s |
| **IQ2_XS** | 52 tokens/s | 1,110 tokens/s |
| **Coder** | 44 tokens/s | 1,420 tokens/s |
</td></tr>
</table>
NVIDIA : Q2_0 avec le moteur 0.1.36, les autres lignes avec 0.1.26 (réponses de 4K, prompts de 32K). Les tableaux
complets sont dans [DETAILS.md](docs/DETAILS.md#speed-measured). Une carte avec plus de VRAM va plus vite : une
RTX 3090 (24 Go) devrait écrire environ 100-140 tokens par seconde. Longues discussions et autres cartes :
[vitesse de chaque modèle](docs/MODELS.md#how-fast-is-each-size), [résultats de la communauté](docs/COMMUNITY_BENCHMARKS.md).
<p align="center"><a href="https://buymeacoffee.com/strataengine"><img src="https://cdn.buymeacoffee.com/buttons/v2/default-yellow.png" alt="Buy Me A Coffee" height="50"></a><br>
<sub>Strata est gratuit. S'il tourne bien sur votre PC, un café aide à poursuivre le travail.</sub></p>
## Ce qu'il vous faut
| | |
| --- | --- |
| **Carte graphique** | **NVIDIA** GeForce RTX série 20, 30, 40 ou 50, ou **AMD** Radeon RX 7900 XT / XTX, RX 7800 XT / 7700 XT, RX 9060 XT, RX 9070 / 9070 XT, Radeon AI PRO R9700 ou série RX 6800 / 6900. Il faut **12 Go de VRAM ou plus**. |
| **RAM** | 32 Go ou plus. Votre RAM décide [quel modèle](#quel-modèle-choisir-) tient. Avec 64 Go, toutes les tailles tournent. |
| **Disque** | Environ 80 Go libres. Prenez un SSD si possible : le premier démarrage est bien plus rapide. |
| **Système** | Windows 10 / 11 ou Linux, avec un pilote graphique NVIDIA ou AMD à jour. |
L'installateur s'occupe de tout le reste. Deux ou trois cartes peuvent se partager le modèle ([multi-GPU](docs/MULTI_GPU.md)).
Expérimental, écrit et testé par des membres de la communauté sur leurs propres machines :
- **Cartes graphiques plus anciennes** (Tesla P40 / V100, GTX 10, Radeon VII / MI50, RX 6700 XT, RX 5500 XT) : [Older GPUs](docs/OLDER_GPUS.md).
- **Intel Arc**, compilé depuis les sources sous Linux : [Intel Arc](docs/INTEL_ARC.md).
- **AMD Ryzen AI Max (Strix Halo)**, compilé depuis les sources sous Linux : [Strix Halo](docs/STRIX_HALO.md).
- **Processeurs plus anciens sans AVX2** : ça marche, mais lentement. [Older CPUs](docs/INSTALL.md#older-cpus-experimental).
La liste complète : [docs/INSTALL.md](docs/INSTALL.md#what-you-need).
## Installation
### Laissez votre IA l'installer
Vous utilisez un assistant de code IA (Claude Code, Cursor, Codex, GitHub Copilot, ...) ? Collez-lui ceci :
```text
Set up Strata on this PC for me: https://github.com/Niko1221/Strata - follow docs/AI_SETUP.md in that repository.
```
Il vérifie votre carte graphique, votre RAM et votre disque, et choisit le modèle qui convient. Ensuite il l'installe,
le démarre et vous explique comment connecter vos applications. Les outils d'IA peuvent aussi installer, démarrer et
arrêter Strata grâce à son [serveur MCP](docs/MCP_SERVER.md).
### Ou faites-le vous-même
[Téléchargez Strata](https://github.com/Niko1221/Strata/archive/refs/heads/main.zip) et décompressez-le (ou faites un `git clone`).
**Windows :** double-cliquez sur **`START-HERE.bat`**. **Linux :** lancez **`./setup.sh`** dans le dossier Strata.
Les étapes sont les mêmes pour NVIDIA et AMD. L'installateur détecte votre carte et installe le bon moteur pour elle.
Il vous pose quelques questions :
- quel modèle et quelle taille,
- combien de contexte (la quantité de texte que le modèle garde en tête),
- s'il doit lire les images.
Appuyez sur Entrée à chaque fois pour garder la réponse recommandée. Ensuite il télécharge le modèle (environ 70 Go)
et le démarre. Si le téléchargement s'arrête, relancez-le : il reprend là où il s'était arrêté. Votre navigateur
ouvre l'application Strata à l'adresse `http://127.0.0.1:8080`.
> **Pendant le démarrage du modèle, votre PC peut ralentir ou ne plus répondre pendant 1-3 minutes** (plus longtemps la première fois).
> Strata charge 35-55 Go dans votre RAM et en réserve une partie pour la carte graphique. C'est normal. Attendez, et
> ne fermez pas la fenêtre. La fenêtre montre ce que fait Strata.
**La fois suivante**, relancez `START-HERE.bat` (ou `./setup.sh`). Il démarre tout de suite et ne télécharge rien
deux fois. Fermez sa fenêtre pour arrêter le modèle. `UPDATE.bat` (`./update.sh`) met Strata à jour sans le démarrer.
Mises à jour, Docker, plusieurs cartes, emplacement des fichiers et toutes les options : [docs/INSTALL.md](docs/INSTALL.md).
## Quel modèle choisir ?
L'installateur en recommande un selon votre RAM. Le même modèle existe en plusieurs tailles, plus ou moins
compressées. Les petites tailles sont plus rapides. Les grandes sont un peu plus intelligentes.
| Votre RAM | Prenez | Pourquoi |
| --- | --- | --- |
| **32 Go** | **Coder** | il tient dans 32 Go et il est fait pour le code (avec une carte de 24 Go, Q2_0 et IQ2_XS tournent aussi) |
| **48 Go** | **IQ2_XS** (ou Q2_0, le plus rapide) | les plus grandes tailles ne tiennent pas |
| **64 Go** | **IQ2_XS** (recommandé), ou IQ3_XXS / IQ3_S | toutes les tailles tiennent ; IQ3_S est le meilleur et le plus lent |
| **96 Go ou plus** | **IQ3_S**, ou l'UD-IQ4_XS d'Unsloth (~4 bits) | de la place pour les plus grandes tailles, avec tout le reste ouvert |
- **[Coder](docs/MODELS.md#coder) :** une version pour le code, avec la moitié des experts en moins. Elle atteint 91 %
du score SWE-bench Verified du modèle complet (mesuré par ses auteurs) et tient dans 32 Go de RAM. Elle est moins
bonne en dehors du code, y compris pour le chinois et les autres textes CJK (#438). Pour ces cas, prenez Q2_0,
IQ2_XS ou IQ3_S, qui gardent tous les experts.
- **[Swift 1.5](docs/MODELS.md#swift-15) :** un fine-tune qui réfléchit bien moins longtemps avant de répondre. Vous
avez la réponse plus tôt, avec à peu près la même qualité.
- **[Unsloth UD-IQ4_XS](docs/MODELS.md#unsloth-ud-iq4_xs)** : la version ~4 bits d'Unsloth, entre IQ3_S et
UD-Q4_K_XL en qualité. Un téléchargement de 94 Go. Avec moins de ~80 Go de RAM, Strata en lit une partie depuis
le SSD pendant qu'il répond, il y est donc plus lent (un SSD NVMe aide).
- **[Unsloth UD-Q4_K_XL](docs/MODELS.md#unsloth-ud-q4_k_xl-experimental)** (expérimental) : le plus proche du modèle
complet. Mais Strata en lit la plus grande partie depuis le SSD pendant qu'il répond, donc il n'écrit que
7-8.5 tokens/s sur un PC avec 64 Go.
- **[OrcaRouter's Uncensored IQ3_XXS](docs/MODELS.md#orcarouter-uncensored-iq3_xxs) :** vous l'installez à la main.
Il n'est pas dans le menu de l'installateur.
Tailles, téléchargements et ce qui tient où : [docs/MODELS.md](docs/MODELS.md). Pour ajouter un autre modèle plus
tard, lancez `SETUP.bat` (Linux : `./setup.sh --setup`).
## Utilisation
<p align="center"><img src="docs/media/runpagoda.png" width="900" alt="L'onglet Monitor de l'application Strata à côté d'un agent de code"><br>
<sub>Le <b>Monitor</b> de l'application Strata (à gauche) pendant qu'un agent de code écrit le jardin de pagode de la vidéo (à droite)</sub></p>
- **Dans le navigateur :** ouvrez `http://127.0.0.1:8080`. Vous y trouvez **Chat**, un **Monitor** en direct du
modèle et de votre GPU/CPU/RAM, et **About** avec les réglages et les adresses.
- **Vos applications et agents de code :** ajoutez un fournisseur « OpenAI-compatible » avec l'URL de base
**`http://127.0.0.1:8080/v1`**. N'importe quelle clé API et n'importe quel nom de modèle fonctionnent.
- Applications qui utilisent l'API d'Anthropic : `http://127.0.0.1:8080/v1/messages` (Claude Code :
`ANTHROPIC_BASE_URL=http://127.0.0.1:8080`).
- Codex CLI et les autres applications qui utilisent l'API OpenAI Responses : `/v1/responses`
([configuration](docs/DETAILS.md#the-responses-api-and-codex-cli)).
- **Réflexion :** choisissez **off, low, medium ou high** dans le menu du chat ou dans le « reasoning effort » de
votre application. Off est le plus rapide. High est le meilleur pour les questions difficiles.
- **Images :** répondez oui à « Images? » pendant l'installation. Ensuite cliquez sur **Picture** dans le chat, ou
joignez des images dans votre application. Les cartes AMD lisent les images sous Linux via le processeur ; sous
Windows, pas encore.
- **Depuis votre téléphone ou un autre PC :** `START-HERE.bat --setup --host 0.0.0.0 --api-key <secret>`. Définissez
toujours une clé.
- **Une requête à la fois :** par défaut, Strata répond à une requête et les autres attendent. Pour répondre à
plusieurs en même temps, mettez `"parallel": 2` ([BATCHING.md](docs/BATCHING.md)). Sur une carte de 12 Go, chaque
réponse devient alors plus lente.
- **Longs prompts :** Strata lit en entier le premier message d'une discussion, environ 1 minute pour 30,000 tokens.
Les messages suivants démarrent en quelques secondes.
Plus d'infos : [où sont stockées vos discussions](docs/INSTALL.md#where-things-are-stored), [l'API](docs/DETAILS.md#using-it).
## Un problème ?
- **Mon PC s'est figé au premier démarrage de Strata.** C'est normal pendant le chargement du modèle. Attendez, et ne
fermez pas la fenêtre. Toujours figé après 10 minutes ? Redémarrez le PC, fermez les autres programmes et
réessayez, ou choisissez une taille plus petite.
- **Il s'est arrêté pendant le téléchargement ou l'installation.** Relancez `START-HERE.bat` (ou `./setup.sh`). Il
reprend là où il s'était arrêté.
- **C'est très lent et le voyant du disque clignote sans arrêt, ou il affiche « the engine stopped unexpectedly ».**
Votre PC n'a pas assez de RAM libre. Fermez les autres programmes (les navigateurs en utilisent beaucoup), ou
choisissez une taille plus petite (Q2_0 ou IQ2_XS).
- **Il dit que le port 8080 est déjà utilisé.** Strata tourne déjà. Cherchez sa fenêtre.
Autres problèmes et leurs solutions : [docs/TROUBLESHOOTING.md](docs/TROUBLESHOOTING.md). Toujours bloqué ? Ouvrez une
[issue](https://github.com/Niko1221/Strata/issues) et joignez `strata-<model>.log` depuis le dossier Strata. Vous
avez trouvé un problème de sécurité ? Signalez-le en privé : [SECURITY.md](SECURITY.md).
## Comment ça marche ?
Les modèles comme celui-ci tournent d'habitude sur des serveurs avec des centaines de gigaoctets de mémoire
graphique. Votre carte graphique a 12-24 Go. Strata fait tenir le modèle en **répartissant le travail sur tout votre
PC**. Pensez à une cuisine : ce que vous utilisez tout le temps reste sur le plan de travail, et le reste attend dans
le placard.
<p align="center"><img src="docs/media/how-it-works.svg" width="860" alt="Les 24,576 experts du modèle : les plus sollicités sur la carte graphique, tous en RAM, une table de correspondance sur le SSD"></p>
- **Le modèle est une équipe de 24,576 petits spécialistes (les « experts »).** Chaque mot n'en demande que 10.
- **Votre carte graphique** garde les quelques milliers d'experts les plus utilisés. **Votre RAM** les contient tous,
et **votre processeur** travaille sur les autres en même temps. **Votre SSD** contient une grande table de
correspondance.
<p align="center"><img src="docs/media/guess-and-check.svg" width="860" alt="Un petit assistant devine les mots suivants ; le grand modèle les vérifie tous d'un coup et garde les bons"></p>
- **Deviner, puis vérifier :** un petit assistant devine les quelques mots suivants. Le grand modèle les vérifie
tous d'un coup. Vous obtenez la même réponse, 1.6-1.8x plus tôt.
- **Les longs textes sont lus par gros morceaux** (jusqu'à 8,192 tokens à la fois), à plus de 1,000 tokens par seconde.
L'explication plus longue : [docs/HOW_IT_WORKS.md](docs/HOW_IT_WORKS.md). Chaque partie et ses chiffres :
[les détails](docs/DETAILS.md#how-it-works) et l'[article](docs/paper/Strata-Paper.pdf).
## Crédits et licence
Le modèle est [Qwen3.8-Flash-Next](https://huggingface.co/Qwen/Qwen3.8-Flash-Next), de l'équipe Qwen. Il a été
compressé par [ISTA-DASLab](https://huggingface.co/ISTA-DASLab/Qwen3.8-Flash-Next-GSQ-RCO-GGUF), UkisAI (Swift 1.5)
et Unsloth. Strata utilise des parties de [llama.cpp / ggml](https://github.com/ggml-org/llama.cpp). Tous les
crédits : [docs/HOW_IT_WORKS.md](docs/HOW_IT_WORKS.md#credits). Strata est open source sous [licence MIT](LICENSE).
Quelques parties et chaque modèle ont leur propre licence ([lesquelles](docs/HOW_IT_WORKS.md#license)).
## Soutenir Strata
Strata est gratuit et open source. S'il vous est utile, vous pouvez soutenir son développement :
<p align="center"><a href="https://buymeacoffee.com/strataengine"><img src="https://cdn.buymeacoffee.com/buttons/v2/default-yellow.png" alt="Buy Me A Coffee" height="50"></a></p>
+211
View File
@@ -0,0 +1,211 @@
<h1 align="center">Strata</h1>
[English](README.md) · [简体中文](README.zh-CN.md) · **日本語** · [Deutsch](README.de.md) · [Français](README.fr.md) · [Español](README.es.md) · [Português](README.pt-BR.md)
<p align="center"><b>1,250 億パラメータの AI モデルを、手元のゲーミング PC で動かす</b><br>
NVIDIA または AMD のグラフィックカード(12 GB 以上) · Windows または Linux · 無料のオープンソース</p>
<p align="center"><a href="https://github.com/Niko1221/Strata/releases/download/v0.1.10/Pagoda.mp4"><img src="docs/media/pagoda-preview.webp" width="720" alt="Strata のモデルが書いたボクセルの五重塔の庭。ブラウザで動いている様子"></a><br>
<sub>ボクセルの五重塔の庭。1 回のプロンプトで作成し、RTX 5070 上の Strata(IQ3_S、128K コンテキスト)で実行 ·
<a href="https://github.com/Niko1221/Strata/releases/download/v0.1.10/Pagoda.mp4">動画全体(49 秒)</a></sub></p>
Strata は **[Qwen3.8-Flash-Next](https://huggingface.co/Qwen/Qwen3.8-Flash-Next)** を普通の PC で動かします。
これは大きくて賢い AI モデルで、ふつうはサーバーが必要です。チャットをしたり、コードを書いたり、画像を読んだりできます。
アプリやコーディングエージェントとも連携します。データが PC の外に出ることはありません。
## どのくらい速い?
ごく普通のゲーミング PC 2 台で測りました。1 トークンは英単語のおよそ ¾ です。
- **回答の書き出し:** 短いチャットで返答が表示される速さです。毎秒 60 トークンあれば、読むより速く表示されます。
- **プロンプトの読み込み:** 送った内容を取り込む速さです(ここでは 32K トークンの文書、コード、またはチャット履歴)。
<table>
<tr><th>NVIDIA: RTX 5070 (12 GB), Ryzen 5 7600, 64 GB RAM</th><th>AMD: RX 9070 XT (16 GB), Ryzen 9 3900X, 47 GB RAM</th></tr>
<tr><td>
| サイズ | 回答の書き出し | プロンプトの読み込み |
| --- | ---: | ---: |
| **Q2_0** | 94 tokens/s | 2,650 tokens/s |
| **IQ2_XS** | 79 tokens/s | 2,090 tokens/s |
| **IQ3_XXS** | 62 tokens/s | 1,750 tokens/s |
| **IQ3_S** | 53 tokens/s | 1,620 tokens/s |
| **Coder** | 55 tokens/s | 2,180 tokens/s |
</td><td>
| サイズ | 回答の書き出し | プロンプトの読み込み |
| --- | ---: | ---: |
| **Q2_0** | 60 tokens/s | 1,160 tokens/s |
| **IQ2_XS** | 52 tokens/s | 1,110 tokens/s |
| **Coder** | 44 tokens/s | 1,420 tokens/s |
</td></tr>
</table>
NVIDIA:Q2_0 はエンジン 0.1.36、ほかの行は 0.1.26 で測定(回答 4K、プロンプト 32K)。表の全体は
[DETAILS.md](docs/DETAILS.md#speed-measured) にあります。VRAM が多いカードほど速くなります。RTX 3090(24 GB)なら
毎秒およそ 100-140 トークンで書き出せるはずです。長いチャットやほかのカードについて:[モデルごとの速度](docs/MODELS.md#how-fast-is-each-size)、
[コミュニティの測定結果](docs/COMMUNITY_BENCHMARKS.md)。
<p align="center"><a href="https://buymeacoffee.com/strataengine"><img src="https://cdn.buymeacoffee.com/buttons/v2/default-yellow.png" alt="Buy Me A Coffee" height="50"></a><br>
<sub>Strata は無料です。あなたの PC でうまく動いたら、コーヒー 1 杯の支援が開発を続ける力になります。</sub></p>
## 必要なもの
| | |
| --- | --- |
| **グラフィックカード** | **NVIDIA** GeForce RTX 20、30、40、50 シリーズ、または **AMD** Radeon RX 7900 XT / XTX、RX 7800 XT / 7700 XT、RX 9060 XT、RX 9070 / 9070 XT、Radeon AI PRO R9700、RX 6800 / 6900 シリーズ。**VRAM 12 GB 以上**が必要です。 |
| **RAM** | 32 GB 以上。RAM の量で[使えるモデル](#どのモデルを選べばいい)が決まります。64 GB あればすべてのサイズが動きます。 |
| **ディスク** | 空き容量が約 80 GB。できれば SSD を使ってください。初回の起動がずっと速くなります。 |
| **OS** | Windows 10 / 11 または Linux。NVIDIA または AMD の最新のグラフィックドライバー。 |
ほかに必要なものはすべてインストーラーが用意します。2 枚や 3 枚のカードでモデルを分担することもできます([マルチ GPU](docs/MULTI_GPU.md))。
試験的なサポート。コミュニティのメンバーが自分のマシンで書き、テストしたものです:
- **古いグラフィックカード**(Tesla P40 / V100、GTX 10、Radeon VII / MI50、RX 6700 XT、RX 5500 XT):[Older GPUs](docs/OLDER_GPUS.md)。
- **Intel Arc**(Linux でソースからビルド):[Intel Arc](docs/INTEL_ARC.md)。
- **AMD Ryzen AI Max(Strix Halo)**(Linux でソースからビルド):[Strix Halo](docs/STRIX_HALO.md)。
- **AVX2 のない古いプロセッサー**:動きますが、遅いです。[Older CPUs](docs/INSTALL.md#older-cpus-experimental)。
全リスト:[docs/INSTALL.md](docs/INSTALL.md#what-you-need)。
## インストール
### AI にセットアップしてもらう
AI コーディングアシスタント(Claude Code、Cursor、Codex、GitHub Copilot など)を使っていますか? これを貼り付けてください:
```text
Set up Strata on this PC for me: https://github.com/Niko1221/Strata - follow docs/AI_SETUP.md in that repository.
```
AI がグラフィックカード、RAM、ディスクを調べて、合うモデルを選びます。それからインストールして起動し、
アプリのつなぎ方を教えてくれます。AI ツールは Strata の [MCP サーバー](docs/MCP_SERVER.md) を通じて、
Strata のインストール、起動、停止もできます。
### 自分でやる
[Strata をダウンロード](https://github.com/Niko1221/Strata/archive/refs/heads/main.zip)して展開します(または `git clone` します)。
**Windows:** **`START-HERE.bat`** をダブルクリック。**Linux:** Strata フォルダーで **`./setup.sh`** を実行します。
手順は NVIDIA でも AMD でも同じです。インストーラーがカードを見つけて、それに合うエンジンを用意します。
いくつか質問があります:
- どのモデルを、どのサイズで使うか
- コンテキストをどのくらいにするか(モデルが覚えておけるテキストの量)
- 画像を読ませるかどうか
毎回 Enter を押せば、おすすめの答えになります。そのあとモデル(約 70 GB)をダウンロードして起動します。
ダウンロードが止まったら、もう一度実行してください。続きから再開します。ブラウザーで Strata アプリが
`http://127.0.0.1:8080` に開きます。
> **モデルの起動中、1-3 分ほど PC が重くなったり、反応しなくなったりすることがあります**(初回がいちばん長いです)。
> Strata は 35-55 GB を RAM に読み込み、その一部をグラフィックカード用に固定します。これは正常です。
> ウィンドウを閉じずに待ってください。Strata が何をしているかはウィンドウに表示されます。
**次回からは** `START-HERE.bat`(または `./setup.sh`)をもう一度実行します。すぐに起動し、同じものを 2 回ダウンロードすることはありません。
モデルを止めるときはウィンドウを閉じます。`UPDATE.bat`(`./update.sh`)を使うと、Strata を起動せずに更新できます。
更新、Docker、複数のカード、ファイルの保存場所、すべてのオプションについて:[docs/INSTALL.md](docs/INSTALL.md)。
## どのモデルを選べばいい
インストーラーが RAM に合わせてひとつ勧めます。同じモデルにも、圧縮の強さが違う複数のサイズがあります。
小さいサイズほど速く、大きいサイズほど少し賢くなります。
| RAM | 選ぶもの | 理由 |
| --- | --- | --- |
| **32 GB** | **Coder** | 32 GB に収まり、コード向けに作られている(24 GB のカードなら Q2_0 と IQ2_XS も動く) |
| **48 GB** | **IQ2_XS**(または最速の Q2_0) | 大きいサイズは収まらない |
| **64 GB** | **IQ2_XS**(おすすめ)、または IQ3_XXS / IQ3_S | すべてのサイズが収まる。IQ3_S がいちばん賢く、いちばん遅い |
| **96 GB 以上** | **IQ3_S**、または Unsloth の UD-IQ4_XS(約 4-bit) | ほかのものを全部開いたままでも、最大のサイズが入る余裕がある |
- **[Coder](docs/MODELS.md#coder):** エキスパートを半分取り除いたコーディング向け版です。フルモデルの SWE-bench Verified スコアの
91% に届き(作者による測定)、32 GB の RAM に収まります。コード以外では弱く、中国語などの CJK のテキストも苦手です(#438)。
そうした用途には、すべてのエキスパートを残している Q2_0、IQ2_XS、IQ3_S を選んでください。
- **[Swift 1.5](docs/MODELS.md#swift-15):** 答える前に考える時間がずっと短いファインチューン版です。
ほぼ同じ品質のまま、答えが早く返ってきます。
- **[Unsloth UD-IQ4_XS](docs/MODELS.md#unsloth-ud-iq4_xs):** Unsloth の約 4-bit 版で、品質は IQ3_S と UD-Q4_K_XL の間です。
ダウンロードは 94 GB。RAM が約 80 GB 未満だと、答えている間に Strata がその一部を SSD から読むので遅くなります(NVMe SSD が有効です)。
- **[Unsloth UD-Q4_K_XL](docs/MODELS.md#unsloth-ud-q4_k_xl-experimental)**(試験的):フルモデルにいちばん近いものです。
ただし答えている間、Strata はその大部分を SSD から読むので、64 GB の PC では毎秒 7-8.5 トークンしか書き出せません。
- **[OrcaRouter's Uncensored IQ3_XXS](docs/MODELS.md#orcarouter-uncensored-iq3_xxs):** 手動でセットアップします。
インストーラーのメニューにはありません。
サイズ、ダウンロード、どれがどこに収まるかについて:[docs/MODELS.md](docs/MODELS.md)。あとで別のモデルを追加するには
`SETUP.bat`(Linux:`./setup.sh --setup`)を実行します。
## 使い方
<p align="center"><img src="docs/media/runpagoda.png" width="900" alt="Strata アプリの Monitor タブと、その横で動くコーディングエージェント"><br>
<sub>Strata アプリの <b>Monitor</b>(左)。コーディングエージェントが動画の五重塔の庭を書いているところ(右)</sub></p>
- **ブラウザーで:** `http://127.0.0.1:8080` を開きます。**Chat**、モデルと GPU/CPU/RAM の様子をリアルタイムで見る **Monitor**、
設定とアドレスが載った **About** があります。
- **アプリやコーディングエージェントから:** 「OpenAI-compatible」プロバイダーを追加し、ベース URL に
**`http://127.0.0.1:8080/v1`** を指定します。API キーとモデル名は何でもかまいません。
- Anthropic の API を使うアプリ:`http://127.0.0.1:8080/v1/messages`(Claude Code:
`ANTHROPIC_BASE_URL=http://127.0.0.1:8080`)。
- Codex CLI など OpenAI Responses API を使うアプリ:`/v1/responses`
([設定方法](docs/DETAILS.md#the-responses-api-and-codex-cli))。
- **思考(Thinking):** チャットのメニュー、またはアプリの「reasoning effort」で **off、low、medium、high** を選びます。
off がいちばん速く、high は難しい質問に向いています。
- **画像:** セットアップで「Images?」に yes と答えます。そのあとチャットで **Picture** をクリックするか、アプリで画像を添付します。
AMD のカードは、Linux ではプロセッサーを使って画像を読みます。Windows ではまだ読めません。
- **スマートフォンや別の PC から:** `START-HERE.bat --setup --host 0.0.0.0 --api-key <secret>`。キーは必ず設定してください。
- **リクエストは 1 つずつ:** 初期設定では Strata は 1 つのリクエストに答え、ほかは待ちます。複数に同時に答えるには
`"parallel": 2` を設定します([BATCHING.md](docs/BATCHING.md))。12 GB のカードでは、それぞれの回答が遅くなります。
- **長いプロンプト:** Strata はチャットの最初のメッセージをすべて読みます。30,000 トークンあたり約 1 分かかります。
2 通目以降のメッセージは数秒で始まります。
詳しくは:[チャットの保存場所](docs/INSTALL.md#where-things-are-stored)、[API](docs/DETAILS.md#using-it)。
## うまくいかないとき
- **Strata の初回起動で PC が固まった。** モデルの読み込み中はよくあることです。ウィンドウを閉じずに待ってください。
10 分たっても固まったまま? PC を再起動し、ほかのプログラムを閉じてもう一度試すか、小さいサイズを選んでください。
- **ダウンロードやインストールの途中で止まった。** `START-HERE.bat`(または `./setup.sh`)をもう一度実行してください。
止まったところから続きます。
- **とても遅く、ディスクのランプが点滅し続ける。または「the engine stopped unexpectedly」と表示される。** PC の空き RAM が
足りません。ほかのプログラムを閉じるか(ブラウザーは多く使います)、小さいサイズ(Q2_0 または IQ2_XS)を選んでください。
- **ポート 8080 がすでに使われていると表示される。** Strata はもう動いています。そのウィンドウを探してください。
ほかの問題と解決方法:[docs/TROUBLESHOOTING.md](docs/TROUBLESHOOTING.md)。それでも解決しない場合は
[issue](https://github.com/Niko1221/Strata/issues) を作成し、Strata フォルダーにある `strata-<model>.log` を添付してください。
セキュリティの問題を見つけたら、非公開で報告してください:[SECURITY.md](SECURITY.md)。
## しくみ
このようなモデルは、ふつう数百ギガバイトのグラフィックメモリを持つサーバーで動きます。あなたのグラフィックカードは
12-24 GB です。Strata は **PC 全体で作業を分担する** ことで、モデルを収めています。台所を思い浮かべてください。
いつも使う道具は調理台に置き、残りは食料庫にしまっておきます。
<p align="center"><img src="docs/media/how-it-works.svg" width="860" alt="モデルの 24,576 個のエキスパート:よく使うものはグラフィックカードに、すべては RAM に、ルックアップテーブルは SSD に"></p>
- **モデルは 24,576 個の小さな専門家(「エキスパート」)のチームです。** 1 語ごとに必要なのはそのうち 10 個だけです。
- **グラフィックカード** は、よく使われる数千個のエキスパートを持ちます。**RAM** はすべてのエキスパートを持ち、
**プロセッサー** が残りを同時に処理します。**SSD** は大きなルックアップテーブルを持ちます。
<p align="center"><img src="docs/media/guess-and-check.svg" width="860" alt="小さなヘルパーが次の単語を予想し、大きなモデルがまとめて確認して正しいものを残す"></p>
- **予想してから確認:** 小さなヘルパーが次の数語を予想します。大きなモデルがそれをまとめて確認します。
答えは同じまま、1.6-1.8x 早く返ってきます。
- **長いテキストは大きなまとまりで読みます**(一度に最大 8,192 トークン)。速さは毎秒 1,000 トークン以上です。
もっと詳しい説明:[docs/HOW_IT_WORKS.md](docs/HOW_IT_WORKS.md)。各部分とその数値:
[詳細](docs/DETAILS.md#how-it-works)と[論文](docs/paper/Strata-Paper.pdf)。
## クレジットとライセンス
モデルは Qwen チームの [Qwen3.8-Flash-Next](https://huggingface.co/Qwen/Qwen3.8-Flash-Next) です。
圧縮は [ISTA-DASLab](https://huggingface.co/ISTA-DASLab/Qwen3.8-Flash-Next-GSQ-RCO-GGUF)、UkisAI(Swift 1.5)、
Unsloth によるものです。Strata は [llama.cpp / ggml](https://github.com/ggml-org/llama.cpp) の一部を使っています。すべてのクレジット:
[docs/HOW_IT_WORKS.md](docs/HOW_IT_WORKS.md#credits)。Strata は [MIT License](LICENSE) のオープンソースです。一部のパーツと
すべてのモデルには、それぞれ独自のライセンスがあります([どれか](docs/HOW_IT_WORKS.md#license))。
## Strata を支援する
Strata は無料のオープンソースです。役に立ったら、開発を支援していただけます:
<p align="center"><a href="https://buymeacoffee.com/strataengine"><img src="https://cdn.buymeacoffee.com/buttons/v2/default-yellow.png" alt="Buy Me A Coffee" height="50"></a></p>
+101 -74
View File
@@ -1,5 +1,7 @@
<h1 align="center">Strata</h1>
**English** · [简体中文](README.zh-CN.md) · [日本語](README.ja.md) · [Deutsch](README.de.md) · [Français](README.fr.md) · [Español](README.es.md) · [Português](README.pt-BR.md)
<p align="center"><b>Run a 125-billion-parameter AI model on your own gaming PC</b><br>
NVIDIA or AMD graphics card (12 GB or more) · Windows or Linux · free and open source</p>
@@ -7,15 +9,16 @@ NVIDIA or AMD graphics card (12 GB or more) · Windows or Linux · free and open
<sub>A voxel pagoda garden, 1 shot prompt running on an RTX 5070 with Strata (IQ3_S, 128K context) ·
<a href="https://github.com/Niko1221/Strata/releases/download/v0.1.10/Pagoda.mp4">full video (49 s)</a></sub></p>
Strata runs **[Qwen3.8-Flash-Next](https://huggingface.co/Qwen/Qwen3.8-Flash-Next)** - a large, smart AI model that
normally needs a server - on a normal PC. It chats, writes code, reads pictures and works with your apps and coding
agents, and nothing leaves your PC.
Strata runs **[Qwen3.8-Flash-Next](https://huggingface.co/Qwen/Qwen3.8-Flash-Next)** on a normal PC. This is a
large, smart AI model that usually needs a server. It chats, writes code, reads pictures and works with your apps
and coding agents. Nothing leaves your PC.
## How fast is it?
Measured on two ordinary gaming PCs. "Writes answers" is how fast the reply appears in a short chat; "reads your
prompt" is how fast it takes in what you send (a 32K-token document, code or chat history). A token is about ¾ of a
word, so 60 tokens per second is faster than you can read.
We measured it on two ordinary gaming PCs. A token is about ¾ of a word.
- **Writes answers:** how fast the reply appears in a short chat. 60 tokens per second is faster than you can read.
- **Reads your prompt:** how fast it takes in what you send (here a 32K-token document, code or chat history).
<table>
<tr><th>NVIDIA: RTX 5070 (12 GB), Ryzen 5 7600, 64 GB RAM</th><th>AMD: RX 9070 XT (16 GB), Ryzen 9 3900X, 47 GB RAM</th></tr>
@@ -40,8 +43,10 @@ word, so 60 tokens per second is faster than you can read.
</td></tr>
</table>
A card with more VRAM is faster: an RTX 3090 (24 GB) should write roughly 100-140 tokens per second. Long chats,
other cards: [speed of each model](docs/MODELS.md#how-fast-is-each-size), [community results](docs/COMMUNITY_BENCHMARKS.md).
NVIDIA: Q2_0 with engine 0.1.36, the other rows with 0.1.26 (4K answers, 32K prompts). The full tables are in
[DETAILS.md](docs/DETAILS.md#speed-measured). A card with more VRAM is faster: an RTX 3090 (24 GB) should write
about 100-140 tokens per second. Long chats and other cards: [speed of each model](docs/MODELS.md#how-fast-is-each-size),
[community results](docs/COMMUNITY_BENCHMARKS.md).
<p align="center"><a href="https://buymeacoffee.com/strataengine"><img src="https://cdn.buymeacoffee.com/buttons/v2/default-yellow.png" alt="Buy Me A Coffee" height="50"></a><br>
<sub>Strata is free. If it runs well on your PC, a coffee keeps the work on it going.</sub></p>
@@ -50,26 +55,34 @@ other cards: [speed of each model](docs/MODELS.md#how-fast-is-each-size), [commu
| | |
| --- | --- |
| **Graphics card** | **NVIDIA** GeForce RTX 20, 30, 40 or 50 series, or **AMD** Radeon RX 7900 XT / XTX, RX 7800 XT / 7700 XT, RX 9060 XT, RX 9070 / 9070 XT, Radeon AI PRO R9700 or RX 6800 / 6900 series - with **12 GB of VRAM or more** |
| **RAM** | 32 GB or more - how much decides [which model](#which-model-should-i-pick) fits; 64 GB runs every size |
| **Disk** | about 80 GB free, on an SSD if you can (the first start is much faster) |
| **System** | Windows 10 / 11 or Linux, and a current graphics driver from NVIDIA or AMD |
| **Graphics card** | **NVIDIA** GeForce RTX 20, 30, 40 or 50 series, or **AMD** Radeon RX 7900 XT / XTX, RX 7800 XT / 7700 XT, RX 9060 XT, RX 9070 / 9070 XT, Radeon AI PRO R9700 or RX 6800 / 6900 series. It needs **12 GB of VRAM or more**. |
| **RAM** | 32 GB or more. Your RAM decides [which model](#which-model-should-i-pick) fits. 64 GB runs every size. |
| **Disk** | About 80 GB free. Use an SSD if you can: the first start is much faster. |
| **System** | Windows 10 / 11 or Linux, and a current graphics driver from NVIDIA or AMD. |
The installer sets up everything else. Two or three cards can share the model ([multi-GPU](docs/MULTI_GPU.md)).
Experimental, written and tested by community members on their own machines:
- **Older graphics cards** (Tesla P40 / V100, GTX 10, Radeon VII / MI50, RX 6700 XT, RX 5500 XT): [Older GPUs](docs/OLDER_GPUS.md).
- **Intel Arc**, built from source on Linux: [Intel Arc](docs/INTEL_ARC.md).
- **AMD Ryzen AI Max (Strix Halo)**, built from source on Linux: [Strix Halo](docs/STRIX_HALO.md).
- **Older processors without AVX2**: they work, but slowly. [Older CPUs](docs/INSTALL.md#older-cpus-experimental).
Everything else is installed for you. Two or three cards can share the model ([multi-GPU](docs/MULTI_GPU.md)).
The full list: [docs/INSTALL.md](docs/INSTALL.md#what-you-need).
## Install
### Let your AI set it up
Use an AI coding assistant (Claude Code, Cursor, Codex, GitHub Copilot, ...)? Paste this into it:
Do you use an AI coding assistant (Claude Code, Cursor, Codex, GitHub Copilot, ...)? Paste this into it:
```text
Set up Strata on this PC for me: https://github.com/Niko1221/Strata - follow docs/AI_SETUP.md in that repository.
```
It checks your graphics card, RAM and disk, picks the model that fits, installs it, starts it and tells you how to
connect your apps. AI tools can also install, start and stop Strata themselves through its
It checks your graphics card, RAM and disk and picks the model that fits. Then it installs and starts it and tells
you how to connect your apps. AI tools can also install, start and stop Strata through its
[MCP server](docs/MCP_SERVER.md).
### Or do it yourself
@@ -77,43 +90,51 @@ connect your apps. AI tools can also install, start and stop Strata themselves t
[Download Strata](https://github.com/Niko1221/Strata/archive/refs/heads/main.zip) and unzip it (or `git clone` it).
**Windows:** double-click **`START-HERE.bat`**. **Linux:** run **`./setup.sh`** in the Strata folder.
The same steps for NVIDIA and AMD: the installer finds your card and sets up the right engine for it. It asks which
model, which size, how much context (how much text it keeps in mind) and whether it should read pictures - press
Enter each time for the recommended answer. Then it downloads the model (~70 GB; you can stop and it continues where
it left off) and starts it. Your browser opens the Strata app at `http://127.0.0.1:8080`.
The steps are the same for NVIDIA and AMD. The installer finds your card and sets up the right engine for it. It
asks you a few questions:
> **While the model starts, your PC can be slow or stop responding for 1-3 minutes** (longest the first time): Strata
> loads 35-55 GB into your RAM and locks part of it for the graphics card. That's normal - wait, and don't close the
> window. The window tells you what it is doing.
- which model and which size,
- how much context (how much text the model keeps in mind),
- whether it should read pictures.
**Next time**, run `START-HERE.bat` (or `./setup.sh`) again: it starts right away, nothing is downloaded twice. Close
Press Enter each time for the recommended answer. Then it downloads the model (about 70 GB) and starts it. If the
download stops, run it again: it continues where it left off. Your browser opens the Strata app at
`http://127.0.0.1:8080`.
> **While the model starts, your PC can be slow or stop responding for 1-3 minutes** (longest the first time).
> Strata loads 35-55 GB into your RAM and locks part of it for the graphics card. This is normal. Wait, and don't
> close the window. The window shows what Strata is doing.
**Next time**, run `START-HERE.bat` (or `./setup.sh`) again. It starts right away and downloads nothing twice. Close
its window to stop the model. `UPDATE.bat` (`./update.sh`) updates Strata without starting it. Updating, Docker,
several cards, where the files go and every option:
[docs/INSTALL.md](docs/INSTALL.md).
several cards, where the files go and every option: [docs/INSTALL.md](docs/INSTALL.md).
## Which model should I pick?
The installer recommends one for your RAM. The same model comes in sizes that are compressed more or less: smaller
is faster, larger is a bit smarter.
The installer recommends one for your RAM. The same model comes in several sizes, compressed more or less. Smaller
sizes are faster. Larger sizes are a bit smarter.
| Your RAM | Take | Why |
| --- | --- | --- |
| **32 GB** | **Coder** | it fits 32 GB, and it is made for code (with a 24 GB card, Q2_0 and IQ2_XS run too) |
| **48 GB** | **IQ2_XS** (or Q2_0, the fastest) | the larger sizes do not fit |
| **64 GB** | **IQ2_XS** (recommended), or IQ3_XXS / IQ3_S | every size fits; IQ3_S is the best, and the slowest |
| **96 GB or more** | **IQ3_S**, or Unsloth's 4-bit (experimental) | room for the largest sizes with everything else open |
| **64 GB** | **IQ2_XS** (recommended), or IQ3_XXS / IQ3_S | every size fits; IQ3_S is the best and the slowest |
| **96 GB or more** | **IQ3_S**, or Unsloth's UD-IQ4_XS (~4-bit) | room for the largest sizes with everything else open |
- **[Coder](docs/MODELS.md#coder)** - a coding version with half of the experts removed: 91% of the full model's
SWE-bench Verified score (by its authors), fits 32 GB of RAM. Weaker outside code, including Chinese and other
CJK text (#438): for those, take Q2_0, IQ2_XS or IQ3_S, which keep every expert.
- **[Swift 1.5](docs/MODELS.md#swift-15)** - a fine-tune that thinks much shorter before it answers, so you get the
answer sooner, at about the same quality.
- **[Unsloth UD-Q4_K_XL](docs/MODELS.md#unsloth-ud-q4_k_xl-experimental)** (experimental) - the closest to the full
model, but most of it is read from the SSD while it answers: 7-8.5 tokens/s on a 64 GB PC.
- **[OrcaRouter's Uncensored IQ3_XXS](docs/MODELS.md#orcarouter-uncensored-iq3_xxs)** - a manual setup, not in the
installer's menu.
- **[Coder](docs/MODELS.md#coder):** a coding version with half of the experts removed. It reaches 91% of the full
model's SWE-bench Verified score (measured by its authors) and fits 32 GB of RAM. It is weaker outside code,
including Chinese and other CJK text (#438). For those, take Q2_0, IQ2_XS or IQ3_S, which keep every expert.
- **[Swift 1.5](docs/MODELS.md#swift-15):** a fine-tune that thinks for a much shorter time before it answers. You
get the answer sooner, at about the same quality.
- **[Unsloth UD-IQ4_XS](docs/MODELS.md#unsloth-ud-iq4_xs):** Unsloth's ~4-bit version, between IQ3_S and
UD-Q4_K_XL in quality. A 94 GB download. With less than ~80 GB of RAM, Strata reads part of it from the SSD
while it answers, so it is slower there (an NVMe SSD helps).
- **[Unsloth UD-Q4_K_XL](docs/MODELS.md#unsloth-ud-q4_k_xl-experimental)** (experimental): the closest to the full
model. But Strata reads most of it from the SSD while it answers, so it writes only 7-8.5 tokens/s on a 64 GB PC.
- **[OrcaRouter's Uncensored IQ3_XXS](docs/MODELS.md#orcarouter-uncensored-iq3_xxs):** you set it up by hand. It is
not in the installer's menu.
Sizes, downloads and what fits where: [docs/MODELS.md](docs/MODELS.md). You can add another model later with
Sizes, downloads and what fits where: [docs/MODELS.md](docs/MODELS.md). To add another model later, run
`SETUP.bat` (Linux: `./setup.sh --setup`).
## Using it
@@ -121,62 +142,68 @@ Sizes, downloads and what fits where: [docs/MODELS.md](docs/MODELS.md). You can
<p align="center"><img src="docs/media/runpagoda.png" width="900" alt="The Strata app's Monitor tab next to a coding agent"><br>
<sub>The Strata app's <b>Monitor</b> (left) while a coding agent writes the pagoda garden from the video (right)</sub></p>
- **In the browser:** `http://127.0.0.1:8080` - **Chat**, a live **Monitor** of the model and your GPU/CPU/RAM, and
**About** with the settings and addresses.
- **Your apps and coding agents:** add an "OpenAI-compatible" provider with base URL **`http://127.0.0.1:8080/v1`**,
any API key and any model name. Apps that use Anthropic's API: `http://127.0.0.1:8080/v1/messages` (Claude Code:
`ANTHROPIC_BASE_URL=http://127.0.0.1:8080`).
- **Thinking:** choose **off, low, medium or high** in the chat menu or your app's "reasoning effort". Off is
fastest; high is best for hard questions.
- **Pictures:** say yes to "Images?" in setup, then click **Picture** in the chat, or attach them in your app
(AMD cards: on Linux through the processor, not on Windows yet).
- **From your phone or another PC:** `START-HERE.bat --setup --host 0.0.0.0 --api-key <secret>` - always with a key.
- **Good to know:** it answers one request at a time. The first message of a chat is read in full (about 1 minute
per 30,000 tokens); follow-ups start in seconds.
- **In the browser:** open `http://127.0.0.1:8080`. It has **Chat**, a live **Monitor** of the model and your
GPU/CPU/RAM, and **About** with the settings and addresses.
- **Your apps and coding agents:** add an "OpenAI-compatible" provider with the base URL
**`http://127.0.0.1:8080/v1`**. Any API key and any model name work.
- Apps that use Anthropic's API: `http://127.0.0.1:8080/v1/messages` (Claude Code:
`ANTHROPIC_BASE_URL=http://127.0.0.1:8080`).
- Codex CLI and other apps that use the OpenAI Responses API: `/v1/responses`
([setup](docs/DETAILS.md#the-responses-api-and-codex-cli)).
- **Thinking:** choose **off, low, medium or high** in the chat menu or in your app's "reasoning effort". Off is the
fastest. High is best for hard questions.
- **Pictures:** say yes to "Images?" in setup. Then click **Picture** in the chat, or attach pictures in your app.
AMD cards read pictures on Linux through the processor; on Windows they can't yet.
- **From your phone or another PC:** `START-HERE.bat --setup --host 0.0.0.0 --api-key <secret>`. Always set a key.
- **One request at a time:** by default Strata answers one request, and the others wait. To answer several at once,
set `"parallel": 2` ([BATCHING.md](docs/BATCHING.md)). On a 12 GB card this makes each answer slower.
- **Long prompts:** Strata reads the first message of a chat in full, about 1 minute per 30,000 tokens. Follow-up
messages start in seconds.
More: [where your chats are stored](docs/INSTALL.md#where-things-are-stored), [the API](docs/DETAILS.md#using-it).
## Something went wrong?
- **My PC froze the first time Strata started.** Normal while it loads the model: wait, don't close the window.
Still frozen after 10 minutes? Restart the PC, close other programs and try again, or pick a smaller size.
- **It stopped while downloading or installing.** Run `START-HERE.bat` (or `./setup.sh`) again: it continues where
- **My PC froze the first time Strata started.** This is normal while it loads the model. Wait, and don't close the
window. Still frozen after 10 minutes? Restart the PC, close other programs and try again, or pick a smaller size.
- **It stopped while downloading or installing.** Run `START-HERE.bat` (or `./setup.sh`) again. It continues where
it stopped.
- **It's very slow and the disk light keeps blinking, or "the engine stopped unexpectedly".** Not enough free RAM:
close other programs (browsers use a lot), or pick a smaller size (Q2_0 or IQ2_XS).
- **It says port 8080 is already in use.** Strata is already running - look for its window.
- **It's very slow and the disk light keeps blinking, or it says "the engine stopped unexpectedly".** Your PC does
not have enough free RAM. Close other programs (browsers use a lot), or pick a smaller size (Q2_0 or IQ2_XS).
- **It says port 8080 is already in use.** Strata is already running. Look for its window.
More problems and their fixes: [docs/TROUBLESHOOTING.md](docs/TROUBLESHOOTING.md). Still stuck? Open an
[issue](https://github.com/Niko1221/Strata/issues) and attach `strata-<model>.log` from the Strata folder.
[issue](https://github.com/Niko1221/Strata/issues) and attach `strata-<model>.log` from the Strata folder. Found a
security problem? Report it privately: [SECURITY.md](SECURITY.md).
## How does it work?
Models like this one normally run on servers with hundreds of gigabytes of graphics memory. Your graphics card has
12-24 GB. Strata makes it fit by **sharing the work across your whole PC** - like a kitchen, where the things you use
all the time stay on the counter and the rest waits in the pantry.
Models like this one usually run on servers with hundreds of gigabytes of graphics memory. Your graphics card has
12-24 GB. Strata makes the model fit by **sharing the work across your whole PC**. Think of a kitchen: the things
you use all the time stay on the counter, and the rest waits in the pantry.
<p align="center"><img src="docs/media/how-it-works.svg" width="860" alt="The model's 24,576 experts: the busiest on the graphics card, all of them in RAM, a lookup table on the SSD"></p>
- **The model is a team of 24,576 small specialists ("experts"),** and each word needs only 10 of them.
- **Your graphics card** keeps the few thousand experts that are asked most often; **your RAM** holds all of them,
- **The model is a team of 24,576 small specialists ("experts").** Each word needs only 10 of them.
- **Your graphics card** keeps the few thousand experts that are used most often. **Your RAM** holds all of them,
and **your processor** works on the rest at the same time. **Your SSD** holds a big lookup table.
<p align="center"><img src="docs/media/guess-and-check.svg" width="860" alt="A small helper guesses the next words; the big model checks them all at once and keeps the right ones"></p>
- **Guess, then check:** a small helper guesses the next few words and the big model checks them all at once, so
you get the same answer, 1.6-1.8x sooner.
- **Long texts are read in big pieces** (up to 8,192 tokens at a time): over 1,000 tokens per second.
- **Guess, then check:** a small helper guesses the next few words. The big model checks them all at once. You get
the same answer, 1.6-1.8x sooner.
- **Long texts are read in big pieces** (up to 8,192 tokens at a time), at over 1,000 tokens per second.
The longer explanation: [docs/HOW_IT_WORKS.md](docs/HOW_IT_WORKS.md). Every part and its numbers: [the
details](docs/DETAILS.md#how-it-works) and the [paper](docs/paper/Strata-Paper.pdf).
The longer explanation: [docs/HOW_IT_WORKS.md](docs/HOW_IT_WORKS.md). Every part and its numbers:
[the details](docs/DETAILS.md#how-it-works) and the [paper](docs/paper/Strata-Paper.pdf).
## Credits and license
The model is [Qwen3.8-Flash-Next](https://huggingface.co/Qwen/Qwen3.8-Flash-Next) by the Qwen team, compressed by
[ISTA-DASLab](https://huggingface.co/ISTA-DASLab/Qwen3.8-Flash-Next-GSQ-RCO-GGUF), UkisAI (Swift 1.5) and Unsloth;
Strata is built with parts of [llama.cpp / ggml](https://github.com/ggml-org/llama.cpp). All credits:
[docs/HOW_IT_WORKS.md](docs/HOW_IT_WORKS.md#credits). Strata is open source under the [MIT License](LICENSE); a few
parts and every model carry their own licenses ([which ones](docs/HOW_IT_WORKS.md#license)).
The model is [Qwen3.8-Flash-Next](https://huggingface.co/Qwen/Qwen3.8-Flash-Next) by the Qwen team. It was
compressed by [ISTA-DASLab](https://huggingface.co/ISTA-DASLab/Qwen3.8-Flash-Next-GSQ-RCO-GGUF), UkisAI (Swift 1.5)
and Unsloth. Strata uses parts of [llama.cpp / ggml](https://github.com/ggml-org/llama.cpp). All credits:
[docs/HOW_IT_WORKS.md](docs/HOW_IT_WORKS.md#credits). Strata is open source under the [MIT License](LICENSE). A few
parts and every model have their own licenses ([which ones](docs/HOW_IT_WORKS.md#license)).
## Support Strata
+217
View File
@@ -0,0 +1,217 @@
<h1 align="center">Strata</h1>
[English](README.md) · [简体中文](README.zh-CN.md) · [日本語](README.ja.md) · [Deutsch](README.de.md) · [Français](README.fr.md) · [Español](README.es.md) · **Português**
<p align="center"><b>Rode um modelo de IA de 125 bilhões de parâmetros no seu próprio PC gamer</b><br>
Placa de vídeo NVIDIA ou AMD (12 GB ou mais) · Windows ou Linux · gratuito e de código aberto</p>
<p align="center"><a href="https://github.com/Niko1221/Strata/releases/download/v0.1.10/Pagoda.mp4"><img src="docs/media/pagoda-preview.webp" width="720" alt="Um jardim com pagode em voxels que o modelo do Strata escreveu, rodando no navegador"></a><br>
<sub>Um jardim com pagode em voxels, feito com um único prompt numa RTX 5070 com o Strata (IQ3_S, contexto de 128K) ·
<a href="https://github.com/Niko1221/Strata/releases/download/v0.1.10/Pagoda.mp4">vídeo completo (49 s)</a></sub></p>
O Strata roda o **[Qwen3.8-Flash-Next](https://huggingface.co/Qwen/Qwen3.8-Flash-Next)** num PC comum. É um modelo
de IA grande e inteligente que normalmente precisa de um servidor. Ele conversa, escreve código, entende imagens e
funciona com seus apps e agentes de programação. Nada sai do seu PC.
## Qual é a velocidade?
Medimos em dois PCs gamer comuns. Um token equivale a cerca de ¾ de uma palavra.
- **Escreve respostas:** a velocidade com que a resposta aparece numa conversa curta. 60 tokens por segundo é mais rápido do que você consegue ler.
- **Lê seu prompt:** a velocidade com que ele absorve o que você envia (aqui, um documento, código ou histórico de conversa de 32K tokens).
<table>
<tr><th>NVIDIA: RTX 5070 (12 GB), Ryzen 5 7600, 64 GB de RAM</th><th>AMD: RX 9070 XT (16 GB), Ryzen 9 3900X, 47 GB de RAM</th></tr>
<tr><td>
| Tamanho | Escreve respostas | Lê seu prompt |
| --- | ---: | ---: |
| **Q2_0** | 94 tokens/s | 2,650 tokens/s |
| **IQ2_XS** | 79 tokens/s | 2,090 tokens/s |
| **IQ3_XXS** | 62 tokens/s | 1,750 tokens/s |
| **IQ3_S** | 53 tokens/s | 1,620 tokens/s |
| **Coder** | 55 tokens/s | 2,180 tokens/s |
</td><td>
| Tamanho | Escreve respostas | Lê seu prompt |
| --- | ---: | ---: |
| **Q2_0** | 60 tokens/s | 1,160 tokens/s |
| **IQ2_XS** | 52 tokens/s | 1,110 tokens/s |
| **Coder** | 44 tokens/s | 1,420 tokens/s |
</td></tr>
</table>
NVIDIA: Q2_0 com o engine 0.1.36, as outras linhas com 0.1.26 (respostas de 4K, prompts de 32K). As tabelas
completas estão em [DETAILS.md](docs/DETAILS.md#speed-measured). Uma placa com mais VRAM é mais rápida: uma
RTX 3090 (24 GB) deve escrever cerca de 100-140 tokens por segundo. Conversas longas e outras placas:
[velocidade de cada modelo](docs/MODELS.md#how-fast-is-each-size), [resultados da comunidade](docs/COMMUNITY_BENCHMARKS.md).
<p align="center"><a href="https://buymeacoffee.com/strataengine"><img src="https://cdn.buymeacoffee.com/buttons/v2/default-yellow.png" alt="Buy Me A Coffee" height="50"></a><br>
<sub>O Strata é gratuito. Se ele roda bem no seu PC, um café ajuda a manter o trabalho nele.</sub></p>
## Do que você precisa
| | |
| --- | --- |
| **Placa de vídeo** | **NVIDIA** GeForce RTX séries 20, 30, 40 ou 50, ou **AMD** Radeon RX 7900 XT / XTX, RX 7800 XT / 7700 XT, RX 9060 XT, RX 9070 / 9070 XT, Radeon AI PRO R9700 ou séries RX 6800 / 6900. Ela precisa de **12 GB de VRAM ou mais**. |
| **RAM** | 32 GB ou mais. Sua RAM decide [qual modelo](#qual-modelo-devo-escolher) cabe. Com 64 GB, roda qualquer tamanho. |
| **Disco** | Cerca de 80 GB livres. Use um SSD se puder: a primeira inicialização fica bem mais rápida. |
| **Sistema** | Windows 10 / 11 ou Linux, e um driver de vídeo atualizado da NVIDIA ou da AMD. |
O instalador cuida de todo o resto. Duas ou três placas podem dividir o modelo ([multi-GPU](docs/MULTI_GPU.md)).
Experimental, escrito e testado por membros da comunidade nas próprias máquinas:
- **Placas de vídeo mais antigas** (Tesla P40 / V100, GTX 10, Radeon VII / MI50, RX 6700 XT, RX 5500 XT): [Older GPUs](docs/OLDER_GPUS.md).
- **Intel Arc**, compilado a partir do código-fonte no Linux: [Intel Arc](docs/INTEL_ARC.md).
- **AMD Ryzen AI Max (Strix Halo)**, compilado a partir do código-fonte no Linux: [Strix Halo](docs/STRIX_HALO.md).
- **Processadores mais antigos sem AVX2**: funcionam, mas devagar. [Older CPUs](docs/INSTALL.md#older-cpus-experimental).
A lista completa: [docs/INSTALL.md](docs/INSTALL.md#what-you-need).
## Instalação
### Deixe sua IA instalar
Você usa um assistente de programação com IA (Claude Code, Cursor, Codex, GitHub Copilot, ...)? Cole isto nele:
```text
Set up Strata on this PC for me: https://github.com/Niko1221/Strata - follow docs/AI_SETUP.md in that repository.
```
Ele verifica sua placa de vídeo, RAM e disco e escolhe o modelo que cabe. Depois instala, inicia e explica como
conectar seus apps. Ferramentas de IA também podem instalar, iniciar e parar o Strata pelo
[servidor MCP](docs/MCP_SERVER.md) dele.
### Ou faça você mesmo
[Baixe o Strata](https://github.com/Niko1221/Strata/archive/refs/heads/main.zip) e descompacte (ou use `git clone`).
**Windows:** clique duas vezes em **`START-HERE.bat`**. **Linux:** rode **`./setup.sh`** na pasta do Strata.
Os passos são os mesmos para NVIDIA e AMD. O instalador encontra sua placa e configura o engine certo para ela.
Ele faz algumas perguntas:
- qual modelo e qual tamanho,
- quanto contexto (quanto texto o modelo guarda na memória),
- se ele deve entender imagens.
Aperte Enter em cada uma para usar a resposta recomendada. Depois ele baixa o modelo (cerca de 70 GB) e o inicia.
Se o download parar, rode de novo: ele continua de onde parou. Seu navegador abre o app do Strata em
`http://127.0.0.1:8080`.
> **Enquanto o modelo inicia, seu PC pode ficar lento ou parar de responder por 1-3 minutos** (mais na primeira vez).
> O Strata carrega 35-55 GB na sua RAM e reserva parte dela para a placa de vídeo. Isso é normal. Espere e não
> feche a janela. A janela mostra o que o Strata está fazendo.
**Da próxima vez**, rode `START-HERE.bat` (ou `./setup.sh`) de novo. Ele inicia na hora e não baixa nada duas
vezes. Feche a janela para parar o modelo. `UPDATE.bat` (`./update.sh`) atualiza o Strata sem iniciá-lo.
Atualização, Docker, várias placas, onde ficam os arquivos e todas as opções: [docs/INSTALL.md](docs/INSTALL.md).
## Qual modelo devo escolher?
O instalador recomenda um de acordo com sua RAM. O mesmo modelo vem em vários tamanhos, mais ou menos comprimidos.
Os tamanhos menores são mais rápidos. Os maiores são um pouco mais inteligentes.
| Sua RAM | Escolha | Por quê |
| --- | --- | --- |
| **32 GB** | **Coder** | cabe em 32 GB e é feito para código (com uma placa de 24 GB, Q2_0 e IQ2_XS também rodam) |
| **48 GB** | **IQ2_XS** (ou Q2_0, o mais rápido) | os tamanhos maiores não cabem |
| **64 GB** | **IQ2_XS** (recomendado), ou IQ3_XXS / IQ3_S | todos os tamanhos cabem; IQ3_S é o melhor e o mais lento |
| **96 GB ou mais** | **IQ3_S**, ou o UD-IQ4_XS da Unsloth (~4-bit) | espaço para os maiores tamanhos com todo o resto aberto |
- **[Coder](docs/MODELS.md#coder):** uma versão para programação com metade dos experts removida. Ela alcança 91%
da pontuação do modelo completo no SWE-bench Verified (medido pelos autores) e cabe em 32 GB de RAM. É mais fraca
fora do código, inclusive em chinês e outros textos CJK (#438). Para isso, escolha Q2_0, IQ2_XS ou IQ3_S, que
mantêm todos os experts.
- **[Swift 1.5](docs/MODELS.md#swift-15):** um fine-tune que pensa por bem menos tempo antes de responder. Você
recebe a resposta mais cedo, com quase a mesma qualidade.
- **[Unsloth UD-IQ4_XS](docs/MODELS.md#unsloth-ud-iq4_xs):** a versão de ~4 bits da Unsloth, entre IQ3_S e
UD-Q4_K_XL em qualidade. Um download de 94 GB. Com menos de ~80 GB de RAM, o Strata lê uma parte dele do SSD
enquanto responde, então ali ele fica mais lento (um SSD NVMe ajuda).
- **[Unsloth UD-Q4_K_XL](docs/MODELS.md#unsloth-ud-q4_k_xl-experimental)** (experimental): o mais próximo do modelo
completo. Mas o Strata lê a maior parte dele do SSD enquanto responde, então ele escreve só 7-8.5 tokens/s num PC
com 64 GB.
- **[OrcaRouter's Uncensored IQ3_XXS](docs/MODELS.md#orcarouter-uncensored-iq3_xxs):** você configura à mão. Ele
não está no menu do instalador.
Tamanhos, downloads e o que cabe onde: [docs/MODELS.md](docs/MODELS.md). Para adicionar outro modelo depois, rode
`SETUP.bat` (Linux: `./setup.sh --setup`).
## Como usar
<p align="center"><img src="docs/media/runpagoda.png" width="900" alt="A aba Monitor do app Strata ao lado de um agente de programação"><br>
<sub>O <b>Monitor</b> do app Strata (à esquerda) enquanto um agente de programação escreve o jardim com pagode do vídeo (à direita)</sub></p>
- **No navegador:** abra `http://127.0.0.1:8080`. Lá tem o **Chat**, um **Monitor** ao vivo do modelo e da sua
GPU/CPU/RAM, e o **About** com as configurações e os endereços.
- **Seus apps e agentes de programação:** adicione um provedor "OpenAI-compatible" com a URL base
**`http://127.0.0.1:8080/v1`**. Qualquer chave de API e qualquer nome de modelo funcionam.
- Apps que usam a API da Anthropic: `http://127.0.0.1:8080/v1/messages` (Claude Code:
`ANTHROPIC_BASE_URL=http://127.0.0.1:8080`).
- Codex CLI e outros apps que usam a OpenAI Responses API: `/v1/responses`
([configuração](docs/DETAILS.md#the-responses-api-and-codex-cli)).
- **Raciocínio:** escolha **off, low, medium ou high** no menu do chat ou no "reasoning effort" do seu app. Off é o
mais rápido. High é o melhor para perguntas difíceis.
- **Imagens:** responda sim para "Images?" na instalação. Depois clique em **Picture** no chat, ou anexe imagens no
seu app. Placas AMD entendem imagens no Linux usando o processador; no Windows, ainda não.
- **Do seu celular ou de outro PC:** `START-HERE.bat --setup --host 0.0.0.0 --api-key <secret>`. Sempre defina uma chave.
- **Uma requisição por vez:** por padrão, o Strata responde uma requisição e as outras esperam. Para responder
várias ao mesmo tempo, defina `"parallel": 2` ([BATCHING.md](docs/BATCHING.md)). Numa placa de 12 GB, isso deixa
cada resposta mais lenta.
- **Prompts longos:** o Strata lê a primeira mensagem de uma conversa inteira, cerca de 1 minuto a cada 30,000
tokens. As mensagens seguintes começam em segundos.
Mais: [onde suas conversas ficam salvas](docs/INSTALL.md#where-things-are-stored), [a API](docs/DETAILS.md#using-it).
## Algo deu errado?
- **Meu PC travou na primeira vez que o Strata iniciou.** Isso é normal enquanto ele carrega o modelo. Espere e não
feche a janela. Ainda travado depois de 10 minutos? Reinicie o PC, feche outros programas e tente de novo, ou
escolha um tamanho menor.
- **Parou durante o download ou a instalação.** Rode `START-HERE.bat` (ou `./setup.sh`) de novo. Ele continua de
onde parou.
- **Está muito lento e a luz do disco não para de piscar, ou aparece "the engine stopped unexpectedly".** Seu PC
não tem RAM livre suficiente. Feche outros programas (navegadores usam muita), ou escolha um tamanho menor (Q2_0
ou IQ2_XS).
- **Diz que a porta 8080 já está em uso.** O Strata já está rodando. Procure a janela dele.
Mais problemas e como resolver: [docs/TROUBLESHOOTING.md](docs/TROUBLESHOOTING.md). Ainda sem solução? Abra uma
[issue](https://github.com/Niko1221/Strata/issues) e anexe o `strata-<model>.log` da pasta do Strata. Encontrou um
problema de segurança? Informe em particular: [SECURITY.md](SECURITY.md).
## Como funciona?
Modelos como este normalmente rodam em servidores com centenas de gigabytes de memória de vídeo. Sua placa de vídeo
tem 12-24 GB. O Strata faz o modelo caber **dividindo o trabalho pelo PC inteiro**. Pense numa cozinha: o que você
usa o tempo todo fica na bancada, e o resto espera na despensa.
<p align="center"><img src="docs/media/how-it-works.svg" width="860" alt="Os 24,576 experts do modelo: os mais usados na placa de vídeo, todos na RAM, uma tabela de consulta no SSD"></p>
- **O modelo é uma equipe de 24,576 pequenos especialistas ("experts").** Cada palavra precisa de só 10 deles.
- **Sua placa de vídeo** guarda os poucos milhares de experts usados com mais frequência. **Sua RAM** guarda todos
eles, e **seu processador** trabalha no resto ao mesmo tempo. **Seu SSD** guarda uma grande tabela de consulta.
<p align="center"><img src="docs/media/guess-and-check.svg" width="860" alt="Um pequeno ajudante adivinha as próximas palavras; o modelo grande confere todas de uma vez e fica com as certas"></p>
- **Adivinhar e conferir:** um pequeno ajudante adivinha as próximas palavras. O modelo grande confere todas de
uma vez. Você recebe a mesma resposta, 1.6-1.8x mais cedo.
- **Textos longos são lidos em grandes pedaços** (até 8,192 tokens por vez), a mais de 1,000 tokens por segundo.
A explicação mais longa: [docs/HOW_IT_WORKS.md](docs/HOW_IT_WORKS.md). Cada parte e seus números:
[os detalhes](docs/DETAILS.md#how-it-works) e o [artigo](docs/paper/Strata-Paper.pdf).
## Créditos e licença
O modelo é o [Qwen3.8-Flash-Next](https://huggingface.co/Qwen/Qwen3.8-Flash-Next), da equipe Qwen. Ele foi
comprimido por [ISTA-DASLab](https://huggingface.co/ISTA-DASLab/Qwen3.8-Flash-Next-GSQ-RCO-GGUF), UkisAI (Swift 1.5)
e Unsloth. O Strata usa partes do [llama.cpp / ggml](https://github.com/ggml-org/llama.cpp). Todos os créditos:
[docs/HOW_IT_WORKS.md](docs/HOW_IT_WORKS.md#credits). O Strata é de código aberto sob a [MIT License](LICENSE).
Algumas partes e todos os modelos têm licenças próprias ([quais](docs/HOW_IT_WORKS.md#license)).
## Apoie o Strata
O Strata é gratuito e de código aberto. Se ele é útil para você, você pode apoiar o desenvolvimento:
<p align="center"><a href="https://buymeacoffee.com/strataengine"><img src="https://cdn.buymeacoffee.com/buttons/v2/default-yellow.png" alt="Buy Me A Coffee" height="50"></a></p>
+200
View File
@@ -0,0 +1,200 @@
<h1 align="center">Strata</h1>
[English](README.md) · **简体中文** · [日本語](README.ja.md) · [Deutsch](README.de.md) · [Français](README.fr.md) · [Español](README.es.md) · [Português](README.pt-BR.md)
<p align="center"><b>在你自己的游戏电脑上运行 1250 亿参数的 AI 模型</b><br>
NVIDIA 或 AMD 显卡(12 GB 及以上)· Windows 或 Linux · 免费开源</p>
<p align="center"><a href="https://github.com/Niko1221/Strata/releases/download/v0.1.10/Pagoda.mp4"><img src="docs/media/pagoda-preview.webp" width="720" alt="Strata 的模型写出的体素宝塔花园,在浏览器中运行"></a><br>
<sub>体素宝塔花园,一次提示生成,在 RTX 5070 上用 Strata 运行(IQ3_S,128K 上下文)·
<a href="https://github.com/Niko1221/Strata/releases/download/v0.1.10/Pagoda.mp4">完整视频(49 秒)</a></sub></p>
Strata 能在普通电脑上运行 **[Qwen3.8-Flash-Next](https://huggingface.co/Qwen/Qwen3.8-Flash-Next)**。这是一个又大又聪明的
AI 模型,通常要用服务器才能跑。它能聊天、写代码、看图片,还能配合你的应用和编程智能体一起工作。所有数据都留在你的电脑上。
## 速度有多快?
我们在两台普通的游戏电脑上做了测试。一个 token 大约相当于 ¾ 个英文单词。
- **写回答:** 短对话中回复出现的速度。每秒 60 个 token 已经比你的阅读速度还快。
- **读提示:** 读入你发送内容的速度(这里是一份 32K token 的文档、代码或聊天记录)。
<table>
<tr><th>NVIDIA:RTX 5070(12 GB)、Ryzen 5 7600、64 GB 内存</th><th>AMD:RX 9070 XT(16 GB)、Ryzen 9 3900X、47 GB 内存</th></tr>
<tr><td>
| 规格 | 写回答 | 读提示 |
| --- | ---: | ---: |
| **Q2_0** | 94 tokens/s | 2,650 tokens/s |
| **IQ2_XS** | 79 tokens/s | 2,090 tokens/s |
| **IQ3_XXS** | 62 tokens/s | 1,750 tokens/s |
| **IQ3_S** | 53 tokens/s | 1,620 tokens/s |
| **Coder** | 55 tokens/s | 2,180 tokens/s |
</td><td>
| 规格 | 写回答 | 读提示 |
| --- | ---: | ---: |
| **Q2_0** | 60 tokens/s | 1,160 tokens/s |
| **IQ2_XS** | 52 tokens/s | 1,110 tokens/s |
| **Coder** | 44 tokens/s | 1,420 tokens/s |
</td></tr>
</table>
NVIDIA:Q2_0 用的是引擎 0.1.36,其他各行用的是 0.1.26(4K 回答,32K 提示)。完整表格见
[DETAILS.md](docs/DETAILS.md#speed-measured)。显存越大的显卡越快:RTX 3090(24 GB)写回答应该能达到每秒约
100-140 个 token。长对话和其他显卡的数据:[各模型的速度](docs/MODELS.md#how-fast-is-each-size)、
[社区测试结果](docs/COMMUNITY_BENCHMARKS.md)。
<p align="center"><a href="https://buymeacoffee.com/strataengine"><img src="https://cdn.buymeacoffee.com/buttons/v2/default-yellow.png" alt="请我喝杯咖啡" height="50"></a><br>
<sub>Strata 是免费的。如果它在你的电脑上跑得不错,请我喝杯咖啡,让这个项目继续下去。</sub></p>
## 你需要什么
| | |
| --- | --- |
| **显卡** | **NVIDIA** GeForce RTX 20、30、40 或 50 系列,或 **AMD** Radeon RX 7900 XT / XTX、RX 7800 XT / 7700 XT、RX 9060 XT、RX 9070 / 9070 XT、Radeon AI PRO R9700 或 RX 6800 / 6900 系列。需要 **12 GB 或以上显存**。 |
| **内存** | 32 GB 或以上。内存大小决定能装下[哪个模型](#该选哪个模型)。64 GB 可以运行所有规格。 |
| **硬盘** | 约 80 GB 可用空间。尽量用 SSD:第一次启动会快很多。 |
| **系统** | Windows 10 / 11 或 Linux,以及 NVIDIA 或 AMD 的最新显卡驱动。 |
其他的都由安装程序搞定。两到三张显卡可以分担同一个模型([多 GPU](docs/MULTI_GPU.md))。
实验性支持,由社区成员在自己的机器上编写和测试:
- **较老的显卡**(Tesla P40 / V100、GTX 10、Radeon VII / MI50、RX 6700 XT、RX 5500 XT):[较老的 GPU](docs/OLDER_GPUS.md)。
- **Intel Arc**,在 Linux 上从源码构建:[Intel Arc](docs/INTEL_ARC.md)。
- **AMD Ryzen AI Max(Strix Halo)**,在 Linux 上从源码构建:[Strix Halo](docs/STRIX_HALO.md)。
- **不支持 AVX2 的老处理器**:能用,但很慢。[较老的 CPU](docs/INSTALL.md#older-cpus-experimental)。
完整列表:[docs/INSTALL.md](docs/INSTALL.md#what-you-need)。
## 安装
### 让你的 AI 帮你装
你在用 AI 编程助手吗(Claude Code、Cursor、Codex、GitHub Copilot 等)?把下面这段粘贴给它:
```text
Set up Strata on this PC for me: https://github.com/Niko1221/Strata - follow docs/AI_SETUP.md in that repository.
```
它会检查你的显卡、内存和硬盘,选出合适的模型。然后安装并启动它,再告诉你怎么连接你的应用。AI 工具也可以通过
Strata 的 [MCP 服务器](docs/MCP_SERVER.md)来安装、启动和停止 Strata。
### 或者自己动手
[下载 Strata](https://github.com/Niko1221/Strata/archive/refs/heads/main.zip) 并解压(或者用 `git clone`)。
**Windows:** 双击 **`START-HERE.bat`**。**Linux:** 在 Strata 文件夹里运行 **`./setup.sh`**。
NVIDIA 和 AMD 的步骤完全一样。安装程序会识别你的显卡,并装好对应的引擎。它会问你几个问题:
- 用哪个模型、哪个规格,
- 上下文多大(模型能记住多少文字),
- 是否要识别图片。
每次直接按回车就是推荐选项。之后它会下载模型(约 70 GB)并启动。如果下载中断了,再运行一次即可:它会从中断的地方继续。
浏览器会打开 Strata 应用,地址是 `http://127.0.0.1:8080`。
> **模型启动时,你的电脑可能会变慢或卡住 1-3 分钟**(第一次最久)。
> Strata 会把 35-55 GB 加载到内存,并为显卡锁定其中一部分。这是正常的。请耐心等待,不要关闭窗口。
> 窗口里会显示 Strata 正在做什么。
**下次使用时**,再运行一次 `START-HERE.bat`(或 `./setup.sh`)。它会马上启动,已下载的东西不会重复下载。关闭它的窗口就能停止模型。
`UPDATE.bat`(`./update.sh`)只更新 Strata,不启动。更新、Docker、多张显卡、文件存放位置以及所有选项:
[docs/INSTALL.md](docs/INSTALL.md)。
## 该选哪个模型
安装程序会根据你的内存推荐一个。同一个模型有几种规格,压缩程度不同。规格越小越快,越大越聪明一些。
| 你的内存 | 选择 | 原因 |
| --- | --- | --- |
| **32 GB** | **Coder** | 32 GB 装得下,而且专为代码打造(如果显卡是 24 GB,Q2_0 和 IQ2_XS 也能跑) |
| **48 GB** | **IQ2_XS**(或 Q2_0,最快) | 更大的规格装不下 |
| **64 GB** | **IQ2_XS**(推荐),或 IQ3_XXS / IQ3_S | 所有规格都装得下;IQ3_S 最好,也最慢 |
| **96 GB 或以上** | **IQ3_S**,或 Unsloth 的 UD-IQ4_XS(约 4-bit) | 开着其他程序也能放下最大的规格 |
- **[Coder](docs/MODELS.md#coder):** 编程版本,去掉了一半专家。它达到完整模型 SWE-bench Verified 分数的 91%(由其作者测得),
32 GB 内存就能装下。代码以外的能力较弱,包括中文和其他中日韩文本(#438)。这类用途请选 Q2_0、IQ2_XS 或 IQ3_S,
它们保留了所有专家。
- **[Swift 1.5](docs/MODELS.md#swift-15):** 一个微调版本,回答前思考的时间短得多。你能更早拿到答案,质量基本不变。
- **[Unsloth UD-IQ4_XS](docs/MODELS.md#unsloth-ud-iq4_xs):** Unsloth 的约 4-bit 版本,质量介于 IQ3_S 和 UD-Q4_K_XL 之间。
下载 94 GB。内存少于约 80 GB 时,Strata 回答时要从 SSD 读取其中一部分,所以会更慢(NVMe SSD 有帮助)。
- **[Unsloth UD-Q4_K_XL](docs/MODELS.md#unsloth-ud-q4_k_xl-experimental)**(实验性):最接近完整模型。但 Strata
回答时要从 SSD 读取其中大部分内容,所以在 64 GB 的电脑上每秒只能写 7-8.5 个 token。
- **[OrcaRouter 的 Uncensored IQ3_XXS](docs/MODELS.md#orcarouter-uncensored-iq3_xxs):** 需要手动设置,不在安装程序的菜单里。
规格、下载以及各配置能装下什么:[docs/MODELS.md](docs/MODELS.md)。以后想再添加模型,运行
`SETUP.bat`(Linux:`./setup.sh --setup`)。
## 使用方法
<p align="center"><img src="docs/media/runpagoda.png" width="900" alt="Strata 应用的 Monitor 标签页,旁边是一个编程智能体"><br>
<sub>Strata 应用的 <b>Monitor</b>(左),编程智能体正在写视频里的宝塔花园(右)</sub></p>
- **在浏览器里:** 打开 `http://127.0.0.1:8080`。里面有 **Chat**(聊天)、实时 **Monitor**(监控模型和你的
GPU/CPU/内存),以及 **About**(设置和地址)。
- **你的应用和编程智能体:** 添加一个“OpenAI 兼容”的提供商,base URL 填 **`http://127.0.0.1:8080/v1`**。
API key 和模型名随便填都行。
- 使用 Anthropic API 的应用:`http://127.0.0.1:8080/v1/messages`(Claude Code:
`ANTHROPIC_BASE_URL=http://127.0.0.1:8080`)。
- Codex CLI 和其他使用 OpenAI Responses API 的应用:`/v1/responses`
([设置方法](docs/DETAILS.md#the-responses-api-and-codex-cli))。
- **思考:** 在聊天菜单或应用的“reasoning effort”里选择 **off、low、medium 或 high**。off 最快,
high 最适合难题。
- **图片:** 在安装时对“Images?”选是。然后在聊天里点 **Picture**,或在你的应用里附上图片。
AMD 显卡在 Linux 上通过处理器识别图片;在 Windows 上暂时还不行。
- **从手机或另一台电脑访问:** `START-HERE.bat --setup --host 0.0.0.0 --api-key <secret>`。一定要设置 key。
- **一次处理一个请求:** 默认情况下 Strata 一次只回答一个请求,其他请求排队等待。想同时回答多个,
设置 `"parallel": 2`([BATCHING.md](docs/BATCHING.md))。在 12 GB 显卡上,这会让每个回答变慢。
- **长提示:** Strata 会完整读入对话的第一条消息,大约每 30,000 个 token 需要 1 分钟。之后的消息几秒内就开始回答。
更多:[聊天记录存在哪里](docs/INSTALL.md#where-things-are-stored)、[API](docs/DETAILS.md#using-it)。
## 出问题了?
- **Strata 第一次启动时电脑卡死了。** 加载模型时这是正常的。请耐心等待,不要关闭窗口。
10 分钟后还卡着?重启电脑,关掉其他程序再试一次,或者换一个更小的规格。
- **下载或安装时中断了。** 再运行一次 `START-HERE.bat`(或 `./setup.sh`)。它会从中断的地方继续。
- **非常慢,硬盘灯一直闪,或者提示“the engine stopped unexpectedly”。** 你的电脑可用内存不够。
关掉其他程序(浏览器很占内存),或者换一个更小的规格(Q2_0 或 IQ2_XS)。
- **提示 8080 端口已被占用。** Strata 已经在运行了。找一下它的窗口。
更多问题和解决办法:[docs/TROUBLESHOOTING.md](docs/TROUBLESHOOTING.md)。还是解决不了?提交一个
[issue](https://github.com/Niko1221/Strata/issues),并附上 Strata 文件夹里的 `strata-<model>.log`。发现了安全问题?
请私下报告:[SECURITY.md](SECURITY.md)。
## 它是怎么工作的?
这类模型通常运行在拥有数百 GB 显存的服务器上。而你的显卡只有 12-24 GB。Strata 让模型装得下的办法是
**把工作分摊到整台电脑上**。可以想象一个厨房:常用的东西放在台面上,其余的放在储藏室里。
<p align="center"><img src="docs/media/how-it-works.svg" width="860" alt="模型的 24,576 个专家:最常用的在显卡上,全部在内存里,一张查找表在 SSD 上"></p>
- **这个模型是由 24,576 个小专家(“experts”)组成的团队。** 每个词只需要其中 10 个。
- **你的显卡** 存放最常用的几千个专家。**你的内存** 存放全部专家,
**你的处理器** 同时处理其余的部分。**你的 SSD** 存放一张大查找表。
<p align="center"><img src="docs/media/guess-and-check.svg" width="860" alt="一个小助手猜测接下来的几个词;大模型一次性全部检查,保留正确的"></p>
- **先猜,再检查:** 一个小助手先猜接下来的几个词,大模型再一次性检查它们。答案完全一样,但快 1.6-1.8 倍。
- **长文本分大块读入**(每次最多 8,192 个 token),速度超过每秒 1,000 个 token。
更详细的解释:[docs/HOW_IT_WORKS.md](docs/HOW_IT_WORKS.md)。每个部分及其数据:
[详细说明](docs/DETAILS.md#how-it-works)和[论文](docs/paper/Strata-Paper.pdf)。
## 致谢与许可证
模型是 Qwen 团队的 [Qwen3.8-Flash-Next](https://huggingface.co/Qwen/Qwen3.8-Flash-Next)。它由
[ISTA-DASLab](https://huggingface.co/ISTA-DASLab/Qwen3.8-Flash-Next-GSQ-RCO-GGUF)、UkisAI(Swift 1.5)
和 Unsloth 压缩。Strata 使用了 [llama.cpp / ggml](https://github.com/ggml-org/llama.cpp) 的部分代码。完整致谢:
[docs/HOW_IT_WORKS.md](docs/HOW_IT_WORKS.md#credits)。Strata 以 [MIT 许可证](LICENSE)开源。少数部分和每个模型
有各自的许可证([具体是哪些](docs/HOW_IT_WORKS.md#license))。
## 支持 Strata
Strata 免费且开源。如果它对你有用,欢迎支持它的开发:
<p align="center"><a href="https://buymeacoffee.com/strataengine"><img src="https://cdn.buymeacoffee.com/buttons/v2/default-yellow.png" alt="请我喝杯咖啡" height="50"></a></p>
+44
View File
@@ -0,0 +1,44 @@
# Security
## Reporting a vulnerability
Please report anything that should not be public until it is fixed **privately**, through GitHub's private
vulnerability reporting: the repository's **Security** tab, **Report a vulnerability**
([direct link](https://github.com/Niko1221/Strata/security/advisories/new)). Say what you found, how to reproduce it
(the request, the config keys involved, the Strata version), and what an attacker gains. We answer there, fix it in
a release, and credit you in the advisory unless you ask us not to.
Ordinary hardening ideas and findings that are safe to discuss in public are welcome as an
[issue](https://github.com/Niko1221/Strata/issues) (#544 collects audit findings) or as a pull request, which we
review like our own code.
Supported: the latest release. Fixes go into the next release, not into older ones.
## What the server exposes
Strata runs a model on your PC and serves it over HTTP (`serve/server.py`). Out of the box it is reachable from
this PC only. The details and every setting are in [docs/DETAILS.md](docs/DETAILS.md) ("From other devices",
"Host names", "Web pages without an API key", "Tools from MCP servers").
- **Where it listens.** `127.0.0.1` by default. `--host 0.0.0.0` (or `"host"` in `strata-<model>.json`) opens it to
your network, and the server then warns when no API key is set.
- **API key.** `"api_key"` in the run config (or `STRATA_API_KEY`) is required on `/v1/*` and on every endpoint
that shows the model's state, requests or answers (`/status`, `/metrics`, `/settings`, `/mcp`, `/props`, `/slots`,
`/api/requests`, `/config`) and on every `POST`. It is compared in constant time. Set one before you open the
server to your network or put a tunnel in front of it.
- **Host check (DNS rebinding, 0.1.38).** Without an API key the server answers only requests whose `Host` is a name
it knows (`localhost`, an IP address, the address it listens on and, when it listens beyond this PC, this PC's
name). Others get 403. `"allowed_hosts"` adds names; with an API key the key decides.
- **Origin check (0.1.38).** Without an API key, a browser `POST` to `/v1/*` from another web site's page (it carries
an `Origin` header) gets 403 unless that origin is Strata's own page, `localhost`, an allowed host, or listed in
`"trusted_origins"` / `"cors_origins"`. Changing settings (`/settings`, and `/config`: the few run config keys the
page's Model settings may change - never the network, key, MCP or program keys), `/load` and `/unload` and the
MCP tools are accepted only as JSON from Strata's own page (or `"trusted_origins"`), so a page elsewhere cannot
change settings or run tools.
- **CORS** is off unless `"cors_origins"` lists origins, and then only for `/v1/*`.
- **MCP tools** are opt-in: only the servers you put in the run config, only for requests from Strata's own page
that ask for them. They run with your user's rights, and the model decides when to call them.
- **The request monitor** (`/api-monitor`, which keeps the last prompts and answers in memory) is off unless
`"api_monitor": true` is set.
Strata has not had an outside security audit yet.
@@ -0,0 +1,121 @@
# Community benchmark: 2x AMD Instinct MI50 16 GB (gfx906), Xeon E5-2666 v3
Measured on 2026-10-03 by [JeanP00l](https://github.com/JeanP00l) on a Linux server. This tests the Coder
(IQ1_M) with a 131,072-token context, the layer split across two MI50 cards, and the gfx906 build of #638. That
build is an opt-in HIP build, separate from the wave32 RDNA backend.
The median decode throughput was **50.1 tok/s at 4,096 prompt tokens, 47.8 tok/s at 32,768 and 45.7 tok/s at
128,000**. Prompts were read at 321, 523 and 517 tok/s. The requests are synthetic code-explanation requests
with greedy decoding and a 256-token output cap. They do not establish general answer quality or performance on
other workloads.
## Hardware and software
- **GPUs:** 2x AMD Instinct MI50 16 GB (Vega 20, gfx906, wave64; HIP names them "AMD Radeon VII"). Power limit
85 W per card, set by the owner (the cards' own limit is higher). PCIe 3.0 x16 on both. Peer access between them
works, through the CPU's root complex (no direct bridge). Blower fans; no clock locking beyond the power limit.
The engine's PCIe probe measured 13.8 GB/s host to device.
- **CPU and RAM:** Intel Xeon E5-2666 v3 (10 cores, 20 threads, AVX2, no AVX-512); the engine used 9 expert-pool
workers plus its host thread. 32 GB DDR4 quad-channel (Linux reports 31.2 GiB). 8 GiB swap file. One SATA SSD.
- **OS and runtime:** Ubuntu 24.04.4 LTS, kernel 6.8.0-138-generic, the kernel's amdgpu driver. HIP 7.14 from the
community image `mixa3607/rocm-gfx906:7.14-complete` (AMD's current ROCm no longer ships gfx906 libraries).
- **Source:** [`mi50-bench-build`](https://github.com/JeanP00l/Strata/tree/mi50-bench-build) at `ea51fd3`, which is
`main` 99f3dbd (engine 0.1.38) with #638 (at 17beb33), #639, #640 and #637 merged. #638's later commits
(a914b13) only fix the CUDA/RDNA compile and do not change the gfx906 binary's behaviour.
- **Build:** `-DSTRATA_HIP_GFX906=ON -DCMAKE_HIP_ARCHITECTURES=gfx906 -DSTRATA_PORTABLE=ON`, ROCm clang, Ninja,
llama.cpp at the pinned 3cf0325.
- **Other services:** the machine's other GPU services were stopped, so the two cards ran only Strata. CPU services
(a database, a sync daemon) kept running. This was not a fully isolated operating system.
## Model and configuration
**Model:** [`ISTA-DASLab/Qwen3.8-Flash-Next-GSQ-RCO-Coder-GGUF`](https://huggingface.co/ISTA-DASLab/Qwen3.8-Flash-Next-GSQ-RCO-Coder-GGUF),
IQ1_M. The repository revision was not recorded; the shard SHA-256 hashes matched the `.sha256` files downloaded
with them:
- `Qwen3.8-Flash-Next-GSQ-RCO-IQ1_M-00001-of-00002.gguf` `e11083ba855e7666b48ea3f2db6a9c3a20c18751a012cc24f948de91b7087fad`
- `Qwen3.8-Flash-Next-GSQ-RCO-IQ1_M-00002-of-00002.gguf` `316b46f3a2dbd68c900f43136ab9449f9dcc3725dfd8c794847c204bc161e113`
The native pack and the MTP draft layer were prepared with the repository's tools on 2026-09-29. The expert profile is the bundled
`data/expert-profile-coder.bin`.
**Configuration** ([strata-coder.json](strata-coder.json); `/m` is the model directory, `/work` the build):
- context 131,072, `--kv int8 --kv-resident 32768` (32,768 KV cells per attention layer in VRAM, the rest streamed
from RAM);
- `layer_split 27`: layers 0-26 on GPU 0, 27-47 on GPU 1. With #639 each GPU loads only its own layers' dense
weights;
- expert cache `auto`, `--vram-reserve-mib 600`: 6,912 + 5,376 slots = **all 12,288 experts in VRAM**, prefilled
from the profile, no eviction;
- `--prefill 4096`, with the prompt path borrowing 1,193 + 1,084 cache slots;
- MTP `--spec 4 --spec-min-p 0.5`; suffix drafting on (the default);
- `--pcie-frac 0`, required with `STRATA_ARENA_MMAP=1` (#640, the expert arena as a read-only mapped file; 23.4
GiB of VRAM-held experts handed back to the OS at start);
- no vision, no calibration, no speed projection, no control vectors;
- reasoning off (`reasoning_effort: none`), temperature 0, 256 generated tokens per speed run.
## Method
[benchmark.py](benchmark.py) is the script of the
[RTX 5090 report](../2026-09-30-community-rtx-5090/README.md), unchanged. It builds deterministic synthetic Python
filler, puts a different nonce near the start of each request, and counts the rendered chat prompt with Strata's
tokenizer. Its request hashes and the output text of every run are in [results.json](results.json); the aggregates
are in [summary.json](summary.json).
The server ran in a container with both cards (the repository's `serve/` and `tools/` from the source above):
```bash
docker run --device=/dev/kfd --device=/dev/dri --group-add video --ipc=host --ulimit memlock=-1 \
-e HSA_OVERRIDE_GFX_VERSION=9.0.6 -e STRATA_ARENA_MMAP=1 ... \
python3 serve/server.py --engine strata --config strata-coder.json --port 8080
python3 benchmark.py --root /opt/strata --pack /m/pack-coder --url http://127.0.0.1:8080 --out results/
python3 tools/needle_bench.py --url http://127.0.0.1:8080 --lengths 32k,128k --depths 10,50,90 --out needles.json
```
- One short warm-up request is excluded.
- Three runs at each length ran serially, in increasing-length order, on the same loaded engine.
- All nine speed requests read their whole prompt: **zero reused tokens**.
- The expert cache was filled at start and kept between requests. Loading time is excluded.
- TTFT is streaming, from just before the HTTP request to the first nonempty text delta, over loopback.
- Prompt throughput is freshly read tokens / `prompt_ms`. Decode throughput is `engine_generated / decode_ms`.
- [engine.log](engine.log) is the engine's whole log for this session, including the start-up decisions.
## Results
Each cell is the median **[minimum-maximum]** of three runs. Every request generated 256 tokens and stopped at
the output limit. No speed request failed or was cancelled.
| Prompt tokens | Reused | Prompt tok/s | Decode tok/s | TTFT seconds | Total seconds |
| ---: | ---: | ---: | ---: | ---: | ---: |
| 4,096 | 0 | 320.9 [309.9-324.7] | 50.1 [49.3-54.0] | 12.81 [12.66-13.26] | 17.90 [17.38-18.43] |
| 32,768 | 0 | 522.8 [522.6-523.4] | 47.8 [47.6-48.5] | 62.81 [62.75-62.84] | 68.10 [68.10-68.15] |
| 128,000 | 0 | 516.8 [516.5-516.9] | 45.7 [44.8-51.5] | 248.09 [248.06-248.27] | 253.75 [253.03-253.84] |
- The decode expert-cache hit rate was 100% for every request. All experts are in VRAM; the CPU pool computes none.
- The 4K prompt reads at about 60% of the longer ones' speed, because a 4,096-token chunk pays the fixed per-chunk
cost once.
- Draft acceptance varies between runs (decode 44.8-51.5 tok/s at 128K) although the text is greedy.
**Memory**, from [telemetry.jsonl](telemetry.jsonl) (1,699 one-second samples over 1,763 s, from start through
the recall checks):
- VRAM peak: 15.42 GiB on GPU 0 and 14.70 GiB on GPU 1 (`mem_info_vram_used`, the whole card).
- RAM: `MemAvailable` never below **25.1 GiB** of 31.2.
- Swap: no growth during the run. The swap file held ~1.3 GiB from earlier use before the start.
These are sampled values; brief peaks between samples can be missed.
## Recall and limitations
The repository's unchanged `tools/needle_bench.py` found **all six needles**, at depths 10%, 50% and 90% at both
lengths ([needles.json](needles.json)):
- `32k` prompts were 25,776-25,777 tokens and `128k` prompts 103,725-103,727 tokens.
- The last 128K case reused 49,785 prompt tokens; the other five reused none.
**Limits of this report:**
- One machine, one quantization, one configuration, and a small synthetic workload.
- Long output, sampled decoding, thinking, coding-task correctness, vision, tool use, concurrency and a sustained
thermal run were not part of this report.
- For context, on the same machine and model, llama.cpp (3cf0325, ROCm) measured 26.9 tok/s `tg128` on 2026-09-29.
That is a different workload, not a controlled comparison.
@@ -0,0 +1,135 @@
#!/usr/bin/env python3
"""Serial, fresh-prompt Strata benchmark. Run inside its prepared environment."""
import argparse
import hashlib
import json
import statistics
import sys
import time
import urllib.request
from pathlib import Path
def get(url):
with urllib.request.urlopen(url, timeout=30) as response:
return json.load(response)
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--root", type=Path, default=Path("/workspace/Strata"))
parser.add_argument("--pack", type=Path, default=Path("/workspace/Strata-data/packs/iq2_xs"))
parser.add_argument("--url", default="http://127.0.0.1:18080")
parser.add_argument("--out", type=Path, default=Path("/workspace/results"))
parser.add_argument("--targets", default="4096,32768,128000")
parser.add_argument("--runs", type=int, default=3)
args = parser.parse_args()
args.out.mkdir(parents=True, exist_ok=True)
sys.path[:0] = [str(args.root), str(args.root / "tools")]
from strata_tokenizer import Tokenizer
from serve.frontend import ChatTemplate, openai_to_messages
directory = args.pack / "tokenizer"
vocab = json.loads((directory / "vocab.json").read_text())
tokens = [None] * len(vocab)
for token, number in vocab.items():
tokens[number] = token
tokenizer = Tokenizer(tokens, (directory / "merges.txt").read_text().splitlines(),
json.loads((directory / "token_type.json").read_text()))
template = ChatTemplate(directory / "chat_template.jinja")
initial = get(args.url + "/v1/status")
(args.out / "initial-status.json").write_text(json.dumps(initial, indent=2) + "\n")
rows = []
def request(content, maximum=256):
return {"model": "strata", "messages": [{"role": "user", "content": content}],
"temperature": 0, "reasoning_effort": "none", "max_tokens": maximum,
"stream": True, "stream_options": {"include_usage": True}}
def count(req):
messages, tools, kwargs = openai_to_messages(req)
return len(tokenizer.encode(template.render(messages, tools, **kwargs), parse_special=True))
def perform(label, req, expected=None):
body = json.dumps(req).encode()
(args.out / (label + "-request.json")).write_bytes(body + b"\n")
print("start", label, "tokens", expected, flush=True)
chunks, texts, first, finish = [], [], None, None
started = time.perf_counter()
wire = urllib.request.Request(args.url + "/v1/chat/completions", data=body,
headers={"Content-Type": "application/json"})
with urllib.request.urlopen(wire, timeout=1800) as response:
for line in response:
if not line.startswith(b"data: "):
continue
payload = line[6:].strip()
if payload == b"[DONE]":
break
chunk = json.loads(payload)
chunks.append(chunk)
for choice in chunk.get("choices", []):
delta = choice.get("delta", {})
text = (delta.get("content") or "") + (delta.get("reasoning_content") or "")
if text:
first = first if first is not None else time.perf_counter() - started
texts.append(text)
finish = choice.get("finish_reason") or finish
elapsed = time.perf_counter() - started
metrics = get(args.url + "/metrics")
engine = metrics["requests"][0]
usage = next((chunk["usage"] for chunk in reversed(chunks) if "usage" in chunk), {})
row = {"label": label, "expected_prompt_tokens": expected,
"request_sha256": hashlib.sha256(body).hexdigest(),
"client_ttft_s": first, "client_elapsed_s": elapsed, "finish_reason": finish,
"usage": usage, "engine": engine, "text": "".join(texts)}
if expected is not None and engine["prompt_tokens"] != expected:
raise RuntimeError(f"token count mismatch: {expected} vs {engine['prompt_tokens']}")
if not texts:
raise RuntimeError(f"no text received for {label}")
(args.out / (label + "-raw.json")).write_text(json.dumps({"chunks": chunks, "row": row}, indent=2) + "\n")
rows.append(row)
(args.out / "results.json").write_text(json.dumps(rows, indent=2) + "\n")
print("done", label, json.dumps({"ttft_s": first, "elapsed_s": elapsed, "engine": engine}), flush=True)
return row
perform("warmup", request("Reply with exactly the word READY.", 16))
# Each nonce occurs before the filler so previous long prompts cannot be reused.
filler = "\n".join(f"def task_{i:05d}(value: int) -> int: return (value * {(i % 97) + 1} + {i}) % 100003"
for i in range(12000))
ending = ("\n\nWrite a detailed explanation of the code above. Discuss deterministic integer transforms, "
"modulo arithmetic, testing, naming, complexity, and maintainability. Write at least 600 words.")
for target in map(int, args.targets.split(",")):
for run in range(1, args.runs + 1):
prefix = f"Benchmark nonce: series-{target}-trial-{run}.\nReview this synthetic Python module:\n"
lo, hi = 0, len(filler)
while lo < hi:
middle = (lo + hi + 1) // 2
if count(request(prefix + filler[:middle] + ending)) <= target:
lo = middle
else:
hi = middle - 1
req = request(prefix + filler[:lo] + ending)
actual = count(req)
if actual < target - 20 or actual > target:
raise RuntimeError(f"could not construct target {target}: {actual}")
perform(f"tokens-{target}-run-{run}", req, actual)
measured = [row for row in rows if row["label"] != "warmup"]
groups = {}
for target in map(int, args.targets.split(",")):
subset = [r for r in measured if r["label"].startswith(f"tokens-{target}-")]
values = {
"prompt_tokens": [r["engine"]["prompt_tokens"] for r in subset],
"cached_tokens": [r["engine"].get("reused", 0) for r in subset],
"output_tokens": [r["engine"]["engine_generated"] for r in subset],
"client_ttft_s": [r["client_ttft_s"] for r in subset],
"client_elapsed_s": [r["client_elapsed_s"] for r in subset],
"prefill_tok_s": [(r["engine"]["prompt_tokens"] - (r["engine"].get("reused") or 0)) / (r["engine"]["prompt_ms"] / 1000) for r in subset],
"decode_tok_s": [r["engine"]["engine_generated"] / (r["engine"]["decode_ms"] / 1000) for r in subset],
}
groups[str(target)] = {key: {"median": statistics.median(v), "min": min(v), "max": max(v)}
for key, v in values.items()}
(args.out / "summary.json").write_text(json.dumps(groups, indent=2) + "\n")
print("summary", json.dumps(groups), flush=True)
if __name__ == "__main__":
main()
@@ -0,0 +1,50 @@
{
"service": "strata",
"model": "qwen3.8-flash-next-coder",
"loaded": true,
"auto_load": true,
"structured_output": {
"formats": [
"json_object",
"json_schema"
],
"method": "prompt_and_validate",
"constrained_decoding": false,
"stream_buffered": true
},
"engine": "0.1.38",
"started": 1791055531,
"uptime_s": 2,
"cache_max_tokens": 131072,
"context": {
"native": 131072,
"max_positions": 131072
},
"concurrency": {
"serving": 1,
"requested": 1
},
"dialects": [
"/v1/chat/completions",
"/v1/messages"
],
"vision": {
"enabled": false,
"available": false,
"error": null
},
"activity": {
"requests": 0,
"in_flight": 0,
"last_request_at": null
},
"last_timings": null,
"machine": {
"at": 1791055534,
"gpu": null,
"ram": {
"used_gib": 4.9,
"total_gib": 31.2
}
}
}
@@ -0,0 +1,56 @@
[
{
"length": "32k",
"depth": 10,
"prompt_tokens": 25776,
"seconds": 52.6,
"found": true,
"word": "meadow-copper-504",
"answer": "meadow-copper-504"
},
{
"length": "32k",
"depth": 50,
"prompt_tokens": 25776,
"seconds": 50.7,
"found": true,
"word": "falcon-quartz-940",
"answer": "falcon-quartz-940"
},
{
"length": "32k",
"depth": 90,
"prompt_tokens": 25777,
"seconds": 50.8,
"found": true,
"word": "willow-cobalt-696",
"answer": "willow-cobalt-696"
},
{
"length": "128k",
"depth": 10,
"prompt_tokens": 103727,
"seconds": 206.9,
"found": true,
"word": "falcon-saffron-138",
"answer": "falcon-saffron-138"
},
{
"length": "128k",
"depth": 50,
"prompt_tokens": 103725,
"seconds": 205.9,
"found": true,
"word": "quartz-tundra-528",
"answer": "quartz-tundra-528"
},
{
"length": "128k",
"depth": 90,
"prompt_tokens": 103725,
"seconds": 112.5,
"found": true,
"word": "quartz-glacier-192",
"answer": "quartz-glacier-192"
}
]
@@ -0,0 +1,382 @@
[
{
"label": "warmup",
"expected_prompt_tokens": null,
"request_sha256": "b63e57873441a996a529b246a66cf12de70a7c671777f99329354d5c195f788f",
"client_ttft_s": 0.44287488800182473,
"client_elapsed_s": 0.5121355729934294,
"finish_reason": "stop",
"usage": {
"prompt_tokens": 19,
"completion_tokens": 2,
"total_tokens": 21,
"prompt_tokens_details": {
"cached_tokens": 0
}
},
"engine": {
"projection": null,
"time": 1791055534.084382,
"duration_s": 0.5,
"finish": "stop",
"prompt_tokens": 19,
"reused": 0,
"output_tokens": 2,
"prompt_total": 19,
"prompt_read": 19,
"engine_generated": 2,
"prompt_ms": 398.1,
"decode_ms": 108.4,
"decode_tok_s": 18.5,
"hit_rate": 1.0,
"ram_blobs": 0,
"file_blobs": 0,
"file_mb": 0.0,
"drafts_offered": 3,
"drafts_accepted": 3
},
"text": "READY"
},
{
"label": "tokens-4096-run-1",
"expected_prompt_tokens": 4096,
"request_sha256": "5d94c652067354bbe72930d2bdad3886564f08cf2546b32411af02d33ad3ce05",
"client_ttft_s": 13.26403624098748,
"client_elapsed_s": 18.427036346998648,
"finish_reason": "length",
"usage": {
"prompt_tokens": 4096,
"completion_tokens": 256,
"total_tokens": 4352,
"prompt_tokens_details": {
"cached_tokens": 0
}
},
"engine": {
"projection": null,
"time": 1791055535.8394356,
"duration_s": 18.4,
"finish": "length",
"prompt_tokens": 4096,
"reused": 0,
"output_tokens": 256,
"prompt_total": 4096,
"prompt_read": 4096,
"engine_generated": 256,
"prompt_ms": 13216.0,
"decode_ms": 5189.0,
"decode_tok_s": 49.3,
"hit_rate": 1.0,
"ram_blobs": 0,
"file_blobs": 0,
"file_mb": 0.0,
"drafts_offered": 210,
"drafts_accepted": 145
},
"text": "The provided Python module presents a series of deterministic integer transforms, each encapsulated within a uniquely named function. These functions, labeled from `task_00000` to `task_00112` (and potentially beyond), follow a consistent mathematical pattern: they take an integer input, apply a linear transformation of the form `(value * multiplier + offset) % modulus`, and return the result. This structure is a classic example of a linear congruential generator (LCG) component, often used in pseudo-random number generation, hashing, or cryptographic primitives, though here it appears to be a synthetic benchmark or test harness.\n\n### Deterministic Integer Transforms and Modulo Arithmetic\n\nEach function implements a linear congruential step: `f(x) = (a * x + c) % m`, where `a` is the multiplier, `c` is the increment, and `m` is the modulus (100003 in all cases). The modulus 100003 is a prime number, which is significant because it ensures that the multiplicative group modulo 100003 is cyclic, and for certain choices of `a` and `c`, the transformation can be a permutation of the"
},
{
"label": "tokens-4096-run-2",
"expected_prompt_tokens": 4096,
"request_sha256": "8da8a04e70e4d916f6630729082bdfb9d6ebbb47117aad60bc0541bce198a213",
"client_ttft_s": 12.663496508001117,
"client_elapsed_s": 17.377855258993804,
"finish_reason": "length",
"usage": {
"prompt_tokens": 4096,
"completion_tokens": 256,
"total_tokens": 4352,
"prompt_tokens_details": {
"cached_tokens": 0
}
},
"engine": {
"projection": null,
"time": 1791055555.3915784,
"duration_s": 17.4,
"finish": "length",
"prompt_tokens": 4096,
"reused": 0,
"output_tokens": 256,
"prompt_total": 4096,
"prompt_read": 4096,
"engine_generated": 256,
"prompt_ms": 12615.4,
"decode_ms": 4740.3,
"decode_tok_s": 54.0,
"hit_rate": 1.0,
"ram_blobs": 0,
"file_blobs": 0,
"file_mb": 0.0,
"drafts_offered": 208,
"drafts_accepted": 156
},
"text": "The provided Python module consists of a series of functions, each named with a specific pattern (`task_XXXXX`), that perform a deterministic integer transformation. Each function takes a single integer argument `value` and returns an integer result computed using a linear transformation followed by a modulo operation. The general form of each function is:\n\n```python\ndef task_XXXXX(value: int) -> int:\n return (value * A + B) % 100003\n```\n\nwhere `A` and `B` are constants specific to each function. The modulus `100003` is a prime number, which is significant in modular arithmetic because it ensures that the multiplicative group modulo 100003 is cyclic and that every non-zero element has a multiplicative inverse. This property is often exploited in cryptographic algorithms, hash functions, and pseudo-random number generators.\n\n### Deterministic Integer Transforms\n\nEach function implements a linear congruential transformation, a common technique in number theory and computer science. The transformation `value * A + B` is an affine transformation over the integers. When combined with the modulo operation, it becomes a function from the finite field (or ring) of integers modulo 10"
},
{
"label": "tokens-4096-run-3",
"expected_prompt_tokens": 4096,
"request_sha256": "946079edb58e764050805886156dc8f877cc29e1ca8d01c84e9a1e7917d49492",
"client_ttft_s": 12.811999237994314,
"client_elapsed_s": 17.900370211005793,
"finish_reason": "length",
"usage": {
"prompt_tokens": 4096,
"completion_tokens": 256,
"total_tokens": 4352,
"prompt_tokens_details": {
"cached_tokens": 0
}
},
"engine": {
"projection": null,
"time": 1791055573.9388902,
"duration_s": 17.9,
"finish": "length",
"prompt_tokens": 4096,
"reused": 0,
"output_tokens": 256,
"prompt_total": 4096,
"prompt_read": 4096,
"engine_generated": 256,
"prompt_ms": 12763.9,
"decode_ms": 5114.7,
"decode_tok_s": 50.1,
"hit_rate": 1.0,
"ram_blobs": 0,
"file_blobs": 0,
"file_mb": 0.0,
"drafts_offered": 222,
"drafts_accepted": 146
},
"text": "The provided Python module presents a series of deterministic integer transforms, each encapsulated within a uniquely named function. These functions, labeled from `task_00000` to `task_00112` (and potentially beyond), follow a consistent mathematical pattern: they take an integer input, apply a linear transformation, and return the result modulo a large prime number, specifically 100003. This structure serves as a synthetic benchmark for evaluating code generation, pattern recognition, and algorithmic consistency in automated systems.\n\n### Deterministic Integer Transforms and Modulo Arithmetic\n\nAt the core of each function is the expression `(value * multiplier + offset) % 100003`. This is a classic linear congruential generator (LCG) step, often used in pseudo-random number generation or cryptographic hashing, though here it is applied deterministically to a single input. The multiplier and offset vary per function, creating a unique mapping for each task. For instance, `task_00000` uses a multiplier of 1 and an offset of 0, effectively returning `value % 100003`. In contrast, `task_00097` uses a multiplier of 1"
},
{
"label": "tokens-32768-run-1",
"expected_prompt_tokens": 32768,
"request_sha256": "e37cfc2ab9867d91130927440df24a57255a736b8d0d829d07d828e22e3de572",
"client_ttft_s": 62.74767600699852,
"client_elapsed_s": 68.09618598900852,
"finish_reason": "length",
"usage": {
"prompt_tokens": 32768,
"completion_tokens": 256,
"total_tokens": 33024,
"prompt_tokens_details": {
"cached_tokens": 0
}
},
"engine": {
"projection": null,
"time": 1791055594.1634562,
"duration_s": 68.0,
"finish": "length",
"prompt_tokens": 32768,
"reused": 0,
"output_tokens": 256,
"prompt_total": 32768,
"prompt_read": 32768,
"engine_generated": 256,
"prompt_ms": 62608.4,
"decode_ms": 5375.3,
"decode_tok_s": 47.6,
"hit_rate": 1.0,
"ram_blobs": 0,
"file_blobs": 0,
"file_mb": 0.0,
"drafts_offered": 230,
"drafts_accepted": 152
},
"text": "This Python module presents a highly repetitive, synthetic collection of functions that serve as a benchmark for deterministic integer transforms. The code consists of a series of functions named `task_00000` through `task_00888` (and potentially beyond), each performing a specific linear transformation on an input integer `value` and returning the result modulo 100003.\n\n### Deterministic Integer Transforms and Modulo Arithmetic\n\nAt the core of each function is a linear congruential generator (LCG) style operation: `(value * multiplier + addend) % 100003`. The modulus, 100003, is a prime number. This choice of modulus is significant because it ensures that the multiplicative group of integers modulo 100003 is cyclic, which is a desirable property for cryptographic or pseudo-random number generation contexts, although here it is used for deterministic hashing or transformation.\n\nThe structure of the functions reveals a pattern in the `multiplier` and `addend` parameters. For the first 97 functions (`task_00000` to `task_00096`), the multiplier increases linearly from 1 to"
},
{
"label": "tokens-32768-run-2",
"expected_prompt_tokens": 32768,
"request_sha256": "ed2dfaa11aecad9f4563f33bb5f5d4b8704486508700b8fa572722cc1225ee26",
"client_ttft_s": 62.81415034100064,
"client_elapsed_s": 68.14694995200261,
"finish_reason": "length",
"usage": {
"prompt_tokens": 32768,
"completion_tokens": 256,
"total_tokens": 33024,
"prompt_tokens_details": {
"cached_tokens": 0
}
},
"engine": {
"projection": null,
"time": 1791055664.4150822,
"duration_s": 68.0,
"finish": "length",
"prompt_tokens": 32768,
"reused": 0,
"output_tokens": 256,
"prompt_total": 32768,
"prompt_read": 32768,
"engine_generated": 256,
"prompt_ms": 62675.9,
"decode_ms": 5359.3,
"decode_tok_s": 47.8,
"hit_rate": 1.0,
"ram_blobs": 0,
"file_blobs": 0,
"file_mb": 0.0,
"drafts_offered": 222,
"drafts_accepted": 147
},
"text": "This Python module presents a highly repetitive, synthetic collection of functions that perform deterministic integer transforms. Each function follows a strict pattern: it accepts an integer `value`, applies a linear transformation of the form `(value * multiplier + offset) % modulus`, and returns the result. The modulus is consistently set to `100003`, which is a prime number. This choice of modulus is significant because it ensures that the multiplicative group of integers modulo 100003 is cyclic, and every non-zero multiplier has a unique modular inverse. This property is often desirable in cryptographic or hashing contexts, though here it appears to be used for simple arithmetic obfuscation or benchmarking.\n\n### Deterministic Integer Transforms and Modulo Arithmetic\n\nThe core operation in each function is a linear congruential generator (LCG) step, commonly used in pseudo-random number generation. The formula $f(x) = (ax + c) \\pmod m$ defines an affine transformation over the finite field $\\mathbb{Z}_m$. In this module, $m = 100003$. The multipliers ($a$) and offsets ($c$) vary per function. For example, `task_00000`"
},
{
"label": "tokens-32768-run-3",
"expected_prompt_tokens": 32768,
"request_sha256": "2a3c7342622e80c3cd8b37e062f0a2f64640c119ece8cccbf168639041eac079",
"client_ttft_s": 62.844907227001386,
"client_elapsed_s": 68.09568050300004,
"finish_reason": "length",
"usage": {
"prompt_tokens": 32768,
"completion_tokens": 256,
"total_tokens": 33024,
"prompt_tokens_details": {
"cached_tokens": 0
}
},
"engine": {
"projection": null,
"time": 1791055734.689819,
"duration_s": 68.0,
"finish": "length",
"prompt_tokens": 32768,
"reused": 0,
"output_tokens": 256,
"prompt_total": 32768,
"prompt_read": 32768,
"engine_generated": 256,
"prompt_ms": 62706.6,
"decode_ms": 5277.5,
"decode_tok_s": 48.5,
"hit_rate": 1.0,
"ram_blobs": 0,
"file_blobs": 0,
"file_mb": 0.0,
"drafts_offered": 234,
"drafts_accepted": 153
},
"text": "This Python module presents a highly repetitive, synthetic collection of functions that perform deterministic integer transforms. Each function follows a strict pattern: it accepts an integer `value`, applies a linear transformation of the form `(value * multiplier + offset) % modulus`, and returns the result. The modulus is consistently set to `100003`, which is a prime number. This choice of modulus is significant because it ensures that the multiplicative group of integers modulo 100003 is cyclic, meaning that for any non-zero multiplier, the transformation is bijective (a permutation) over the set of integers modulo 100003. This property is often desirable in cryptographic contexts or pseudo-random number generation, although here it appears to be used for benchmarking or synthetic data generation.\n\n### Deterministic Integer Transforms and Modulo Arithmetic\n\nThe core operation in each function is a linear congruential generator (LCG) step, defined by the formula $f(x) = (ax + c) \\pmod m$. In this module, $m = 100003$. The multiplier $a$ and the increment $c$ vary per function. For example, `task_00000` uses $a"
},
{
"label": "tokens-128000-run-1",
"expected_prompt_tokens": 128000,
"request_sha256": "3235aac3f934cf1d889c25973ca172b355509dc2522fdfb463fbefe979a9075d",
"client_ttft_s": 248.26734286299325,
"client_elapsed_s": 253.83500957599608,
"finish_reason": "length",
"usage": {
"prompt_tokens": 128000,
"completion_tokens": 256,
"total_tokens": 128256,
"prompt_tokens_details": {
"cached_tokens": 0
}
},
"engine": {
"projection": null,
"time": 1791055809.118869,
"duration_s": 253.5,
"finish": "length",
"prompt_tokens": 128000,
"reused": 0,
"output_tokens": 256,
"prompt_total": 128000,
"prompt_read": 128000,
"engine_generated": 256,
"prompt_ms": 247841.6,
"decode_ms": 5596.2,
"decode_tok_s": 45.7,
"hit_rate": 1.0,
"ram_blobs": 0,
"file_blobs": 0,
"file_mb": 0.0,
"drafts_offered": 232,
"drafts_accepted": 152
},
"text": "This Python module represents a synthetic benchmark designed to test the performance and correctness of integer arithmetic operations, specifically focusing on linear transforms and modular arithmetic. The code consists of a series of functions, each named `task_XXXXX`, where `XXXXX` is a zero-padded integer index. Each function accepts a single integer argument `value` and returns an integer result.\n\n### 1. Deterministic Integer Transforms\n\nThe core logic of every function in this module is identical in structure:\n```python\nreturn (value * A + B) % M\n```\nWhere:\n- `value` is the input integer.\n- `A` is a multiplier (coefficient).\n- `B` is an additive constant.\n- `M` is the modulus, which is consistently `100003` throughout the entire module.\n\nThis represents a **linear congruential transform**. In computer science, such transforms are fundamental in pseudorandom number generation (LCG), hashing functions, and cryptographic primitives. The specific form `(value * A + B) % M` ensures that the output is always within the range `[0, M-1]`, regardless of how large the input `value` is.\n\n### 2"
},
{
"label": "tokens-128000-run-2",
"expected_prompt_tokens": 128000,
"request_sha256": "5f4b8bc9f0afc7765320abd89960c0938e0d7cf5974e962fdbd0613fef31c8d4",
"client_ttft_s": 248.0881588330085,
"client_elapsed_s": 253.0268927290017,
"finish_reason": "length",
"usage": {
"prompt_tokens": 128000,
"completion_tokens": 256,
"total_tokens": 128256,
"prompt_tokens_details": {
"cached_tokens": 0
}
},
"engine": {
"projection": null,
"time": 1791056068.9450965,
"duration_s": 252.7,
"finish": "length",
"prompt_tokens": 128000,
"reused": 0,
"output_tokens": 256,
"prompt_total": 128000,
"prompt_read": 128000,
"engine_generated": 256,
"prompt_ms": 247669.4,
"decode_ms": 4967.1,
"decode_tok_s": 51.5,
"hit_rate": 1.0,
"ram_blobs": 0,
"file_blobs": 0,
"file_mb": 0.0,
"drafts_offered": 205,
"drafts_accepted": 163
},
"text": "This Python module represents a synthetic benchmark designed to test the performance and correctness of integer arithmetic operations, specifically focusing on linear transforms and modular arithmetic. The code consists of a series of functions, each named `task_XXXXX`, where `XXXXX` is a zero-padded integer index. Each function accepts a single integer argument `value` and returns an integer result.\n\n### 1. Deterministic Integer Transforms\n\nThe core logic of each function is a linear transformation of the form:\n$$ f(x) = (a \\cdot x + b) \\pmod m $$\nwhere:\n- $x$ is the input `value`.\n- $a$ is the multiplier (coefficient).\n- $b$ is the additive constant.\n- $m$ is the modulus, which is consistently set to `100003` across all functions.\n\nThe multiplier $a$ and the constant $b$ vary deterministically based on the function's index. For example:\n- `task_00000`: $a=1, b=0$\n- `task_00001`: $a=2, b=1$\n- `task_00002`:"
},
{
"label": "tokens-128000-run-3",
"expected_prompt_tokens": 128000,
"request_sha256": "5c3fb71db9f9f44765f28389d1032f2c8b6687d1bf5ca6f33aeb924072cfa6e4",
"client_ttft_s": 248.0611828550027,
"client_elapsed_s": 253.7529949669988,
"finish_reason": "length",
"usage": {
"prompt_tokens": 128000,
"completion_tokens": 256,
"total_tokens": 128256,
"prompt_tokens_details": {
"cached_tokens": 0
}
},
"engine": {
"projection": null,
"time": 1791056328.0391443,
"duration_s": 253.4,
"finish": "length",
"prompt_tokens": 128000,
"reused": 0,
"output_tokens": 256,
"prompt_total": 128000,
"prompt_read": 128000,
"engine_generated": 256,
"prompt_ms": 247650.3,
"decode_ms": 5720.0,
"decode_tok_s": 44.8,
"hit_rate": 1.0,
"ram_blobs": 0,
"file_blobs": 0,
"file_mb": 0.0,
"drafts_offered": 235,
"drafts_accepted": 149
},
"text": "This Python module represents a synthetic benchmark designed to test the performance and correctness of integer arithmetic operations, specifically focusing on linear transformations combined with modular arithmetic. The code consists of a series of functions, each named `task_XXXXX`, where `XXXXX` is a zero-padded integer index. Each function accepts a single integer argument `value` and returns a new integer calculated using the formula:\n\n$$ f(value) = (value \\times A + B) \\pmod{M} $$\n\nwhere $A$ is a multiplier, $B$ is an additive constant, and $M$ is the modulus, which is consistently set to `100003` throughout the module.\n\n### Deterministic Integer Transforms and Modulo Arithmetic\n\nThe core logic of each function is a deterministic linear congruential transformation. In computer science, such transformations are fundamental to pseudorandom number generators (PRNGs), hash functions, and cryptographic primitives. The specific form $ (value \\times A + B) \\pmod{M} $ ensures that the output is always within the range $[0, M-1]$.\n\nThe modulus $M = 100003$ is a prime number. This choice is significant because"
}
]
@@ -0,0 +1,45 @@
{
"exe": "/work/build-906/strata",
"args": [
"--pack",
"/m/pack-coder",
"--native",
"/m/coder/Qwen3.8-Flash-Next-GSQ-RCO-IQ1_M-00001-of-00002.gguf",
"--ple-gguf",
"/m/coder/Qwen3.8-Flash-Next-GSQ-RCO-IQ1_M-00002-of-00002.gguf",
"--expert-profile",
"/opt/strata/data/expert-profile-coder.bin",
"--expert-cache",
"auto",
"--prefill",
"4096",
"--spec",
"4",
"--spec-min-p",
"0.5",
"--mtp",
"/m/mtp/rt",
"--max-context",
"131072",
"--kv",
"int8",
"--vram-reserve-mib",
"600",
"--kv-resident",
"32768",
"--pcie-frac",
"0"
],
"cwd": "/opt/strata",
"tokenizer": "/m/pack-coder/tokenizer",
"model_name": "qwen3.8-flash-next-coder",
"log": "/work/strata-coder.log",
"lib_dirs": [],
"port": 8080,
"host": "0.0.0.0",
"gpu": [
0,
1
],
"layer_split": "27"
}
@@ -0,0 +1,113 @@
{
"4096": {
"prompt_tokens": {
"median": 4096,
"min": 4096,
"max": 4096
},
"cached_tokens": {
"median": 0,
"min": 0,
"max": 0
},
"output_tokens": {
"median": 256,
"min": 256,
"max": 256
},
"client_ttft_s": {
"median": 12.811999237994314,
"min": 12.663496508001117,
"max": 13.26403624098748
},
"client_elapsed_s": {
"median": 17.900370211005793,
"min": 17.377855258993804,
"max": 18.427036346998648
},
"prefill_tok_s": {
"median": 320.90505253096626,
"min": 309.9273607748184,
"max": 324.68253087496237
},
"decode_tok_s": {
"median": 50.05181144544157,
"min": 49.3351320100212,
"max": 54.00502077927557
}
},
"32768": {
"prompt_tokens": {
"median": 32768,
"min": 32768,
"max": 32768
},
"cached_tokens": {
"median": 0,
"min": 0,
"max": 0
},
"output_tokens": {
"median": 256,
"min": 256,
"max": 256
},
"client_ttft_s": {
"median": 62.81415034100064,
"min": 62.74767600699852,
"max": 62.844907227001386
},
"client_elapsed_s": {
"median": 68.09618598900852,
"min": 68.09568050300004,
"max": 68.14694995200261
},
"prefill_tok_s": {
"median": 522.8165850031671,
"min": 522.5606236026192,
"max": 523.3802492956216
},
"decode_tok_s": {
"median": 47.767432313921596,
"min": 47.625248823321485,
"max": 48.50781620085268
}
},
"128000": {
"prompt_tokens": {
"median": 128000,
"min": 128000,
"max": 128000
},
"cached_tokens": {
"median": 0,
"min": 0,
"max": 0
},
"output_tokens": {
"median": 256,
"min": 256,
"max": 256
},
"client_ttft_s": {
"median": 248.0881588330085,
"min": 248.0611828550027,
"max": 248.26734286299325
},
"client_elapsed_s": {
"median": 253.7529949669988,
"min": 253.0268927290017,
"max": 253.83500957599608
},
"prefill_tok_s": {
"median": 516.8179839737974,
"min": 516.4588995551998,
"max": 516.857843499483
},
"decode_tok_s": {
"median": 45.745327186305,
"min": 44.75524475524476,
"max": 51.5391274586781
}
}
}
File diff suppressed because it is too large. Load diff
@@ -0,0 +1,104 @@
# V100 (sm_70): a tensor-core QSA prompt attention (2026-10-03)
On sm_70 the prompt path ran the QSA attention on the decode kernel, one query at a time with FP32 FMAs. `prompt_attn_v70_kernel`
(`src/kernels/cuda/qsa_prompt_attn.cu`) does the two matrix products on Volta's tensor cores (`mma.sync.m8n8k4`).
`STRATA_PROMPT_ATTN_OLD=1` is the old path, so every number below is the same binary with and without that switch.
## Rig
- Tesla V100-PCIE-32GB (sm_70, PCIe gen3 x16), driver 550.120, one card used; a second V100 and a Quadro RTX 4000 (sm_75) are in the
PC and hidden with `CUDA_VISIBLE_DEVICES`. Intel Core i9-7960X (AVX-512), 125 GB RAM, Linux 6.8, CUDA 12.8, GCC 13.
- Engine: `origin/main` at `99f3dbd` (0.1.35 + 83 commits) with this change, built with
`-DCMAKE_CUDA_ARCHITECTURES=70 -DSTRATA_EXPERIMENTAL_SM60=ON`.
- Model: Unsloth `Qwen3.8-Flash-Next-UD-IQ4_XS` (3 shards), packed with `tools/iq_pack.py --compat-bf16`, MTP head from
`tools/mtp_fetch.py`. Not one of setup's models (see the limits). `--max-context 204800`, `--kv int8`, `--spec 2`.
## Prompt speed
One server start per configuration (cold), a 2K warm-up request, then three prompts of random words, each answered greedily
(temperature 0, 48 tokens). The numbers are the server's own line `strata serve: prompt N tokens = 0 reused + N read in T ms`. One run per
cell; repeated starts of the same configuration gave the same seconds.
| Prompt tokens | old path | new kernel | gain | read time, old -> new |
| ---: | ---: | ---: | ---: | ---: |
| 7,194 | 1,010 tok/s | 1,164 tok/s | +15% | 7.1 s -> 6.2 s |
| 28,650 | 1,063 tok/s | 1,251 tok/s | +18% | 26.9 s -> 22.9 s |
| 114,338 | 973 tok/s | 1,123 tok/s | +15% | 117.5 s -> 101.8 s |
Decode is unchanged within the noise (38-51 tok/s, 48 tokens per request): it does not use this kernel.
## Where the time went (nsys, one V100, 28,650-token prompt, GPU kernel time of the whole capture: prefill plus 128-193 decode tokens)
`strata-1gpu-32k-*.nsys-rep`; the "before" capture is 0.1.35 (`d9ab843`), the "after" one is the same base with this change and the BF16 change
of #540 / the closed #593 (BF16 GEMMs through FP16; that one only removes `magma_sgemmEx`, which is not in this table).
| | before | after |
| --- | ---: | ---: |
| QSA attention on the decode kernel (`attn_chunk_kernel<1>`) | 5.78 s (19.2%) | 0.18 s (decode steps only) |
| `prompt_attn_v70_kernel` | - | 2.16 s (9.0%; 12% of the prompt part) |
## Accuracy
`qsa_prompt_attn_parity` (synthetic, no model; build with `-DSTRATA_PARITY_PROMPT_ATTN=ON`), V100, 32K context, 2,048 queries per chunk, output scale 3.6:
| KV | error vs FP64, old kernel | error vs FP64, new kernel | time per chunk, old -> new |
| --- | ---: | ---: | ---: |
| int8 | 2.4e-6 | 5.1e-6 | 38.1 -> 13.4 ms (2.8x) |
| fp16 | 1.9e-6 | 5.3e-6 | 37.3 -> 28.3 ms (1.3x) |
The 1,500- and 2,100-token contexts also pass (int8: 3.2x and 2.2x). The test's Q4_0 cases are skipped below sm_80, where the dispatcher keeps the
old kernel (upstream's mode 4 is sm_80+ only).
## Output checks
- **Needle recall** (`tools/needle_bench.py --lengths 8k,32k,128k --depths 10,30,50,70,90`, greedy, thinking off, a fresh server per
configuration; prompts of 7.9K, 32.0K and 122.7K tokens): **15 of 15 found with the old path, 15 of 15 with the new kernel.** The read times of
the later depths are shorter because the server reuses the checkpoint of the text before the needle.
- **Greedy output** (48 tokens, the three prompts above): identical for the 28,650- and 114,338-token prompts. For the 7,194-token prompt the
answer starts the same and **differs later**: the kernel sums in another order and a greedy decode can follow a near tie elsewhere. A recall test
with a code word does not tell small shifts of the output distribution apart, and a teacher-forced top-1 comparison was not run.
## Distribution check (teacher-forced, against the old path and against controls)
The method of `docs/UNSLOTH_Q4.md`: `STRATA_LOGPOS=<file> STRATA_LOGPOS_TOPK=256` on a serve engine (`--short-read 640 --adapt-every 100000`, greedy,
`--max-context 204800`). Each prompt is a chat: a long first message (8,543 / 32,906 / 99,040 tokens of this repository's docs and code, read by the
batched prompt path, i.e. by the kernel under test), a short reply, and a last message of ~600 tokens of other repository text that goes through the verify
windows, where every token is scored: 617 positions per prompt. KL is old || other over the old run's top 256 plus a bucket for the rest.
| comparison | KL mean (8.5K / 33K / 99K) | argmax same | top-10 overlap |
| --- | --- | --- | --- |
| old path vs old path (a second run) | 0 / 0 / 0 | 100% | 100% |
| new kernel vs new kernel (a second run) | 0 / 0 / 0 | 100% | 100% |
| **old path vs new kernel** | 0.028 / 0.040 / 0.116 | 97.7 / 93.2 / 86.4% | 93.3 / 91.9 / 88.4% |
| control A: old path, BF16 GEMMs without the FP16 detour (`STRATA_BF16_VIA_F16=0`) | 0.026 / 0.051 / 0.090 | 97.2 / 91.4 / 91.2% | 92.9 / 91.4 / 88.5% |
| control B: old path, `--prefill 2048` instead of auto (8192) | 0.023 / 0.035 / 0.038 | 97.9 / 94.8 / 95.6% | 92.9 / 92.2 / 89.7% |
Median KL old vs new: 0.0007 / 0.0088 / 0.022; p99: 0.35 / 0.39 / 1.12 (heavy-tailed). The perplexity of the true tokens moves a lot under every
change (old 365 / 127 / 24; new 346 / 192 / 12; control A 395 / 161 / 18), so it says nothing about quality here.
Reading: the model amplifies small numeric differences, and both controls, which change nothing but the order of the rounding, move the distribution as
much as the kernel does. At 99K the kernel is somewhat above both controls; this is one document and 617 positions, so a real effect there is neither
confirmed nor excluded.
**The kernel on the model's own data.** A temporary check (not in this PR) ran the decode kernel beside the new one on the same inputs, for every 8th batch of
32 queries, in all 12 QSA layers, on the three prompts above (positions up to 98K, ~5 million (query, head) rows of 256): ||new - old|| / ||old|| per row is
4.4e-7 on average and 4.8e-6 in the worst row; the largest absolute difference is 4e-5 against values up to 31. No trend over the layers or the context
length. That is FP32 rounding level, so the differences above come from the model's sensitivity, not from the kernel's arithmetic.
## Limits
- sm_70 only (`__CUDA_ARCH__` 700-749). Q4_0 KV (`--kv q4_0`) and the QSA block scores (`block_scores_kernel`, the warp kernel below sm_80) still use their
old kernels on Volta.
- One request, one card. The Unsloth IQ4_XS pack is not a setup model; the GSQ-RCO quants were not run on this rig.
- The profile above is one capture of one prompt. The distribution check is one document per length; the 99K row is the least certain.
## Reproduce
```sh
cmake -S . -B build -DSTRATA_ENABLE_CUDA=ON -DSTRATA_BUILD_TESTS=OFF -DSTRATA_PARITY_PROMPT_ATTN=ON \
-DCMAKE_CUDA_ARCHITECTURES=70 -DSTRATA_EXPERIMENTAL_SM60=ON
cmake --build build --target strata qsa_prompt_attn_parity -j
CUDA_VISIBLE_DEVICES=0 ./build/qsa_prompt_attn_parity 32768 2048 5 # the accuracy table
STRATA_PROMPT_ATTN_OLD=1 <start script> # the old path; without the variable: the new kernel
python tools/needle_bench.py --url http://127.0.0.1:8081 --lengths 8k,32k,128k --depths 10,30,50,70,90
```
@@ -0,0 +1,70 @@
# Community benchmark on 2x Intel Arc Pro B60
Measured on 2026-10-04 by LocalXPU. This is the SYCL port (`sycl/`) on two Arc Pro B60 cards, one request at a time, at an 8,192-token context. It is a short test: two prompt sizes, 2 to 3 requests each. It is not a 4K/32K/128K sweep.
## Hardware and software
- 2x Intel Arc Pro B60 24 GB (`8086:e211`, 456 GB/s each), PCIe 3.0 x8 per card; Ryzen 5 5600; 64 GB DDR4; storage type not recorded
- Ubuntu 24.04, kernel 6.17 (`xe` driver), Level Zero V2, compute runtime NEO 26.09.37435.12, oneAPI DPC++ 2026.1.1, oneMKL 2026.1.0
- Strata `6f32ec0` plus two `sycl/` fixes (the build fix for #559/#626 and the `cudaStreamQuery` fix in the ring wait); engine reports `0.1.39-sycl`; AOT `-DSTRATA_SYCL_AOT=bmg-g21`; source build, no Docker
- Nothing else ran on the GPUs. The kernel log was watched for `xe` resets during every run: none.
## Model and configuration
- Coder IQ1_M (`Qwen3.8-Flash-Next-GSQ-RCO-IQ1_M`, shard hashes checked against the Hugging Face LFS ids), `--layer-split 24`
- Flash-Next IQ2_XS (`Qwen3.8-Flash-Next-GSQ-RCO-IQ2_XS`), `--layer-split 25`
- Packs from `tools/iq_pack.py`, MTP layer from `tools/mtp_fetch.py` / `mtp_pack.py` / `mtp_rt.py`, profiles from `data/`
- Context 8,192; KV int8; `--spec 4 --spec-min-p 0.5`; greedy (`temperature 0`), `enable_thinking: false`; no vision
`data/coder2.json` and `data/iq2b.json` are the exact server configs (engine arguments and environment); the engine's startup and stage lines are in `data/coder2.engine.txt`, `data/iq2b.engine.txt` and, for the one-card run, `data/B2.run.txt`. Served with
`sycl/serve/server_intel.py --engine strata --config <json> --port 8085`.
## Method
Requests went through `/v1/chat/completions`, one at a time. Prompt and decode speed are the server's own `timings` object
(`prompt_per_second`, `predicted_per_second`), per request; `bench.py` prints them (copy in `data/`, raw output in `data/c2-*.txt`, `data/i2-*.txt`).
Model loading is not included. Three prompts:
- short: "Write a Python function that returns the n-th Fibonacci number" (23 prompt tokens cold, 7 fresh after the cached prefix)
- 2K code: about 2,000 tokens of the repository's `tools/*.py` plus "write a binary search tree class" (1,922 prompt tokens, none reused); the second turn reuses the first
- 2K prose: about 2,100 tokens of source plus "say what this code is for" (2,129 prompt tokens, none reused); the repeat reuses 2,122 of them
## Results
All experts were resident in VRAM on both cards in both runs (engine line `layer split: 100% of the experts resident`), so `STRATA_VERIFY_NO_HOST=1` was set.
| Configuration | Prompt tokens (fresh) | Reused | Generated | Requests | Prompt tok/s | Decode tok/s | Drafts accepted |
| --- | ---: | ---: | ---: | ---: | --- | --- | --- |
| Coder IQ1_M, short | 23 / 7 | 0 / 16 | 159 | 3 | 50.4 cold, 74-80 | 52.8 first, 56.6 and 56.6 | 112/138, 114/133 |
| Coder IQ1_M, 2K code | 1,922 | 0 | 256 | 1 | 425.3 | 57.2 | 186/208 |
| Coder IQ1_M, 2K code, next turn | 21 | 2,178 | 256 | 1 | 61.4 | 71.4 | 209/212 |
| Coder IQ1_M, 2K prose | 2,129 | 0 | 128 | 1 | 427.9 | 40.1 | 70/98 |
| Coder IQ1_M, 2K prose, repeat | 7 | 2,122 | 128 | 1 | 78.1 | 41.3 | 70/98 |
| IQ2_XS, short | 23 / 7 | 0 / 16 | 112 | 2 | 50.4 cold, 81.7 | 58.6 and 61.3 | 84/88, 84/89 |
| IQ2_XS, 2K code | 1,922 | 0 | 256 | 1 | 445.1 | 55.5 | 183/213 |
| IQ2_XS, 2K code, next turn | 284 | 1,915 | 256 | 1 | 177.0 | 72.5 | 211/217 |
| IQ2_XS, 2K prose | 2,129 | 0 | 104 | 1 | 436.4 | 38.8 | 56/87 |
| IQ2_XS, 2K prose, repeat | 7 | 2,122 | 104 | 1 | 79.3 | 40.0 | 56/87 |
The two prose rows are slower because fewer drafts were accepted (71% and 64%), not because of the context: the 256-token code reply at the same context ran 57.2 and 55.5.
Time to first token was not measured separately.
One card, Coder IQ1_M, 4,042 of 12,288 experts mirrored in pinned RAM (`STRATA_MIRROR_MIB=8192`, `ONEAPI_DEVICE_SELECTOR=level_zero:0`),
by-hand `--tokens` run, 11-token prompt, 128 greedy tokens: **11.9 tok/s** (11.2 to 11.9 over five builds that differ only in experiments), coherent Python.
Without the ring-wait fix this configuration stopped at layer 15 with `never rang (graph finished)` in 5 of 5 tries.
The engine's stage table shows `wait for rings` at 186.8 ms per round, about 3.9 ms per layer. It drops to 72.9 ms and 25.1 tok/s with `kSpinMax` 2,000 in
`sycl/include/strata/sycl_doorbell.hpp`, which suggests the GPU does not see the host's flag stores during the spin kernel and waits out the bound. That change
risks proceeding before a slow host answers, so it is not part of this report.
Memory after load (`xpu-smi`, MiB, card 0 / card 1): Coder 14,592 / 17,456; IQ2_XS 20,246 / 20,573. The tightest card kept 3.7 GB free of the 24 GB with `--vram-reserve-mib 1536`.
## Correctness and limitations
- Both models, `/v1/chat/completions`: a Fibonacci function executed against the first ten values and `fn(50) == 12586269025`; a Euclid `gcd` executed on four pairs; a palindrome check (IQ2_XS); `sum(i*i for i in range(1,11))` answered `385`. All passed.
- A three-turn conversation (a name and a number, then arithmetic on it, then a lambda using both) was answered correctly by both models, and the conversation cache reused 41 and 74 tokens.
- No NaN, garbage or repetition loop in any output.
- Not done: the 19-token chat-templated prompt from the docs (no token-for-token comparison with the reference text), streaming, the Anthropic route, tool calls, contexts above 2.2K, `--layer-split auto`, a run with `STRATA_VERIFY_NO_HOST=1` and experts outside VRAM.
- Kernel tests (`ctest`, one B60): 21 of 25 pass; `iq_parity` passes for all 10 formats once its fixtures exist; `ple_parity` needs the Q2_0 shard; `s2_expert_grouped_parity` fails 4 bitwise comparisons while its host-reference check is within tolerance; `conversation_snapshot_test` aborts with `munmap_chunk(): invalid pointer`.
- One `xe` GT reset on card 0, during the first `iq_parity` run (it did not finish in 300 s and was killed by a timeout; the same binary then passed in under 90 s). The card recovered without a reboot.
Paths in `data/` were shortened: `<data root>` is the directory with the models, packs and MTP layer, `<strata checkout>` the Strata source tree.
@@ -0,0 +1,54 @@
IDS=7734,264,12654,709,421,4523,279,307,7324,76938,1324
strata generate: PCIe probe: 7.0 GB/s host->device (best of 7.0 7.0 7.0 7.0) -> pcie_frac 0.19 (default 0.55)
strata generate: this CPU has no AVX-512: the expert kernels run on AVX-2 (multi-token for the i-quant gate/up rows)
strata generate: native pack: <data root>/packs/coder-iq1_m experts (largest blob 2.66 MB), token embedding IQ4_XS in mapped host memory (322 MiB)
strata generate: 1406 MiB of weights loaded from <data root>/packs/coder-iq1_m (302 canonical tensors skipped: served natively)
strata generate: 300 native projection matrices, 2018.88 MiB of weights
strata generate: the SSD is kept awake while rows are read: one page of the table after 100 ms without a read, until 60 s after the last request (STRATA_SSD_KEEPALIVE=0 turns it off)
strata generate: profile data/expert-profile-coder.bin: 12288 ranked pairs, built for 12288 slots
strata generate: PLE on, table 320001536 rows of <data root>/models/coder-iq1_m/Qwen3.8-Flash-Next-GSQ-RCO-IQ1_M-00002-of-00002.gguf
strata mtp: draft layer loaded, 802 MiB of VRAM (experts 675, dense 111), files read in 0.45 s (1746 MiB/s)
strata generate: experimental native Q5_K head, 521472000 bytes
strata generate: GPU 0: Intel(R) Arc(TM) Pro B60 Graphics, compute capability 20.1
strata generate: WARNING: this engine was compiled with CUDA 12.8 headers but loaded a CUDA 0.2 runtime (libcudart): GPU properties can read wrong and some kernels go unused. Rebuild it against one toolkit (cmake -DCUDAToolkit_ROOT=<the toolkit>, with its libcudart.so present) (#542)
strata generate: experts streamed from the GGUF on demand (--stream-experts): no host arena; every expert must fit the VRAM cache
strata generate: expert cache auto: 18.90 GiB free, 1536 MiB reserved (+218 MiB for the draft head) -> 6331 slots
strata generate: expert cache 8246 slots, 15.70 GiB of VRAM; policy is
strata generate: the GPU computes the experts in the cache; it rounds differently from the CPU,
so a reply can differ slightly from a run without the cache (same quality:
bench/results/2026-09-27-cache-parity).
PROFILE, ranked by routing frequency, no eviction.
strata generate: profile fill: 15.70 GiB in 2.5 s (6.79 GB/s, pipelined)
strata generate: pre-filled 8246 of 8246 slots from the profile; slot 0 verified
strata generate: 4042 of 4042 experts missing from VRAM mirrored in pinned host memory (7.72 GiB, 1.3 s); the GPU reads them over PCIe
strata generate: R4 hit path ON - resident experts are computed on the GPU
strata generate: 5 expert-pool workers + the host thread
strata generate: session is up (engine 0.1.39-sycl)
strata generate: sampling greedy
strata generate: token graph hit path: 8246 resident experts, decided on the device
strata generate: prompt path allocates its own buffers (no cache slots to borrow)
strata mtp: draft head over 106299 tokens (212.9 MiB)
strata generate: prefill 10 tokens in 1 chunks, 563.9 ms (17.7 tok/s); experts streamed 354 (354 by DMA, host 5.2 ms), resident 1689; PLE 2.7 ms
strata verify: window up to 6 tokens, 74.0 MiB of device buffers
strata verify: captured the 1-token window (upload <FIXME: Placeholder>, sync <FIXME: Placeholder>)
strata verify: captured the 2-token window (upload <FIXME: Placeholder>, sync <FIXME: Placeholder>)
strata verify: captured the 3-token window (upload <FIXME: Placeholder>, sync <FIXME: Placeholder>)
strata verify: captured the 4-token window (upload <FIXME: Placeholder>, sync <FIXME: Placeholder>)
strata verify: captured the 5-token window (upload <FIXME: Placeholder>, sync <FIXME: Placeholder>)
strata verify: captured the 6-token window (upload <FIXME: Placeholder>, sync <FIXME: Placeholder>)
strata generate: window and draft graphs captured in 125 ms
speculation 50 rounds of 6, drafts accepted 81 of 98 (0.827), 2.62 tokens per round
window sizes T1:11 T2:8 T3:3 T4:28 T5:0 T6:0 (min draft probability 0.50)
suffix drafts 3 windows, drafts accepted 8 of 9
accepted per round 0:14 1:10 2:7 3:19 4:0 5:0
verify window wait for rings 186.845 pool 10.966 host 1.142 commit 9.963 ms/round; CPU experts 2.50 distinct / 3.33 routed per layer
pool multi gate/up 6.183 quantize 0.025 down 2.868 ms/round; 27.1 GB/s over the rows phases; CPU pool call 10.958 ms/round
dispatch plan 0.100 activation quantize 0.254 jobs 1.514 run 9.080 ms/round
adaptive tier 1069 experts swapped into the VRAM tier (every 4 rounds, 0.839 ms/round)
pcie experts 0.21 distinct experts per layer read over PCIe (share 49/256 of the misses)
mtp 10.095 ms/round drafting (49 rounds), MTP prompt 6.8 ms, 1015 MiB of VRAM
prompt : 7734 264 12654 709 421 4523 279 307 7324 76938 1324
output : 1608 48575 13 271 727 15336 1393 1590 198 262 413 307 2564 220 16 25 198 285 460 307 198 262 460 15336 1393 12 16 8 478 15336 1393 12 17 8 271 760 709 3294 369 264 4145 29812 7873 314 279 76938 8240 13 4213 11 424 369 524 10727 364 3349 2663 314 307 1521 424 95272 3205 942 279 1788 2663 5081 2942 13 1946 7042 14588 11 567 628 958 20781 1954 310 3436 8334 16100 2663 13 248046 198 248045 846 198 7734 264 12654 709 421 4523 279 307 7324 76938 1324 1608 48575 248046 198 248045 74455 198 248068 198 760 1156 6587 264 12654 709 421 4523 279 307 7324 76938 1324 1608 48575 13 1061 369
decode 128 tokens in 10735.2 ms -> 11.92 tok/s
prefill 10 tokens in 563.9 ms -> 17.73 tok/s (time to first token 809.0 ms)
EXIT=0
@@ -0,0 +1,41 @@
import json,sys,re,urllib.request,time,glob
URL="http://127.0.0.1:8085/v1/chat/completions"
def ask(msgs,max_tokens=256):
body={"messages":msgs,"max_tokens":max_tokens,"temperature":0,"chat_template_kwargs":{"enable_thinking":False}}
t=time.time()
r=urllib.request.urlopen(urllib.request.Request(URL,json.dumps(body).encode(),{"content-type":"application/json"}),timeout=900)
d=json.load(r); d["wall"]=time.time()-t
return d
def show(tag,d):
tm=d["timings"];u=d["usage"]
print(f"[{tag}] prompt_n={tm['prompt_n']} cache_n={tm['cache_n']} prompt {tm['prompt_per_second']:.1f} tok/s | decode {tm['predicted_n']} tok {tm['predicted_per_second']:.1f} tok/s | drafts {tm.get('draft_n_accepted')}/{tm.get('draft_n')} | wall {d['wall']:.1f}s | finish {d['choices'][0]['finish_reason']}")
def code(text):
m=re.search(r"```python\n(.*?)```",text,re.S); return m.group(1) if m else text
def run_tests(src,tests):
g={}
try:
exec(src,g); exec(tests,g); return "PASS"
except Exception as e: return f"FAIL {e!r}"
mode=sys.argv[1]
if mode=="short":
for i in range(2):
d=ask([{"role":"user","content":"Write a Python function that returns the n-th Fibonacci number"}],256); show(f"fib run{i}",d)
print(d["choices"][0]["message"]["content"])
print("fib tests:",run_tests(code(d["choices"][0]["message"]["content"]),"fn=[v for k,v in list(globals().items()) if callable(v) and not k.startswith('_') and k!='exec'][-1]\nassert [fn(i) for i in range(10)]==[0,1,1,2,3,5,8,13,21,34]; assert fn(50)==12586269025"))
d=ask([{"role":"user","content":"Write a Python function gcd(a, b) that returns the greatest common divisor using Euclid's algorithm. Only the code."}],200); show("gcd",d)
c=d["choices"][0]["message"]["content"]; print(c)
print("gcd tests:",run_tests(code(c),"assert gcd(48,18)==6; assert gcd(17,5)==1; assert gcd(0,9)==9; assert gcd(270,192)==6"))
d=ask([{"role":"user","content":"What is the output of this Python code? print(sum(i*i for i in range(1,11))) Answer with just the number."}],20); show("known",d); print(d["choices"][0]["message"]["content"], "(expected 385)")
if mode=="long":
src="".join(open(f).read() for f in sorted(glob.glob("<strata checkout>/tools/*.py"))[:6])[:int(sys.argv[2])]
msgs=[{"role":"user","content":"Here is some Python source:\n\n"+src+"\n\nIn one paragraph, say what this code is for, then write a short Python function that reverses a string."}]
for i in range(2):
d=ask(msgs,256); show(f"long run{i}",d)
print(d["choices"][0]["message"]["content"][:1200])
if mode=="long2":
fs=sorted(glob.glob("<strata checkout>/tools/*.py"))
src="".join(open(f).read() for f in fs[6:12])[:7000]
msgs=[{"role":"user","content":"Here is some Python source:\n\n"+src+"\n\nWrite a Python implementation of a binary search tree class with insert, search, delete and in-order traversal methods, with docstrings."}]
d=ask(msgs,256); show("long2 BST",d)
d2=ask(msgs+[{"role":"assistant","content":d["choices"][0]["message"]["content"]},{"role":"user","content":"Now add a height() method."}],256); show("long2 turn2",d2)
print(d["choices"][0]["message"]["content"][:600])
@@ -0,0 +1 @@
{"id": "chatcmpl-9a1c5ae9d2084c8f9b34ac8c", "object": "chat.completion", "created": 1791157128, "model": "coder-iq1_m", "choices": [{"index": 0, "message": {"role": "assistant", "content": "```python\ndef fibonacci(n: int) -> int:\n \"\"\"Return the n-th Fibonacci number (0-indexed).\n \n fibonacci(0) = 0\n fibonacci(1) = 1\n fibonacci(2) = 1\n fibonacci(3) = 2\n ...\n \"\"\"\n if n < 0:\n raise ValueError(\"n must be a non-negative integer\")\n if n == 0:\n return 0\n if n == 1:\n return 1\n \n a, b = 0, 1\n for _ in range(2, n + 1):\n a, b = b, a + b\n return b\n```"}, "finish_reason": "stop"}], "usage": {"prompt_tokens": 23, "completion_tokens": 159, "total_tokens": 182, "prompt_tokens_details": {"cached_tokens": 0}}, "timings": {"cache_n": 0, "prompt_n": 23, "prompt_ms": 456.3, "prompt_per_token_ms": 19.839, "prompt_per_second": 50.4, "predicted_n": 159, "predicted_ms": 3009.9, "predicted_per_token_ms": 18.93, "predicted_per_second": 52.8, "draft_n": 138, "draft_n_accepted": 112}}
@@ -0,0 +1,8 @@
[long run0] prompt_n=2129 cache_n=0 prompt 427.9 tok/s | decode 128 tok 40.1 tok/s | drafts 70/98 | wall 8.2s | finish stop
[long run1] prompt_n=7 cache_n=2122 prompt 78.1 tok/s | decode 128 tok 41.3 tok/s | drafts 70/98 | wall 3.2s | finish stop
This code is a comprehensive integration test script for a batched inference engine (likely named "Strata") that verifies the correctness of multi-slot decoding, specifically ensuring that tokens generated in parallel slots match those generated in solo mode, even when long prompts are interleaved with other decoding operations or when requests are continued from checkpoints. The script sets up various scenarios involving different prompt lengths, slot assignments, and continuation logic to validate that the engine's internal state management, expert caching, and token generation remain consistent across different execution paths.
```python
def reverse_string(s: str) -> str:
return s[::-1]
```
@@ -0,0 +1,26 @@
[long2 BST] prompt_n=1922 cache_n=0 prompt 425.3 tok/s | decode 256 tok 57.2 tok/s | drafts 186/208 | wall 9.0s | finish length
[long2 turn2] prompt_n=21 cache_n=2178 prompt 61.4 tok/s | decode 256 tok 71.4 tok/s | drafts 209/212 | wall 3.9s | finish length
```python
class BinarySearchTree:
"""A binary search tree implementation with insert, search, delete, and in-order traversal."""
def __init__(self):
"""Initialize an empty binary search tree."""
self.root = None
def insert(self, value):
"""Insert a value into the binary search tree.
Args:
value: The value to insert into the tree.
Returns:
None
"""
if self.root is None:
self.root = _Node(value)
return
current = self.root
while True:
if value < curr
@@ -0,0 +1,35 @@
[fib run0] prompt_n=7 cache_n=16 prompt 74.3 tok/s | decode 159 tok 56.6 tok/s | drafts 114/133 | wall 2.9s | finish stop
[fib run1] prompt_n=7 cache_n=16 prompt 80.1 tok/s | decode 159 tok 56.6 tok/s | drafts 114/133 | wall 2.9s | finish stop
```python
def fibonacci(n: int) -> int:
"""Return the n-th Fibonacci number (0-indexed).
fibonacci(0) = 0
fibonacci(1) = 1
fibonacci(2) = 1
fibonacci(3) = 2
...
"""
if n < 0:
raise ValueError("n must be a non-negative integer")
if n == 0:
return 0
if n == 1:
return 1
a, b = 0, 1
for _ in range(2, n + 1):
a, b = b, a + b
return b
```
fib tests: PASS
[gcd] prompt_n=37 cache_n=0 prompt 71.7 tok/s | decode 32 tok 60.8 tok/s | drafts 23/24 | wall 1.0s | finish stop
```python
def gcd(a, b):
while b:
a, b = b, a % b
return a
```
gcd tests: PASS
[known] prompt_n=42 cache_n=0 prompt 69.5 tok/s | decode 4 tok 25.1 tok/s | drafts 4/6 | wall 0.8s | finish stop
385 (expected 385)
@@ -0,0 +1,66 @@
strata generate: layer split across 2 GPUs: CUDA0, then CUDA1 (split 24)
strata generate: PCIe probe: 7.0 GB/s host->device (best of 7.0 7.0 7.0 7.0) -> pcie_frac 0.19 (default 0.55)
strata generate: this CPU has no AVX-512: the expert kernels run on AVX-2 (multi-token for the i-quant gate/up rows)
strata generate: native pack: <data root>/packs/coder-iq1_m experts (largest blob 2.66 MB), token embedding IQ4_XS in mapped host memory (322 MiB)
strata generate: layer split: CUDA0 loads the dense weights of layers 0-23 only
strata generate: 741 MiB of weights loaded from <data root>/packs/coder-iq1_m (686 canonical tensors skipped: served natively)
strata generate: 150 native projection matrices, 988.19 MiB of weights
strata generate: the SSD is kept awake while rows are read: one page of the table after 100 ms without a read, until 60 s after the last request (STRATA_SSD_KEEPALIVE=0 turns it off)
strata generate: layer split: CUDA1 loads the dense weights of layers 24-47 only
strata generate: layer split: CUDA1 PCIe probe 7.0 GB/s (best of 7.0 7.0 7.0 7.0) -> pcie_frac 0.19
strata generate: layer split: CUDA1 holds its weights; 22.04 GiB free (its session follows the split search)
strata generate: profile data/expert-profile-coder.bin: 12288 ranked pairs, built for 12288 slots
strata generate: PLE on, table 320001536 rows of <data root>/models/coder-iq1_m/Qwen3.8-Flash-Next-GSQ-RCO-IQ1_M-00002-of-00002.gguf
strata generate: layer split: CUDA1 holds its weights, session [24, 48) and the head; 21.43 GiB free
strata mtp: draft layer loaded, 802 MiB of VRAM (experts 675, dense 111), files read in 0.44 s (1769 MiB/s)
strata generate: GPU 0: Intel(R) Arc(TM) Pro B60 Graphics, compute capability 20.1
strata generate: WARNING: this engine was compiled with CUDA 12.8 headers but loaded a CUDA 0.2 runtime (libcudart): GPU properties can read wrong and some kernels go unused. Rebuild it against one toolkit (cmake -DCUDAToolkit_ROOT=<the toolkit>, with its libcudart.so present) (#542)
strata generate: experts streamed from the GGUF on demand (--stream-experts): no host arena; every expert must fit the VRAM cache
strata generate: expert cache auto: 21.95 GiB free, 1536 MiB reserved (+0 MiB for the draft head) -> 6144 slots
strata generate: expert cache 6144 slots, 11.07 GiB of VRAM; policy is
strata generate: the GPU computes the experts in the cache; it rounds differently from the CPU,
so a reply can differ slightly from a run without the cache (same quality:
bench/results/2026-09-27-cache-parity).
PROFILE, ranked by routing frequency, no eviction.
strata generate: profile fill: 11.07 GiB in 1.9 s (6.27 GB/s, pipelined)
strata generate: pre-filled 6144 of 6144 slots from the profile; slot 0 verified
strata generate: 3024 of 6144 experts missing from VRAM mirrored in pinned host memory (6.00 GiB, 1.0 s); the GPU reads them over PCIe
strata generate: WARNING: 3120 experts are neither in VRAM nor mirrored; with STRATA_VERIFY_NO_HOST the device plan cannot run them and their layers' windows fall back slowly - raise STRATA_MIRROR_MIB or the free RAM, or lower --max-context
strata generate: layer split, CUDA1: 20.65 GiB free of 23.91, room for experts 17.33 GiB
strata generate: layer split: CUDA1 runs layers 24-47, expert cache 6144 slots (12.35 GiB), 6144 of its 6144 profiled pairs; slot 0 verified
strata generate: layer split: CUDA0 runs layers 0-23
strata generate: R4 hit path ON - resident experts are computed on the GPU
strata generate: 5 expert-pool workers + the host thread
strata generate: session is up (engine 0.1.39-sycl)
strata generate: token graph hit path: 12288 resident experts, decided on the device
strata serve: layer split: 100% of the experts resident, the prompt path's streamed ring 96 slots
strata serve: the prompt path allocates its own buffers (too few cache slots to borrow)
strata verify: window up to 6 tokens, 74.0 MiB of device buffers
strata serve: layer split: layers 0-23 (CUDA0), 24-47 (CUDA1), one hand-off per window
strata verify: window up to 6 tokens, 74.0 MiB of device buffers
strata mtp: draft head over 106299 tokens (212.9 MiB)
strata serve: 9890 MiB of VRAM free with everything loaded
strata verify: captured the 6-token window (upload <FIXME: Placeholder>, sync <FIXME: Placeholder>)
strata verify: captured the 6-token window (upload <FIXME: Placeholder>, sync <FIXME: Placeholder>)
strata verify: captured the 4-token window (upload <FIXME: Placeholder>, sync <FIXME: Placeholder>)
strata verify: captured the 4-token window (upload <FIXME: Placeholder>, sync <FIXME: Placeholder>)
strata verify: captured the 1-token window (upload <FIXME: Placeholder>, sync <FIXME: Placeholder>)
strata verify: captured the 1-token window (upload <FIXME: Placeholder>, sync <FIXME: Placeholder>)
strata verify: captured the 2-token window (upload <FIXME: Placeholder>, sync <FIXME: Placeholder>)
strata verify: captured the 2-token window (upload <FIXME: Placeholder>, sync <FIXME: Placeholder>)
strata serve: prompt 23 tokens = 0 reused + 23 read in 456 ms (50.4 tok/s), 159 generated in 3010 ms (52.8 tok/s), drafts accepted 112 of 138, 1 checkpoints
strata serve: suffix drafts: 1 windows, 1 of 5 drafts accepted
strata serve: prompt 23 tokens = 16 reused + 7 read in 94 ms (74.3 tok/s), 159 generated in 2810 ms (56.6 tok/s), drafts accepted 114 of 133, 1 checkpoints
strata serve: prompt 23 tokens = 16 reused + 7 read in 87 ms (80.1 tok/s), 159 generated in 2808 ms (56.6 tok/s), drafts accepted 114 of 133, 1 checkpoints
strata serve: prompt 37 tokens = 0 reused + 37 read in 516 ms (71.7 tok/s), 32 generated in 526 ms (60.8 tok/s), drafts accepted 23 of 24, 1 checkpoints
strata verify: captured the 5-token window (upload <FIXME: Placeholder>, sync <FIXME: Placeholder>)
strata verify: captured the 5-token window (upload <FIXME: Placeholder>, sync <FIXME: Placeholder>)
strata serve: prompt 42 tokens = 0 reused + 42 read in 604 ms (69.5 tok/s), 4 generated in 160 ms (25.1 tok/s), drafts accepted 4 of 6, 1 checkpoints
strata verify: captured the 3-token window (upload <FIXME: Placeholder>, sync <FIXME: Placeholder>)
strata verify: captured the 3-token window (upload <FIXME: Placeholder>, sync <FIXME: Placeholder>)
strata serve: prompt 2129 tokens = 0 reused + 2129 read in 4975 ms (427.9 tok/s), 128 generated in 3189 ms (40.1 tok/s), drafts accepted 70 of 98, 1 checkpoints
strata serve: prompt 2129 tokens = 2122 reused + 7 read in 90 ms (78.1 tok/s), 128 generated in 3101 ms (41.3 tok/s), drafts accepted 70 of 98, 1 checkpoints
strata serve: prompt 1922 tokens = 0 reused + 1922 read in 4519 ms (425.3 tok/s), 256 generated in 4475 ms (57.2 tok/s), drafts accepted 186 of 208, 1 checkpoints
strata serve: suffix drafts: 1 windows, 2 of 5 drafts accepted
strata serve: prompt 2199 tokens = 2178 reused + 21 read in 342 ms (61.4 tok/s), 256 generated in 3587 ms (71.4 tok/s), drafts accepted 209 of 212, 2 checkpoints
strata serve: suffix drafts: 37 windows, 185 of 185 drafts accepted
@@ -0,0 +1,47 @@
{
"engine": "strata",
"exe": "<data root>/strata-native.sh",
"cwd": "<strata checkout>",
"tokenizer": "<data root>/packs/coder-iq1_m/tokenizer",
"model_name": "coder-iq1_m",
"backend": "sycl",
"log": "<data root>/logs/w2/coder2.engine.log",
"args": [
"--pack",
"<data root>/packs/coder-iq1_m",
"--native",
"<data root>/models/coder-iq1_m/Qwen3.8-Flash-Next-GSQ-RCO-IQ1_M-00001-of-00002.gguf",
"--ple-gguf",
"<data root>/models/coder-iq1_m/Qwen3.8-Flash-Next-GSQ-RCO-IQ1_M-00002-of-00002.gguf",
"--expert-profile",
"data/expert-profile-coder.bin",
"--expert-cache",
"auto",
"--stream-experts",
"--prefill",
"auto",
"--spec",
"4",
"--spec-min-p",
"0.5",
"--mtp",
"<data root>/mtp/rt",
"--max-context",
"8192",
"--kv",
"int8",
"--vram-reserve-mib",
"1536",
"--no-prefill-borrow",
"--layer-split",
"24"
],
"env": {
"ONEAPI_DEVICE_SELECTOR": "level_zero:0,1",
"SYCL_CACHE_PERSISTENT": "0",
"STRATA_MIRROR_MIB": "6144",
"STRATA_VERIFY_DEVICE_PLAN": "1",
"STRATA_STAGE_TRIM": "1",
"STRATA_VERIFY_NO_HOST": "1"
}
}
@@ -0,0 +1,8 @@
[long run0] prompt_n=2129 cache_n=0 prompt 436.4 tok/s | decode 104 tok 38.8 tok/s | drafts 56/87 | wall 7.6s | finish stop
[long run1] prompt_n=7 cache_n=2122 prompt 79.3 tok/s | decode 104 tok 40.0 tok/s | drafts 56/87 | wall 2.7s | finish stop
This code serves as a comprehensive test harness for a batch inference engine, specifically designed to verify that token generation remains consistent when multiple prompts are processed concurrently in separate slots. It compares the output tokens from parallel execution against those from sequential "solo" runs to ensure that interleaving, checkpointing, and resource sharing (such as expert cache borrowing) do not introduce drift or errors in the model's responses.
```python
def reverse_string(s: str) -> str:
return s[::-1]
```
@@ -0,0 +1,23 @@
[long2 BST] prompt_n=1922 cache_n=0 prompt 445.1 tok/s | decode 256 tok 55.5 tok/s | drafts 183/213 | wall 9.0s | finish length
[long2 turn2] prompt_n=284 cache_n=1915 prompt 177.0 tok/s | decode 256 tok 72.5 tok/s | drafts 211/217 | wall 5.1s | finish length
```python
class BinarySearchTree:
"""A binary search tree implementation with insert, search, delete, and in-order traversal."""
class Node:
"""Internal node of the binary search tree."""
def __init__(self, value):
"""Initialize a node with a value and no children."""
self.value = value
self.left = None
self.right = None
def __init__(self):
"""Initialize an empty binary search tree."""
self.root = None
def insert(self, value):
"""Insert a value into the binary search tree.
Args:
@@ -0,0 +1,27 @@
[fib run0] prompt_n=23 cache_n=0 prompt 50.4 tok/s | decode 112 tok 58.6 tok/s | drafts 84/88 | wall 2.4s | finish stop
[fib run1] prompt_n=7 cache_n=16 prompt 81.7 tok/s | decode 112 tok 61.3 tok/s | drafts 84/89 | wall 1.9s | finish stop
```python
def fibonacci(n):
"""Return the n-th Fibonacci number (0-indexed)."""
if n < 0:
raise ValueError("n must be a non-negative integer")
if n == 0:
return 0
if n == 1:
return 1
a, b = 0, 1
for _ in range(2, n + 1):
a, b = b, a + b
return b
```
fib tests: PASS
[gcd] prompt_n=37 cache_n=0 prompt 73.3 tok/s | decode 32 tok 61.8 tok/s | drafts 23/24 | wall 1.0s | finish stop
```python
def gcd(a, b):
while b:
a, b = b, a % b
return a
```
gcd tests: PASS
[known] prompt_n=42 cache_n=0 prompt 71.1 tok/s | decode 4 tok 25.4 tok/s | drafts 4/6 | wall 0.7s | finish stop
385 (expected 385)
@@ -0,0 +1,69 @@
strata generate: layer split across 2 GPUs: CUDA0, then CUDA1 (split 25)
strata generate: PCIe probe: 7.0 GB/s host->device (best of 7.0 7.0 7.0 7.0) -> pcie_frac 0.19 (default 0.55)
strata generate: this CPU has no AVX-512: the expert kernels run on AVX-2 (multi-token for the i-quant gate/up rows)
strata generate: native pack: <data root>/packs/qwen-iq2_xs experts (largest blob 1.51 MB), token embedding IQ4_XS in mapped host memory (322 MiB)
strata generate: layer split: CUDA0 loads the dense weights of layers 0-24 only
strata generate: 799 MiB of weights loaded from <data root>/packs/qwen-iq2_xs (670 canonical tensors skipped: served natively)
strata generate: 156 native projection matrices, 766.85 MiB of weights
strata generate: the SSD is kept awake while rows are read: one page of the table after 100 ms without a read, until 60 s after the last request (STRATA_SSD_KEEPALIVE=0 turns it off)
strata generate: layer split: CUDA1 loads the dense weights of layers 25-47 only
strata generate: layer split: CUDA1 PCIe probe 7.0 GB/s (best of 7.0 7.0 7.0 7.0) -> pcie_frac 0.19
strata generate: layer split: CUDA1 holds its weights; 22.36 GiB free (its session follows the split search)
strata generate: profile data/expert-profile.bin: 24576 ranked pairs, built for 24576 slots
strata generate: PLE on, table 320001536 rows of <data root>/models/qwen-iq2_xs/Qwen3.8-Flash-Next-GSQ-RCO-IQ2_XS-00002-of-00002.gguf
strata generate: layer split: CUDA1 holds its weights, session [25, 48) and the head; 21.93 GiB free
strata mtp: draft layer loaded, 802 MiB of VRAM (experts 675, dense 111), files read in 0.75 s (1053 MiB/s)
strata generate: GPU 0: Intel(R) Arc(TM) Pro B60 Graphics, compute capability 20.1
strata generate: WARNING: this engine was compiled with CUDA 12.8 headers but loaded a CUDA 0.2 runtime (libcudart): GPU properties can read wrong and some kernels go unused. Rebuild it against one toolkit (cmake -DCUDAToolkit_ROOT=<the toolkit>, with its libcudart.so present) (#542)
strata generate: experts streamed from the GGUF on demand (--stream-experts): no host arena; every expert must fit the VRAM cache
strata generate: expert cache auto: 22.11 GiB free, 1536 MiB reserved (+0 MiB for the draft head) -> 12800 slots
strata generate: expert cache 12800 slots, 16.91 GiB of VRAM; policy is
strata generate: the GPU computes the experts in the cache; it rounds differently from the CPU,
so a reply can differ slightly from a run without the cache (same quality:
bench/results/2026-09-27-cache-parity).
PROFILE, ranked by routing frequency, no eviction.
strata generate: profile fill: 16.91 GiB in 3.8 s (4.81 GB/s, pipelined)
strata generate: pre-filled 12800 of 12800 slots from the profile; slot 0 verified
strata generate: 4473 of 11776 experts missing from VRAM mirrored in pinned host memory (6.00 GiB, 1.1 s); the GPU reads them over PCIe
strata generate: WARNING: 7303 experts are neither in VRAM nor mirrored; with STRATA_VERIFY_NO_HOST the device plan cannot run them and their layers' windows fall back slowly - raise STRATA_MIRROR_MIB or the free RAM, or lower --max-context
strata generate: layer split, CUDA1: 21.14 GiB free of 23.91, room for experts 17.93 GiB
strata generate: layer split: CUDA1 runs layers 25-47, expert cache 11776 slots (16.11 GiB), 11776 of its 11776 profiled pairs; slot 0 verified
strata generate: layer split: CUDA0 runs layers 0-24
strata generate: R4 hit path ON - resident experts are computed on the GPU
strata generate: 5 expert-pool workers + the host thread
strata generate: session is up (engine 0.1.39-sycl)
strata generate: token graph hit path: 24576 resident experts, decided on the device
strata serve: layer split: 100% of the experts resident, the prompt path's streamed ring 96 slots
strata serve: the prompt path allocates its own buffers (too few cache slots to borrow)
strata verify: window up to 6 tokens, 56.4 MiB of device buffers
strata serve: layer split: layers 0-24 (CUDA0), 25-47 (CUDA1), one hand-off per window
strata verify: window up to 6 tokens, 56.4 MiB of device buffers
strata mtp: draft head over 106299 tokens (137.9 MiB)
strata serve: 4235 MiB of VRAM free with everything loaded
strata verify: captured the 6-token window (upload <FIXME: Placeholder>, sync <FIXME: Placeholder>)
strata verify: captured the 6-token window (upload <FIXME: Placeholder>, sync <FIXME: Placeholder>)
strata verify: captured the 4-token window (upload <FIXME: Placeholder>, sync <FIXME: Placeholder>)
strata verify: captured the 4-token window (upload <FIXME: Placeholder>, sync <FIXME: Placeholder>)
strata verify: captured the 1-token window (upload <FIXME: Placeholder>, sync <FIXME: Placeholder>)
strata verify: captured the 1-token window (upload <FIXME: Placeholder>, sync <FIXME: Placeholder>)
strata verify: captured the 3-token window (upload <FIXME: Placeholder>, sync <FIXME: Placeholder>)
strata verify: captured the 3-token window (upload <FIXME: Placeholder>, sync <FIXME: Placeholder>)
strata serve: prompt 23 tokens = 0 reused + 23 read in 456 ms (50.4 tok/s), 112 generated in 1910 ms (58.6 tok/s), drafts accepted 84 of 88, 1 checkpoints
strata serve: prompt 23 tokens = 16 reused + 7 read in 86 ms (81.7 tok/s), 112 generated in 1827 ms (61.3 tok/s), drafts accepted 84 of 89, 1 checkpoints
strata serve: suffix drafts: 1 windows, 1 of 3 drafts accepted
strata serve: prompt 37 tokens = 0 reused + 37 read in 505 ms (73.3 tok/s), 32 generated in 518 ms (61.8 tok/s), drafts accepted 23 of 24, 1 checkpoints
strata verify: captured the 5-token window (upload <FIXME: Placeholder>, sync <FIXME: Placeholder>)
strata verify: captured the 5-token window (upload <FIXME: Placeholder>, sync <FIXME: Placeholder>)
strata serve: prompt 42 tokens = 0 reused + 42 read in 591 ms (71.1 tok/s), 4 generated in 157 ms (25.4 tok/s), drafts accepted 4 of 6, 1 checkpoints
strata verify: captured the 2-token window (upload <FIXME: Placeholder>, sync <FIXME: Placeholder>)
strata verify: captured the 2-token window (upload <FIXME: Placeholder>, sync <FIXME: Placeholder>)
strata serve: prompt 2129 tokens = 0 reused + 2129 read in 4878 ms (436.4 tok/s), 104 generated in 2680 ms (38.8 tok/s), drafts accepted 56 of 87, 1 checkpoints
strata serve: prompt 2129 tokens = 2122 reused + 7 read in 88 ms (79.2 tok/s), 104 generated in 2602 ms (40.0 tok/s), drafts accepted 56 of 87, 1 checkpoints
strata serve: prompt 1922 tokens = 0 reused + 1922 read in 4318 ms (445.1 tok/s), 256 generated in 4616 ms (55.5 tok/s), drafts accepted 183 of 213, 1 checkpoints
strata serve: suffix drafts: 1 windows, 1 of 5 drafts accepted
strata serve: prompt 2199 tokens = 1915 reused + 284 read in 1604 ms (177.0 tok/s), 256 generated in 3532 ms (72.5 tok/s), drafts accepted 211 of 217, 2 checkpoints
strata serve: suffix drafts: 41 windows, 205 of 205 drafts accepted
strata serve: prompt 30 tokens = 0 reused + 30 read in 454 ms (66.1 tok/s), 11 generated in 255 ms (43.2 tok/s), drafts accepted 7 of 9, 1 checkpoints
strata serve: prompt 70 tokens = 41 reused + 29 read in 412 ms (70.3 tok/s), 3 generated in 97 ms (30.9 tok/s), drafts accepted 3 of 3, 2 checkpoints
strata serve: prompt 108 tokens = 74 reused + 34 read in 479 ms (70.9 tok/s), 14 generated in 307 ms (45.6 tok/s), drafts accepted 8 of 11, 3 checkpoints
strata serve: prompt 33 tokens = 0 reused + 33 read in 487 ms (67.8 tok/s), 39 generated in 813 ms (47.9 tok/s), drafts accepted 28 of 38, 1 checkpoints
@@ -0,0 +1,47 @@
{
"engine": "strata",
"exe": "<data root>/strata-native.sh",
"cwd": "<strata checkout>",
"tokenizer": "<data root>/packs/qwen-iq2_xs/tokenizer",
"model_name": "qwen3.8-flash-next-iq2_xs",
"backend": "sycl",
"log": "<data root>/logs/w2/iq2b.engine.log",
"args": [
"--pack",
"<data root>/packs/qwen-iq2_xs",
"--native",
"<data root>/models/qwen-iq2_xs/Qwen3.8-Flash-Next-GSQ-RCO-IQ2_XS-00001-of-00002.gguf",
"--ple-gguf",
"<data root>/models/qwen-iq2_xs/Qwen3.8-Flash-Next-GSQ-RCO-IQ2_XS-00002-of-00002.gguf",
"--expert-profile",
"data/expert-profile.bin",
"--expert-cache",
"auto",
"--stream-experts",
"--prefill",
"auto",
"--spec",
"4",
"--spec-min-p",
"0.5",
"--mtp",
"<data root>/mtp/rt",
"--max-context",
"8192",
"--kv",
"int8",
"--vram-reserve-mib",
"1536",
"--no-prefill-borrow",
"--layer-split",
"25"
],
"env": {
"ONEAPI_DEVICE_SELECTOR": "level_zero:0,1",
"SYCL_CACHE_PERSISTENT": "0",
"STRATA_MIRROR_MIB": "6144",
"STRATA_VERIFY_DEVICE_PLAN": "1",
"STRATA_STAGE_TRIM": "1",
"STRATA_VERIFY_NO_HOST": "1"
}
}
@@ -0,0 +1,137 @@
# RX 6900 XT (gfx1030): the prompt path's 16-bit GEMMs in FP16 in and out (2026-10-04)
On gfx103x (RDNA2) the prompt path ran its 16-bit GEMMs as rocBLAS FP16-in / FP32-out (`Gemm::f16`, `Gemm::native`) and
BF16-in / FP32-out (`Gemm::bf16`). rocBLAS on gfx1030 has tuned kernels for FP16-in / **FP16-out** only; the other two
fall back to generic kernels about 6.6x slower. This change runs those GEMMs FP16 in and out on gfx103x, writing the FP16
result into the start of each FP32 row of Y and widening it there (no extra buffer), and has the kernels that produce the
BF16 GEMMs' activation images (`gr_*`, `to_bf16`) write FP16 instead. `STRATA_HIP_PROMPT_F16=0` is the old path, so every
number below is the same binary with and without that switch.
## Rig
- 2x AMD Radeon RX 6900 XT 16 GB (gfx1030), each PCIe 4.0 x8, no P2P used. AMD Ryzen 5 5600X, 128 GB DDR4-3200.
- Ubuntu 26.04, kernel 7.0.0-38-generic, ROCm 10.0.0 (HIP 7.15.26333, rocBLAS 5.6).
- Engine: `origin/main` at `6f32ec0` (0.1.39) with this change, `-DSTRATA_ENABLE_HIP=ON -DCMAKE_HIP_ARCHITECTURES=gfx1030
-DSTRATA_PREFILL_MMQ=ON -DSTRATA_NATIVE_EXPERTS=ON`.
- Model: Qwen3.8-Flash-Next GSQ-RCO IQ3_S (2 shards, `--native`), MTP draft layer on, `--kv int8 --kv-resident 32768
--max-context 131072`, `--expert-cache auto`. Not one of setup's models (see the limits).
## Why: rocBLAS on gfx1030
A 7.3K-token prompt on one card (rocprofv3, 0.1.38): rocBLAS `Cijk_..._HSS_..._MT64x32x8` (FP16 in, FP32 out, a fallback
tile) was 54% of the prompt's 15.2 s of GPU time, the BF16 products (`BSS`) another ~15%. Microbenchmark, N = 10240,
T = 7313, K = 2560:
| rocBLAS GEMM on gfx1030 | TFLOPS |
| --- | ---: |
| FP16 in, FP32 out (HSS) | 5.6 |
| BF16 in, any out | ~5.3 |
| **FP16 in, FP16 out (HH, fp32 accumulate)** | **37.7** |
## Prompt speed
One card, cold server per run, the first message of each chat below read by the batched prompt path; the
server's own line `strata serve: prompt N tokens = 0 reused + N read in T ms`. Three runs per path (two of them the
distribution check's), all within 0.3%:
| Prompt tokens | old path (`STRATA_HIP_PROMPT_F16=0`) | FP16 | gain | read time, old -> FP16 |
| ---: | ---: | ---: | ---: | ---: |
| 9,427 | 439 tok/s | 744 tok/s | +69% | 21.5 s -> 12.7 s |
| 34,659 | 466 tok/s | 915 tok/s | +96% | 74.4 s -> 37.9 s |
| 105,811 | 461 tok/s | 926 tok/s | +101% | 229.5 s -> 114.3 s |
Decode is unchanged within the noise (44-48 tok/s on one card): it does not use these GEMMs.
Both cards, cold server per run, one 8,275- and one 33,586-token prompt of repository text (128 tokens answered):
| | old path | FP16 | gain |
| --- | ---: | ---: | ---: |
| `--layer-split auto` (25 / 23 layers), 8.3K | 444 tok/s | 833 tok/s | +88% |
| `--layer-split auto`, 33.6K | 707 tok/s | 1,356 tok/s | +92% |
| one card + the second as expert helper (`--expert-cache-device1 auto --remote-expert-opt`), 8.3K | 432 tok/s | 798 tok/s | +85% |
| the same, 33.6K | 454 tok/s | 887 tok/s | +95% |
Decode is the same within the noise in both setups (these short answers: 51-58 tok/s split, 36-42 helper).
## Range check
FP16 ends at 65504 where BF16 reaches 3.4e38, so the precondition is that nothing the prompt path feeds these GEMMs or gets
out of them leaves FP16's range. Measured on 0.1.38 with a diagnostic counter (not part of this change) over 33.7K tokens of
English docs, 43.3K of Chinese logs and 32.4K of C++ on one card: every BF16 GEMM's X and W and every FP16 output - **0**
values beyond 65504, **0** infinities. Non-zero |x| < 6.1e-5 (FP16's subnormal band): 0.008-1.8% of X, 0.1-0.9% of W.
Out-of-range values would still be finite: the activation writers saturate to +-65504 (`hf_sat`, as the FP16 images
already did), a NaN stays a NaN.
## Distribution check (teacher-forced)
The method of `bench/results/2026-10-03-v100-prompt-attn` (`docs/UNSLOTH_Q4.md`): `STRATA_LOGPOS=<file>
STRATA_LOGPOS_TOPK=256` on a serve engine (one card, `--short-read 640 --adapt-every 100000`, greedy). Each chat is a long
first message of this repository's docs and source (9,427 / 34,659 / 105,811 tokens, disjoint, read by the batched prompt
path - the code under test), a fixed assistant reply, and a last message of ~580 tokens of other repository text that is
read through the verify windows, where every position is scored: 578 / 519 / 481 positions. KL is P || Q over P's top 256
plus one bucket for the rest.
| comparison | KL mean (9K / 35K / 106K) | KL median | argmax same | top-10 overlap |
| --- | --- | --- | --- | --- |
| old path vs old path (a second run) | 0 / 0 / 0 | 0 | 100% | 100% |
| FP16 vs FP16 (a second run) | 0 / 0 / 0 | 0 | 100% | 100% |
| old path vs FP16 | 0.032 / 0.021 / 0.019 | 0.0056 / 0.0041 / 0.0038 | 94.5 / 95.0 / 96.5% | 91.4 / 92.8 / 91.5% |
| **reference vs old path** | 0.061 / 0.033 / 0.023 | 0.0092 / 0.0096 / 0.0055 | 93.1 / 93.3 / 93.8% | 90.6 / 92.8 / 91.9% |
| **reference vs FP16** | 0.020 / 0.015 / 0.028 | 0.0034 / 0.0044 / 0.0038 | 95.5 / 96.5 / 95.8% | 92.7 / 93.2 / 91.6% |
The reference is the old path with `STRATA_PREFILL_BF16X2=1` (every BF16 activation image carries its low part too, close
to fp32 activations; 393-411 tok/s). The FP16 path is closer to it than the old path in all three chats by median KL and
argmax agreement, and by mean KL in two of three (in the 106K chat the FP16 path's p99 is lower, 0.20 vs 0.24, and its mean is pulled up
by a few positions beyond that). That is the expected direction: an FP16 activation keeps 10 mantissa bits where BF16 keeps 7, which outweighs
rounding the GEMM's output to FP16 before it is widened.
A rounding-order control as in the V100 report did not work here: `--prefill 2048` instead of auto (8192; the smaller
chunk did take effect, 1,059 lent slots instead of the 8192-token ring) gave results **bit-identical** to the old path, so on
this path the chunking does not change the arithmetic and cannot serve as a control.
## Where the rounding moves
Round-to-nearest bounds per value rounded, for values in FP16's normal range (|x| ≥ 6.1e-5; below that FP16 is
subnormal and its relative error grows): BF16 keeps 8 significant bits (≤ 2^-8 ≈ 0.39%), FP16 keeps 11
(≤ 2^-11 ≈ 0.049%) but ends at 65504. Both paths accumulate in fp32.
| | old path | FP16 path |
| --- | --- | --- |
| BF16-weight products: hyper-connection down / up / inject, router, shared-expert gate, indexer, `ssm_alpha` / `ssm_beta`, PLE | W: the pack's BF16; **X rounded to BF16** (≤ 0.39% per element); Y fp32 | W converted to FP16 (exact in the normal range; 0.1-0.9% of these weights lie below it); X rounded to FP16 (≤ 0.049% per element); **Y rounded to FP16** (≤ 0.049%), then widened |
| quantized-weight products through `Gemm::native` / `Gemm::f16`, beta = 0 (X and W already FP16 in both paths) | Y fp32 | **Y rounded to FP16** (≤ 0.049%), then widened |
So the change removes most of the BF16 activation rounding and adds an FP16 rounding of every output. The two are not
directly comparable - an element's rounding enters a K-long dot product, where it partly cancels or adds up depending
on the data - which is why the distribution check above measures the net effect instead of arguing it. The reference
(`STRATA_PREFILL_BF16X2=1`) differs from the old path in exactly the term this change shrinks (it adds the BF16 low part
to every one of these activations), and the FP16 path comes out closer to it: by median KL and argmax agreement in all
three chats, by mean KL in two (the 106K chat's mean is pulled up by a few positions beyond its lower p99).
Not shown: the reference is not an fp32 forward pass, and this is one model, one corpus and 1,578 positions. A comparison
against an fp32 forward pass (e.g. llama.cpp on the CPU) is a follow-up for when time allows. Range is
the cost: the range check found nothing beyond 65504 here, but a model whose activations or BF16 weights reach that far
would need `STRATA_HIP_PROMPT_F16=0`.
## Output checks
- **Needle recall** (`tools/needle_bench.py --lengths 8k,32k,128k --depths 10,30,50,70,90`, greedy, thinking off, a fresh
server per path, one card; prompts of 7.9K, 32.2K and 125.4K tokens): **15 of 15 found with the old path, 15 of 15 with
FP16.** A 125K-token read takes 275 s on the old path, 139 s with FP16 (the later depths are shorter because the server
reuses the checkpoint of the text before the needle).
- **Repeatability:** two runs of each path give bit-identical log-probabilities (table above), and no run of this report
(17 server starts) stalled.
## What changed
- `Gemm::set_f16_io(bool)`: off by default, so every caller other than the prompt path (e.g. `gemm_bf16_parity`) runs what
it ran. `Prefill::init` turns it on together with `set_act_f16` when `prompt_f16()` is true for its device.
- `prompt_f16()`: gfx103x, cached per device (a layer split can mix cards); `STRATA_HIP_PROMPT_F16=0/1` overrides.
- With it on: `Gemm::f16` / `Gemm::native` run HH into Y's own rows (`ldc = 2 ldy` halves) and `widen_rows_f16` widens each
row from its end; `Gemm::bf16` converts W row slices to FP16 through the existing dequantization scratch (as
`native()` does) and takes X as the FP16 image. `bf16x2_mode()` is 0 there (the BF16 low part has no FP16 meaning).
- No new device memory, no host sync, no fallback branch; CUDA builds are unchanged (`#if defined(__HIPCC__)`).
## Limits
- One machine, one model (IQ3_S, packed from the GSQ-RCO GGUF, not one of setup's), one corpus for the distribution check.
- gfx1031/1032 (the same rocBLAS family) are enabled by the same `gfx103` prefix but were not run.
@@ -0,0 +1,55 @@
# 2x RX 6900 XT (gfx1030): a helper GPU's experts and the PCIe share (2026-10-04)
With a second GPU holding experts (`--expert-cache-device1`, `--remote-expert-opt`), the verify window's GPU plan counted
the experts that helper holds among the layer's misses and moved part of them - the PCIe share, `pcie_frac` of the misses
- to the primary card over PCIe, where the primary computed them while the helper sat idle for them. The plan runs before
`RemoteExperts::begin()` claims its rows, and `begin()` only takes the rows the plan left alone. #854 makes the plan treat
a helper's experts as it treats the peer tier's: not a miss, never in the PCIe share. The numbers below are the engine's
own `STRATA_DECODE_TIMING=1` report per request.
## Rig
- 2x AMD Radeon RX 6900 XT 16 GB (gfx1030), each PCIe 4.0 x8 through the CPU; no P2P. The engine's probe measured 14.1
GB/s host to device and set `pcie_frac` to 0.39 (0.55 is the default without a probe). AMD Ryzen 5 5600X (6 cores,
AVX2, no AVX-512; 5 expert-pool workers plus the host thread), 128 GB DDR4-3200.
- Ubuntu 26.04, kernel 7.0.0-38-generic, ROCm 10.0.0 (HIP 7.15.26333).
- Model: Qwen3.8-Flash-Next GSQ-RCO IQ3_S (2 shards, `--native`), MTP draft layer on, `--kv int8 --kv-resident 32768
--max-context 131072`. Configuration [strata-helper.json](strata-helper.json) (`/m` the model directory, `/work` the
build; `HIP_VISIBLE_DEVICES=1,0` makes the card on PCI bus 0f the primary): `--expert-cache auto
--expert-cache-device1 auto --remote-expert-opt --spec 4 --spec-min-p 0.5`. The primary's cache holds 4,861 experts
(9.26 GiB), the helper 8,163 more (15.46 GiB); 24,576 experts in all.
- Engines, all `-DSTRATA_ENABLE_HIP=ON -DCMAKE_HIP_ARCHITECTURES=gfx1030 -DSTRATA_PREFILL_MMQ=ON`:
- *baseline*: `origin/main` at `6f32ec0` (0.1.39) with #835 (`6add1a7`; #835 changes the prompt path's GEMMs only,
not the decode path measured here - its prompt tok/s are therefore higher than the fix's below);
- *baseline, same base* (2026-10-05): `6f32ec0` unchanged;
- *fix*: `6f32ec0` with #854 (`ffca1b0`).
## Method
A fresh server per arm. Two requests of about 5,000 prompt tokens each (two offsets into one text), "Summarize the
document above in about 300 words", sampled (temperature 1.0, top-p 0.95, top-k 20), up to 600 tokens; the engine's
`strata decode timing` line per request. Milliseconds per verify window and its parts are the measure; tok/s also
depends on how many drafts a sampled answer accepts and varies more.
## Results
| arm | ms / window | of which: GPU-reach wait | per-layer host | PCIe entries / layer-window | CPU entries / layer-window | decode tok/s |
| --- | ---: | ---: | ---: | ---: | ---: | ---: |
| baseline (`6add1a7`), probed share 0.39 | 59.6 / 58.5 | 43.4 / 43.7 | 8.6 / 8.3 | 4.00 / 3.99 | 0.83 / 0.43 | 40.6 / 39.3 |
| baseline, same base (`6f32ec0`), probed share 0.39 | 60.4 / 57.8 | 43.7 / 43.0 | 8.8 / 8.3 | 4.07 / 3.91 | 0.95 / 0.38 | 41.0 / 37.5 |
| **fix, probed share 0.39** | **37.2 / 33.8** | 19.5 / 18.6 | 9.9 / 9.1 | 0.26 / 0.06 | 1.40 / 0.57 | **65.6 / 67.7** |
| fix, probed share 0.39, run 2 (2026-10-05) | 35.2 / 33.8 | 18.8 / 18.7 | 9.3 / 9.1 | 0.20 / 0.06 | 1.17 / 0.56 | 63.0 / 64.4 |
| fix, `--pcie-frac 0` | 36.5 / 35.1 | 18.8 / 18.8 | 10.1 / 9.5 | 0 / 0 | 1.61 / 0.84 | 66.9 / 67.1 |
| baseline (`6add1a7`), `--pcie-frac 0` (the workaround) | 34.6 / 34.1 | 18.2 / 18.7 | 9.5 / 9.3 | 0 / 0 | - | 60.5 / 67.7 |
"GPU-reach wait" is the host waiting for the GPU to reach the next layer: with the share on, the primary spent 24 ms more
per window reading and computing about 4 experts per layer-window that the helper also held (the "PCIe entries"
column), and the helper waited. With the fix the share keeps only experts no GPU holds: 0.06-0.26 per layer-window here,
and the probed share costs nothing against `--pcie-frac 0` while taking a little off the CPU (1.40 -> 1.61 CPU entries
without it). Before the fix, `--pcie-frac 0` was the way around it.
## Limits
One machine, one model, two prompts per arm; one server run per arm, two for the fix with the probed share (one on each day). The change
is host code and backend-independent, but only HIP and only one helper were run; three or four helpers go through the same
loop and were not tested. The prompt path does not use this plan and is unaffected by the change.
@@ -0,0 +1,46 @@
{
"exe": "/work/build-hip/strata",
"args": [
"--pack",
"/m/packs/iq3_s",
"--native",
"/m/gguf/Qwen3.8-Flash-Next-GSQ-RCO-IQ3_S-00001-of-00002.gguf",
"--ple-gguf",
"/m/gguf/Qwen3.8-Flash-Next-GSQ-RCO-IQ3_S-00002-of-00002.gguf",
"--expert-profile",
"/work/data/expert-profile.bin",
"--expert-cache",
"auto",
"--prefill",
"auto",
"--spec",
"4",
"--spec-min-p",
"0.5",
"--mtp",
"/m/mtp/rt",
"--max-context",
"131072",
"--kv",
"int8",
"--kv-resident",
"32768",
"--expert-cache-device1",
"auto",
"--remote-expert-opt"
],
"cwd": "/work",
"tokenizer": "/m/packs/iq3_s/tokenizer",
"model_name": "qwen3.8-flash-next-iq3_s",
"log": "/work/logs/strata.log",
"lib_dirs": [
"/opt/rocm/lib"
],
"port": 8089,
"backend": "hip",
"gpu": 1,
"gpus_asked": true,
"env": {
"HIP_VISIBLE_DEVICES": "1,0"
}
}
@@ -0,0 +1,143 @@
# Windows mapped expert-page release
Measured October 4, 2026, against upstream `6f32ec070f23ced9f50e704d854d775da52591ab` (0.1.39).
The same patched executable was run with `STRATA_FILE_RELEASE=0` and `STRATA_FILE_RELEASE=1`.
Releasing file-backed expert pages after GPU copies substantially increased available physical RAM on this
machine. Prefill was slightly slower. This is a memory-pressure improvement, not a general speed claim.
## What changes
Windows `FileExpertSource::release()` uses `VirtualUnlock` on complete interior pages of its read-only file views.
Windows documents that unlocked pages are removed from the process working set even when the call returns
`FALSE` with `ERROR_NOT_LOCKED`. Other errors are reported. The mapping stays valid and later reads fault in the
original bytes. Shared boundary pages are retained. The code resolves file spans directly, so it does not trim
resident complements, CUDA-pinned buffers or the temporary buffers used to assemble GGUF experts.
Existing cache-upload release calls use this implementation. Two additional calls release pages after prefill's
lent cache slots have finished refilling, in serving and one-shot generation. Other platforms are unchanged.
`STRATA_FILE_RELEASE=1` enables it; unset, `0`, or any other value leaves it disabled. The switch is read once
per engine process, so restart the engine when changing it.
This builds on the existing working-set handling in [#467](https://github.com/Niko1221/Strata/issues/467) and the
mapped-arena work in [#640](https://github.com/Niko1221/Strata/pull/640). Multi-GPU placement already exists upstream;
this patch does not implement it. See Microsoft's [VirtualUnlock documentation](https://learn.microsoft.com/en-us/windows/win32/api/memoryapi/nf-memoryapi-virtualunlock).
## Configuration
| Component | Measured configuration |
| --- | --- |
| System | Windows 11 Pro build 26200, Core i9-14900HX, 31.77 GiB usable RAM, 2 x 16 GB DDR5-5600 |
| GPU 0 | RTX 4080 Laptop, 12,282 MiB VRAM; also drives the desktop |
| GPU 1 | RTX 3090, 24,576 MiB VRAM, Thunderbolt eGPU |
| Links | Engine H2D probes approximately 12.3-12.9 GB/s and 2.4 GB/s respectively; no CUDA peer read/write |
| Driver | NVIDIA 616.56 |
| Storage | Model and pack on an internal WD PC SN560 NVMe |
| Build | MSVC 19.29.30157, CUDA 13.1.80, Release, AVX2 portable, SM 86/89, native experts enabled |
| Model | `ISTA-DASLab/Qwen3.8-Flash-Next-GSQ-RCO-GGUF`, IQ2_XS, with MTP |
| Placement | GPUs `0,1`, layer split 12, stage-weight trim, mapped experts |
| Runtime | Context 32768, int8 KV, 15 pool workers, auto expert cache and prefill, spec 4, minimum draft probability 0.5 |
GPU 0 held 6,144 expert pairs (about 8.00 GiB), GPU 1 held 13,381 (about 18.15 GiB); 19,525 of 24,576 pairs in total.
Prefill selected 8192 and borrowed approximately 3.77 GiB of expert cache on each GPU. The native pack used
`experts.bin` (35,454,976,000 bytes), `dense.bin` (1,538,035,200 bytes), and the GGUF PLE table on disk.
Power was AC; the existing power plan, drivers and system-managed pagefile were not changed.
Model revision: `ed59f92082b1e93c0e96d60a8b11aab089b52f09`.
MTP checkpoint revision: `de4b8e4d43b917e7706784d8bb445c9af86a3540`.
llama.cpp revision: `3cf03257f219afbe7334045ff7c6a06ac68c627d`.
The two downloaded GGUF shards were fully SHA-256 verified:
| Shard | Bytes | SHA-256 |
| --- | ---: | --- |
| 1 | 39,225,954,592 | `92cee27ae5bbadcd732416a0f7a7f0acc092399dbbe8f5a5efa707c2ec0a49d7` |
| 2 | 28,800,138,432 | `316b46f3a2dbd68c900f43136ab9449f9dcc3725dfd8c794847c204bc161e113` |
The measured prototype executable had SHA-256
`9118b02dcc0ad4bd8398ba033c641feb93800e0eeecea59ee42be141e4a186fc`.
It predates the submitted patch's decision to default the switch to OFF. Explicit `0` and `1` have the same
behavior in both versions; the numbers below are from the prototype, not a benchmark of the final rebuild.
## Measurements
Each arm had one excluded short warmup followed by three fresh prompts at each length, with 256 output tokens,
temperature zero and thinking disabled. Prompts ask for explanations of synthetic integer-processing code.
The harness checks that all prompt tokens were processed, no prefix tokens were reused, the response completed,
and exactly one request was recorded by the idle dedicated server. Actual prompt lengths were 1023, 4096 and 16384.
Medians of three requests per cell:
| Prompt | Prefill OFF / ON, tok/s | Decode OFF / ON, tok/s | Client TTFT OFF / ON, s |
| --- | ---: | ---: | ---: |
| 1K | 263.68 / 253.83 | 77.35 / 77.65 | 3.93 / 4.07 |
| 4K | 583.51 / 558.24 | 81.99 / 94.17 | 7.06 / 7.38 |
| 16K | 1045.85 / 1000.16 | 80.88 / 84.93 | 15.73 / 16.45 |
An independent Windows monitor sampled host RAM, process working sets, commit, pagefile use and physical-disk
counters approximately once per second. The following window includes warmup and gaps between requests, from
the first request's start to the last request's finish. GiB means bytes divided by 2^30.
| Metric | Release OFF | Release ON |
| --- | ---: | ---: |
| Available RAM, median | 0.512 GiB | 12.187 GiB |
| Available RAM, minimum | 0.277 GiB | 6.110 GiB |
| Engine working set, median | 23.379 GiB | 11.946 GiB |
| Engine nonprivate working set, median | 20.460 GiB | 8.689 GiB |
| System commit, median | 51.880 GiB | 51.973 GiB |
| Engine private commit, median | 36.807 GiB | 36.804 GiB |
| Pagefile use, median | 1.233 GiB | 1.218 GiB |
| Internal physical-disk reads, total | 16.750 GiB | 16.809 GiB |
The benefit was available physical RAM. Commit was effectively unchanged; this does not solve a commit-limit
failure or eliminate the need for pagefile capacity. Physical reads were also effectively unchanged.
Nonprivate working-set bytes include file mappings and shared images, not only model pages. Physical-disk
counters are system-wide and omit about 1.1-1.3 seconds at the sampled interval edges. The external disk was idle.
These were sequential OFF-then-ON runs on one machine, with warm engines and retained OS file-cache history.
The adaptive expert cache remained enabled. The OS cache was not flushed; this is not a cold-SSD experiment.
Three repetitions provide observed ranges, not confidence intervals. Code-explanation outputs differed between
the two arms despite identical measured requests; speculative decode rates are workload-dependent. Prefill
throughput dropped 3.7-4.4%, and median whole-request time changed by +3.5%, -1.1% and +2.9% at 1K, 4K and 16K.
These observations do not establish a decode-speed improvement, output equivalence, or results on other hardware.
## Reproduction and checks
`benchmark.py` and `monitor_windows.py` are the scripts used for these measurements (the raw per-trial export and the harness
self-tests were left out of the repository). The switch was called `STRATA_ARENA_RELEASE` in the prototype that measured this;
it is `STRATA_FILE_RELEASE` here, because the old name already means the opposite elsewhere (`=0` keeps experts a GPU holds).
The benchmark needs the repository's normal server/tokenizer dependencies; the Windows monitor also needs psutil.
Start a dedicated server on `127.0.0.1:18080` using a configuration with the runtime settings above, the patched
engine, and `"env": {"STRATA_FILE_RELEASE": "0"}`. Wait until the engine is loaded and idle. In another terminal,
run the monitor against the server process ID, and then the benchmark (replace the uppercase placeholders):
```powershell
python bench/results/2026-10-04-windows-mapped-release/monitor_windows.py --server-pid SERVER_PID --out off-host.jsonl --stop-file off-monitor.stop
```
```powershell
python bench/results/2026-10-04-windows-mapped-release/benchmark.py --pack PACK_DIR --config CONFIG_JSON --label release-off --out results-off --targets 1024,4096,16384 --runs 3 --max-tokens 256
```
Stop the monitor by creating its stop file. Unload and stop the server, change only the release value to `1`,
restart, and repeat with new ON output paths. Keep available VRAM, background workloads and model files stable.
For host summaries, select samples with `min(started_epoch_s) <= epoch_s <= max(finished_epoch_s)` from the
benchmark's results, including warmup. Compute sample medians/minima and physical-disk last-minus-first counters.
Sum `windows_memory` counters for `strata.exe` processes only; nonprivate working set is total minus private.
The C++ tests cover canonical and variable native layouts, full-page alignment, preserved boundary pages,
three release/read cycles with byte-for-byte checks, invalid indices, a closed source, and three GGUF roles
across two shards. Build with `STRATA_BUILD_TESTS=ON` and `STRATA_NATIVE_EXPERTS=ON`, then run
`ctest --test-dir BUILD_DIR -R "^(file_expert_source_test|expert_layout_test)$" --output-on-failure` in fresh
processes with the switch unset, `0`, and `1`.
The final submitted source was rebuilt with the toolchain above. Both C++ tests passed with the switch unset,
`0`, and `1`. That rebuilt engine also completed a warmup and one fresh
request each at 1023 and 4096 prompt tokens, generating 256 tokens per measured request on both GPUs with release
enabled. This was an inference smoke test, not a replacement for the prototype's OFF/ON performance comparison.
Its executable SHA-256 is `9f1e96c36b2ddb530422ee37734606491c0f4c3c9b720095c68d63953e89913b`.
Additional prototype checks with release ON completed 30 requests without runtime errors: 21 of 24 objective
checks passed, with three JSON-only checks failing because of Markdown fences. All 24 objective responses and
statuses matched the official engine with the same GPU placement, including those three failures. Six prose
requests were unscored. Needle retrieval passed near 1K/4K/16K tokens at 50% depth. The functional battery was not
run on the release-OFF prototype, and these limited checks do not prove general model quality or token parity.
@@ -0,0 +1,381 @@
#!/usr/bin/env python3
"""Serial fresh-prompt benchmark, adapted from the community MI50/RTX 5090 harness.
Start a dedicated Strata server separately. This script never starts or stops it.
Expert adaptation and the OS file cache stay warm between requests; prefix reuse
is rejected. Engine file-tier counters describe decode, not physical disk reads.
"""
from __future__ import annotations
import argparse
import hashlib
import json
import platform
import statistics
import subprocess
import sys
import threading
import time
import urllib.error
import urllib.request
import uuid
from pathlib import Path
def write_json(path, value):
path.write_text(json.dumps(value, indent=2, ensure_ascii=False) + "\n", encoding="utf-8")
def get(url, timeout=30):
with urllib.request.urlopen(url, timeout=timeout) as response:
return json.load(response)
def digest(value):
return hashlib.sha256(json.dumps(value, sort_keys=True).encode("utf-8")).hexdigest()
def executable_record(config):
"""Resolve the configured binary exactly as serve/server.py does, without executing it."""
executable = Path(config["exe"])
if not executable.is_absolute():
executable = Path(config.get("cwd") or ".") / executable
executable = executable.resolve()
checksum = hashlib.sha256()
with executable.open("rb") as source:
for block in iter(lambda: source.read(1024 * 1024), b""):
checksum.update(block)
return {"path": str(executable), "sha256": checksum.hexdigest(), "size_bytes": executable.stat().st_size}
def engine_provenance_record(path, executable):
"""Keep compact build provenance and verify its executable artifact, when recorded."""
path = path.resolve()
raw = path.read_bytes()
source = json.loads(raw.decode("utf-8-sig"))
artifacts = source.get("artifacts", {})
if isinstance(artifacts, list):
entries = [(entry["file"], entry) for entry in artifacts]
elif isinstance(artifacts, dict):
entries = list(artifacts.items())
else:
raise ValueError("Provenance artifacts must be a list or dictionary")
matches = [entry for name, entry in entries
if Path(name).name.casefold() == Path(executable["path"]).name.casefold()]
if len(matches) > 1:
raise ValueError("Provenance has multiple artifacts matching the configured executable")
expected = matches[0]["sha256"].lower() if matches else None
if expected is not None and expected != executable["sha256"]:
raise ValueError("Provenance executable SHA-256 does not match the configured executable")
toolchain_keys = ("cmake", "ninja", "msvc", "windows_sdk", "nvcc", "cuda", "cuda_architectures",
"cxx_standard", "cuda_standard", "build_type", "portable_avx2", "msvc_runtime",
"parallel_jobs", "tests_built")
toolchain = {key: value for key, value in source.get("toolchain", {}).items() if key in toolchain_keys}
# Release BUILD.json records CUDA and target architectures at the top level.
toolchain.update({key: source[key] for key in ("cuda", "archs", "ptx", "portable") if key in source})
return {"path": str(path), "sha256": hashlib.sha256(raw).hexdigest(),
"version": source.get("strata_version", source.get("version")), "commit": source.get("commit"),
"fingerprint": source.get("engine_source_fingerprint", source.get("source_fingerprint")),
"toolchain": toolchain, "repository": source.get("fork_repository"),
"llama_cpp_commit": source.get("llama_cpp_pinned_commit"),
"executable_hash_verified": expected is not None, "recorded_executable_sha256": expected}
def validate_completion(before, after, chunks, expected, finish, done, text):
if not done:
raise RuntimeError("The stream ended without [DONE]")
if not text:
raise RuntimeError("The response contained no text")
if finish not in ("length", "stop"):
raise RuntimeError(f"Unsuccessful finish reason: {finish!r}")
if after["totals"]["requests"] != before["totals"]["requests"] + 1:
raise RuntimeError("Request counter mismatch; another client or engine restart invalidated this trial")
if after["live"]["state"] != "idle" or after["live"].get("queued", 0):
raise RuntimeError("The dedicated server was not idle after the request")
engine = after["requests"][0]
usage_chunk = next((chunk for chunk in reversed(chunks) if "usage" in chunk), None)
if not usage_chunk or not usage_chunk.get("timings"):
raise RuntimeError("The final stream chunk lacks usage or engine timings")
usage, timings = usage_chunk["usage"], usage_chunk["timings"]
if engine.get("reused") != 0 or usage["prompt_tokens_details"]["cached_tokens"] != 0:
raise RuntimeError("Fresh prompt reused cached tokens; restart this arm before resuming")
if engine["prompt_tokens"] != expected or usage["prompt_tokens"] != expected:
raise RuntimeError(f"Prompt token count mismatch; expected {expected}")
if engine.get("prompt_total") != expected or engine.get("prompt_read") != expected:
raise RuntimeError("The engine did not report reading the complete prompt")
if engine["finish"] != finish or engine["output_tokens"] != usage["completion_tokens"]:
raise RuntimeError("Stream usage or finish reason does not match /metrics")
if timings["prompt_n"] != expected or timings["predicted_n"] != usage["completion_tokens"]:
raise RuntimeError("Final stream timings do not match the completed request")
for name, timing_name in (("prompt_ms", "prompt_ms"), ("decode_ms", "predicted_ms")):
if engine[name] <= 0 or abs(engine[name] - timings[timing_name]) > 0.11:
raise RuntimeError(f"Invalid or mismatched engine timing: {name}")
if engine["engine_generated"] <= 0:
raise RuntimeError("The engine reported no generated tokens")
return engine, usage, timings
def perform(url, out, session, label, attempt, req, expected, interval=1.0, timeout=1800):
stem = f"{label}-attempt-{attempt}"
body = json.dumps(req, ensure_ascii=False).encode("utf-8")
(out / f"{stem}-request.json").write_bytes(body + b"\n")
row = {"label": label, "attempt": attempt, "session": session, "status": "failed",
"expected_prompt_tokens": expected, "request_sha256": hashlib.sha256(body).hexdigest(),
"started_epoch_s": time.time(), "raw_stream": f"{stem}-sse.jsonl",
"telemetry": f"{stem}-telemetry.jsonl", "text": "", "reasoning": ""}
chunks, texts, reasoning, telemetry_errors = [], [], [], []
stop = threading.Event()
monitor = None
started = None
def sample(output):
metrics = get(url + "/metrics")
hardware = dict(metrics["hardware"])
if hardware.get("ram_total") is not None and hardware.get("ram_used") is not None:
hardware["ram_available"] = hardware["ram_total"] - hardware["ram_used"]
sample_row = {"epoch_s": time.time(), "metrics_time": metrics.get("time"),
"live": metrics["live"], "hardware": hardware,
"completed_requests": metrics["totals"]["requests"]}
output.write(json.dumps(sample_row) + "\n")
output.flush()
def monitor_loop(output):
while not stop.wait(interval):
try:
sample(output)
except Exception as error:
telemetry_errors.append(f"{type(error).__name__}: {error}")
output.write(json.dumps({"epoch_s": time.time(), "error": telemetry_errors[-1]}) + "\n")
output.flush()
break
try:
before = get(url + "/metrics")
row["metrics_before"] = before
if before["live"]["state"] != "idle" or before["live"].get("queued", 0):
raise RuntimeError("The dedicated server must already be loaded and idle")
if expected + req["max_tokens"] > before["engine"]["max_context"]:
raise RuntimeError("Prompt plus output cap exceeds the configured context")
with (out / row["telemetry"]).open("w", encoding="utf-8") as telemetry:
sample(telemetry)
monitor = threading.Thread(target=monitor_loop, args=(telemetry,), daemon=True)
monitor.start()
try:
with (out / row["raw_stream"]).open("w", encoding="utf-8") as raw:
started = time.perf_counter()
first, finish, done = None, None, False
wire = urllib.request.Request(url + "/v1/chat/completions", data=body,
headers={"Content-Type": "application/json"})
with urllib.request.urlopen(wire, timeout=timeout) as response:
for line in response:
raw.write(json.dumps({"elapsed_s": time.perf_counter() - started,
"line": line.decode("utf-8")}, ensure_ascii=False) + "\n")
raw.flush()
if not line.startswith(b"data:"):
continue
payload = line[5:].strip()
if payload == b"[DONE]":
done = True
break
chunk = json.loads(payload)
chunks.append(chunk)
if chunk.get("error"):
raise RuntimeError(f"Stream error: {chunk['error']}")
for choice in chunk.get("choices", []):
delta = choice.get("delta", {})
content = delta.get("content") or ""
thought = delta.get("reasoning_content") or ""
if (content or thought) and first is None:
first = time.perf_counter() - started
texts.append(content)
reasoning.append(thought)
finish = choice.get("finish_reason") or finish
row.update(client_ttft_s=first, client_elapsed_s=time.perf_counter() - started,
finish_reason=finish, stream_done=done)
after = get(url + "/metrics")
row["metrics_after"] = after
engine, usage, timings = validate_completion(before, after, chunks, expected,
finish, done, "".join(texts + reasoning))
row.update(engine=engine, usage=usage, timings=timings,
prefill_tok_s=expected / (engine["prompt_ms"] / 1000),
decode_tok_s=engine["engine_generated"] / (engine["decode_ms"] / 1000))
finally:
stop.set()
monitor.join()
try:
sample(telemetry)
except Exception as error:
telemetry_errors.append(f"{type(error).__name__}: {error}")
if telemetry_errors:
raise RuntimeError("Telemetry failed: " + "; ".join(telemetry_errors))
row["status"] = "ok"
except urllib.error.HTTPError as error:
row["error"] = {"type": type(error).__name__, "message": str(error), "http_status": error.code,
"body": error.read().decode("utf-8", errors="replace")}
except Exception as error:
row["error"] = {"type": type(error).__name__, "message": str(error)}
finally:
stop.set()
if monitor is not None and monitor.is_alive():
monitor.join()
row.update(text="".join(texts), reasoning="".join(reasoning), finished_epoch_s=time.time())
if started is not None and "client_elapsed_s" not in row:
row["client_elapsed_s"] = time.perf_counter() - started
write_json(out / f"{stem}-record.json", row)
write_json(out / f"{stem}-chunks.json", chunks)
return row
def summary(rows, targets):
groups = {}
for target in targets:
selected = [r for r in rows if r["label"].startswith(f"tokens-{target}-")]
good = [r for r in selected if r["status"] == "ok"]
fields = ("client_ttft_s", "client_elapsed_s", "prefill_tok_s", "decode_tok_s")
groups[str(target)] = {"successful_runs": len(good), "failed_attempts": len(selected) - len(good),
**{field: {"median": statistics.median(v), "min": min(v), "max": max(v)}
for field in fields if (v := [r[field] for r in good])}}
return groups
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--root", type=Path, default=Path(__file__).resolve().parents[3])
parser.add_argument("--pack", type=Path, required=True)
parser.add_argument("--config", type=Path, help="Server configuration, saved with relevant fields only")
parser.add_argument("--engine-provenance", type=Path,
help="Build provenance JSON or release BUILD.json; requires --config")
parser.add_argument("--url", default="http://127.0.0.1:18080")
parser.add_argument("--model", default="strata")
parser.add_argument("--label", required=True, help="Configuration label, such as iq2-xs-3090-resident")
parser.add_argument("--out", type=Path, required=True, help="A separate directory for each configuration")
parser.add_argument("--targets", default="1024,4096,16384")
parser.add_argument("--runs", type=int, default=3)
parser.add_argument("--max-tokens", type=int, default=256)
parser.add_argument("--timeout", type=float, default=1800)
parser.add_argument("--telemetry-interval", type=float, default=1.0)
parser.add_argument("--resume", action="store_true", help="Keep completed trials and preserve failed attempts")
args = parser.parse_args()
targets = [int(value) for value in args.targets.split(",")]
if not targets or min(targets) <= 0 or len(set(targets)) != len(targets):
parser.error("Targets must be unique positive token counts")
if args.runs < 1 or args.max_tokens < 1 or args.timeout <= 0 or args.telemetry_interval <= 0:
parser.error("Runs, output cap, timeout, and telemetry interval must be positive")
if args.engine_provenance and not args.config:
parser.error("--engine-provenance requires --config to identify the executable")
args.root, args.pack, args.out = args.root.resolve(), args.pack.resolve(), args.out.resolve()
url = args.url.rstrip("/")
sys.path[:0] = [str(args.root), str(args.root / "tools")]
from strata_tokenizer import Tokenizer
from serve.frontend import ChatTemplate, openai_to_messages
directory = args.pack / "tokenizer"
vocab = json.loads((directory / "vocab.json").read_text(encoding="utf-8"))
tokens = [None] * len(vocab)
for token, number in vocab.items():
tokens[number] = token
tokenizer = Tokenizer(tokens, (directory / "merges.txt").read_text(encoding="utf-8").splitlines(),
json.loads((directory / "token_type.json").read_text(encoding="utf-8")))
template = ChatTemplate(directory / "chat_template.jinja")
def request(content, maximum=args.max_tokens):
return {"model": args.model, "messages": [{"role": "user", "content": content}],
"temperature": 0, "reasoning_effort": "none", "max_tokens": maximum,
"stream": True, "stream_options": {"include_usage": True}}
def count(req):
messages, tools, kwargs = openai_to_messages(req)
return len(tokenizer.encode(template.render(messages, tools, **kwargs), parse_special=True))
initial = get(url + "/metrics")
if initial["live"]["state"] != "idle" or initial["live"].get("queued", 0):
raise RuntimeError("Start a dedicated, loaded and idle server before this benchmark")
config = {}
executable = None
if args.config:
source = json.loads(args.config.read_text(encoding="utf-8-sig"))
executable = executable_record(source)
keys = ("exe", "cwd", "args", "gpu", "layer_split", "split_skip_if_fits", "backend", "tokenizer", "log",
"model_name", "parallel", "expert_profile_save", "expert_profile_save_every")
config = {key: source[key] for key in keys if key in source}
compute_variables = {"CUDA_VISIBLE_DEVICES", "CUDA_DEVICE_ORDER", "STRATA_STAGE_TRIM", "STRATA_SPLIT_OWN",
"STRATA_ARENA_PIN_GIB", "STRATA_RESIDENT_PIN", "STRATA_RESIDENT_HEADROOM_GIB",
"STRATA_ARENA_MMAP", "STRATA_PREFILL_HELP", "STRATA_FETCH_THREADS", "STRATA_LOOKAHEAD",
"STRATA_DECODE_TIMING", "STRATA_SPLIT_TIMING", "STRATA_VERIFY_PROFILE",
"STRATA_SPLIT_MISS_MS", "STRATA_PF_FUSED", "STRATA_FILE_RELEASE"}
config["env"] = {key: value for key, value in source.get("env", {}).items()
if key in compute_variables}
revision = subprocess.run(["git", "-c", f"safe.directory={args.root.as_posix()}", "rev-parse", "HEAD"],
cwd=args.root, capture_output=True, text=True, check=True).stdout.strip()
provenance = engine_provenance_record(args.engine_provenance, executable) if args.engine_provenance else None
manifest = {"label": args.label, "frontend_source_commit": revision, "pack": str(args.pack),
"config": config, "engine_executable": executable,
"engine_provenance": provenance, "engine_reported_version": initial["engine"].get("version"),
"targets": targets, "runs": args.runs, "max_tokens": args.max_tokens,
"model": args.model, "server_model": initial["engine"]["model"],
"max_context": initial["engine"]["max_context"],
"hardware_static": initial.get("hardware_static"),
"tokenizer_sha256": {name: hashlib.sha256((directory / name).read_bytes()).hexdigest()
for name in ("vocab.json", "merges.txt", "token_type.json", "chat_template.jinja")},
"script_sha256": hashlib.sha256(Path(__file__).read_bytes()).hexdigest(),
"method": "Serial warm-engine trials; default adaptive expert state retained; zero prompt reuse required",
"disk_counter_note": "file_mb is the decode file tier, possibly OS-cached; disk_read_mb is system-wide physical I/O"}
manifest_path = args.out / "manifest.json"
if manifest_path.exists():
if not args.resume:
raise RuntimeError("Output already has a manifest; use a new output directory or --resume")
if digest(json.loads(manifest_path.read_text(encoding="utf-8"))) != digest(manifest):
raise RuntimeError("Resume manifest differs; use a new output directory")
elif args.resume:
raise RuntimeError("Cannot resume: output has no manifest")
args.out.mkdir(parents=True, exist_ok=True)
write_json(manifest_path, manifest)
rows_path = args.out / "results.json"
rows = json.loads(rows_path.read_text(encoding="utf-8")) if args.resume and rows_path.exists() else []
session = uuid.uuid4().hex[:12]
write_json(args.out / f"session-{session}.json", {"session": session, "epoch_s": time.time(),
"python": sys.version, "platform": platform.platform(), "initial_metrics": initial,
"initial_status": get(url + "/v1/status"), "resumed": args.resume})
def run(label, req):
if any(row["label"] == label and row["status"] == "ok" for row in rows):
print("skip completed", label, flush=True)
return
attempt = sum(row["label"] == label for row in rows) + 1
print("start", args.label, label, "tokens", count(req), flush=True)
row = perform(url, args.out, session, label, attempt, req, count(req),
args.telemetry_interval, args.timeout)
rows.append(row)
write_json(rows_path, rows)
write_json(args.out / "summary.json", summary(rows, targets))
print(row["status"], label, json.dumps({key: row.get(key) for key in
("client_ttft_s", "prefill_tok_s", "decode_tok_s", "error")}), flush=True)
if row["status"] != "ok":
raise RuntimeError(f"Benchmark stopped after failed trial {label}; evidence was retained")
run(f"warmup-{session}", request(f"Warmup {session}. Reply with exactly the word READY.", 16))
filler = "\n".join(f"def task_{i:05d}(value: int) -> int: return (value * {(i % 97) + 1} + {i}) % 100003"
for i in range(12000))
ending = ("\n\nWrite a detailed explanation of the code above. Discuss deterministic integer transforms, "
"modulo arithmetic, testing, naming, complexity, and maintainability. Write at least 600 words.")
for target in targets:
for trial in range(1, args.runs + 1):
# Checkpoints cover a complete turn or >=16K prompt tokens by default;
# this early differing nonce prevents a whole checkpoint from matching.
prefix = f"Benchmark nonce: series-{target}-trial-{trial}.\nReview this synthetic Python module:\n"
low, high = 0, len(filler)
while low < high:
middle = (low + high + 1) // 2
if count(request(prefix + filler[:middle] + ending)) <= target:
low = middle
else:
high = middle - 1
req = request(prefix + filler[:low] + ending)
if not target - 20 <= count(req) <= target:
raise RuntimeError(f"Could not construct target {target}: {count(req)}")
run(f"tokens-{target}-run-{trial}", req)
print("Complete:", args.out / "summary.json", flush=True)
if __name__ == "__main__":
main()
@@ -0,0 +1,139 @@
"""Record Windows host memory and physical-disk counters during a local experiment."""
from __future__ import annotations
import argparse
import ctypes
from ctypes import wintypes
import json
from pathlib import Path
import time
import psutil
class PerformanceInformation(ctypes.Structure):
_fields_ = [("cb", wintypes.DWORD)] + [
(name, ctypes.c_size_t) for name in (
"CommitTotal", "CommitLimit", "CommitPeak", "PhysicalTotal",
"PhysicalAvailable", "SystemCache", "KernelTotal", "KernelPaged",
"KernelNonpaged", "PageSize",
)
] + [(name, wintypes.DWORD) for name in (
"HandleCount", "ProcessCount", "ThreadCount",
)]
performance = ctypes.WinDLL("psapi", use_last_error=True).GetPerformanceInfo
performance.argtypes = [ctypes.POINTER(PerformanceInformation), wintypes.DWORD]
performance.restype = wintypes.BOOL
class ProcessMemoryCounters(ctypes.Structure):
_fields_ = [("cb", wintypes.DWORD), ("page_fault_count", wintypes.DWORD)] + [
(name, ctypes.c_size_t) for name in (
"peak_working_set_bytes", "working_set_bytes", "peak_paged_pool_bytes",
"paged_pool_bytes", "peak_nonpaged_pool_bytes", "nonpaged_pool_bytes",
"commit_bytes", "peak_commit_bytes", "private_commit_bytes",
"private_working_set_bytes",
)
] + [("shared_commit_bytes", ctypes.c_ulonglong)]
kernel = ctypes.WinDLL("kernel32", use_last_error=True)
kernel.OpenProcess.argtypes = [wintypes.DWORD, wintypes.BOOL, wintypes.DWORD]
kernel.OpenProcess.restype = wintypes.HANDLE
kernel.CloseHandle.argtypes = [wintypes.HANDLE]
kernel.CloseHandle.restype = wintypes.BOOL
process_memory = ctypes.WinDLL("psapi", use_last_error=True).GetProcessMemoryInfo
process_memory.argtypes = [wintypes.HANDLE, ctypes.POINTER(ProcessMemoryCounters), wintypes.DWORD]
process_memory.restype = wintypes.BOOL
def extended_memory(process):
handle = kernel.OpenProcess(0x1000, False, process.pid) # PROCESS_QUERY_LIMITED_INFORMATION
if not handle:
if not process.is_running():
raise psutil.NoSuchProcess(process.pid)
raise ctypes.WinError(ctypes.get_last_error())
try:
info = ProcessMemoryCounters()
info.cb = ctypes.sizeof(info)
if not process_memory(handle, ctypes.byref(info), info.cb):
raise ctypes.WinError(ctypes.get_last_error())
return {name: getattr(info, name) for name, _ in info._fields_ if name != "cb"}
finally:
kernel.CloseHandle(handle)
def sample(server):
info = PerformanceInformation()
info.cb = ctypes.sizeof(info)
if not performance(ctypes.byref(info), info.cb):
raise ctypes.WinError(ctypes.get_last_error())
processes = []
if server is not None:
try:
descendants = [server, *server.children(recursive=True)]
except psutil.NoSuchProcess:
descendants = []
for process in descendants:
try:
with process.oneshot():
processes.append({
"pid": process.pid,
"name": process.name(),
"memory": process.memory_info()._asdict(),
"windows_memory": extended_memory(process),
"cpu_seconds": process.cpu_times()._asdict(),
"io_counters": process.io_counters()._asdict(),
})
except psutil.NoSuchProcess:
continue
battery = psutil.sensors_battery()
return {
"epoch_s": time.time(),
"memory": psutil.virtual_memory()._asdict(),
"pagefile": psutil.swap_memory()._asdict(),
"commit_bytes": info.CommitTotal * info.PageSize,
"commit_limit_bytes": info.CommitLimit * info.PageSize,
"commit_peak_bytes": info.CommitPeak * info.PageSize,
"system_cache_bytes": info.SystemCache * info.PageSize,
"physical_disks": {
name: counters._asdict()
for name, counters in psutil.disk_io_counters(perdisk=True).items()
},
"ac_power": battery.power_plugged if battery is not None else None,
"processes": processes,
}
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--server-pid", type=int)
parser.add_argument("--out", type=Path, required=True)
parser.add_argument("--stop-file", type=Path, required=True)
parser.add_argument("--interval", type=float, default=1.0)
parser.add_argument("--samples", type=int, help="Optional bounded diagnostic run")
args = parser.parse_args()
if args.interval <= 0 or (args.samples is not None and args.samples < 1):
parser.error("Interval and sample limit must be positive")
if args.stop_file.exists():
parser.error("The stop file already exists; choose a new experiment path")
server = psutil.Process(args.server_pid) if args.server_pid is not None else None
args.out.parent.mkdir(parents=True, exist_ok=True)
with args.out.open("x", encoding="utf-8") as output:
count = 0
while not args.stop_file.exists():
started = time.monotonic()
output.write(json.dumps(sample(server)) + "\n")
output.flush()
count += 1
if args.samples is not None and count >= args.samples:
break
if server is not None and not server.is_running():
break
time.sleep(max(0, args.interval - (time.monotonic() - started)))
if __name__ == "__main__":
main()
+288
View File
@@ -0,0 +1,288 @@
{
"kind": "clean-rotation-only-q2-native-same-binary-ab",
"date": "2026-10-04",
"source_commit": "18a30ad775bce86e99a54e6dc552ccb72c66d16e",
"source_archive_sha256": "a726e0be8b03c40d1c5c09b49da37e28ba6e34ef3fd247bb85adf043de927a88",
"engine_sha256": "6ed9db1dcc04b94e9816b73df30730395ff44e42f4def3b46722199aea5d881d",
"upstream_base": "6f32ec070f23ced9f50e704d854d775da52591ab",
"ggml_commit": "3cf03257f219afbe7334045ff7c6a06ac68c627d",
"hardware": {
"cpu": "AMD Ryzen 9 7950X 16-Core Processor",
"memory": "MemTotal: 130975976 kB",
"gpu": "NVIDIA RTX PRO 6000 Blackwell Workstation Edition, 97887, 595.91.07",
"kernel": "7.0.0-34-generic",
"compiler": "g++ (Ubuntu 13.3.0-6ubuntu2~24.04.1) 13.3.0"
},
"build": {
"type": "Release",
"cuda": "13.2",
"cuda_architectures": "120"
},
"model": {
"repo": "ISTA-DASLab/Qwen3.8-Flash-Next-GSQ-RCO-GGUF",
"revision": "ed59f92082b1e93c0e96d60a8b11aab089b52f09",
"verification": "Both contents verified against the pinned Hugging Face LFS SHA-256 values.",
"files": [
{
"path": "Q2_0/Qwen3.8-Flash-Next-GSQ-RCO-Q2_0-00001-of-00002.gguf",
"size": 37623740192,
"sha256": "69820c02ec7d0b45ef2ebb19d6620299db749fe2aded7f39f93c6b88b199b720"
},
{
"path": "Q2_0/Qwen3.8-Flash-Next-GSQ-RCO-Q2_0-00002-of-00002.gguf",
"size": 28800138432,
"sha256": "316b46f3a2dbd68c900f43136ab9449f9dcc3725dfd8c794847c204bc161e113"
}
]
},
"model_hash_note": "Both downloaded/reused shard contents were verified against the pinned publisher LFS hashes; size/inode/mtime are checked for stability before each arm.",
"layout": {
"layers": 48,
"experts_per_layer": 512,
"blob_bytes": 1382400,
"gate_up_type": "Q2_0",
"down_type": "Q2_0"
},
"model_formats": "Published GSQ-RCO Q2_0 model with its matching IQ4_NL PLE shard; FP16 KV.",
"pack_command_template": [
"python",
"{source}/tools/iq_pack.py",
"--gguf",
"{native}",
"--out",
"{pack}"
],
"pack_hashes": {
"native_experts_sha256": "015e53d3ae36324f172be4942bcf129539b343e2eb4fcd4f25ca5eff180bfa41",
"index_sha256": "c758314d495a1261d06a806b1c415596dfabe62e0cbf44ce592dc84c53229ccd",
"dense_sha256": "a43d7d0b69525335f4b000d5197a1d3aefcd3c0014fc3e12b3455e0a5d827ba2"
},
"qualification": "Storage parity on the standard Q2_0 model; no Q8 PLE reader extension or model conversion is required.",
"cache_note": "The 12000-slot cache is deliberately smaller than this 96 GB GPU allows to exercise RAM/GPU exchanges. This is not the fastest full-GPU Q2 configuration.",
"resident_complement_gib": 16.19,
"input_tokens": 1024,
"output_tokens": 1024,
"allocated_context": 40960,
"prompt_task": "Fixed-length storage parity and timing, not a model quality evaluation.",
"temperature": 0,
"input_token_ids_sha256": "9f8ae9eec9b56e2a04adf499638c0c1c275be8fc34cf627e632a26d3711f65a7",
"eos_note": "EOS matching uses the out-of-vocabulary sentinel 2147483647 so all arms generate the requested count even if a normal end-of-turn token occurs.",
"args_template": [
"--pack",
"{pack}",
"--native",
"{native}",
"--expert-cache",
"12000",
"--resident-budget-gib",
"32",
"--expert-profile",
"{source}/data/expert-profile.bin",
"--adapt-every",
"4",
"--adapt-swaps",
"96",
"--adapt-decay",
"0.7",
"--pcie-frac",
"-1",
"--spec",
"2",
"--mtp",
"{mtp}",
"--mtp-max-t",
"1",
"--suffix-draft",
"0",
"--kv",
"fp16",
"--max-context",
"40960",
"--prompt-cache",
"0",
"--conversation-cache-mib",
"0",
"--prefill",
"1024",
"--no-prefill-borrow",
"--vram-reserve-mib",
"2048",
"--ple-io",
"ram",
"--ple-row-cache",
"1048576",
"--eos-ids",
"2147483647",
"--stats",
"--native-dense-gguf",
"{model}/Qwen3.8-Flash-Next-GSQ-RCO-Q2_0-00001-of-00002.gguf",
"--native-dense-gguf",
"{model}/Qwen3.8-Flash-Next-GSQ-RCO-Q2_0-00002-of-00002.gguf"
],
"environment": {
"STRATA_ADAPT_NOWAIT": "0",
"STRATA_DECODE_TIMING": "1"
},
"profile_sha256": "8f59b4aa8873209dff11c11e37bcda9529a1335b724a1afeea37bf6388975baf",
"process_policy": "A fresh native engine per arm; no prompt or conversation reuse.",
"decoding": "Target-only: --mtp-max-t 1 --suffix-draft 0. Required MTP runtime loaded, zero offered drafts.",
"output_tokens_identical": true,
"first_differing_output_index": null,
"token_hash_encoding": "SHA-256 of UTF-8 compact JSON array, no whitespace or trailing newline.",
"token_ids_file": "exchange-rotation-token-ids.json",
"token_ids_file_sha256": "2c904cf4a7f41650ec06f54ffa773636114efed8cff059d825e5431f5aa666aa",
"arms": [
{
"rotation": 0,
"output_tokens": 1024,
"decode_tokens_per_second": 104.89546306634843,
"startup_seconds": 23.845058003003942,
"wall_seconds": 10.330669555987697,
"timings": {
"generated": 1024,
"prompt_tokens": 1024,
"prompt_ms": 567.9,
"decode_ms": 9762.1,
"finish": "length",
"drafts_accepted": 0,
"drafts_offered": 0,
"reused": 0,
"hits": 454042,
"lookups": 480592,
"ram_blobs": 41285,
"file_blobs": 0,
"file_mb": 0.0,
"prompt_read": 1024,
"offloaded": 10928
},
"rotation_active": false,
"rotated_blocks": 0,
"resident_exchanges": 3802,
"avoided_memcpy_payload_bytes": 0,
"engine_info": {
"version": "0.1.39",
"context": 40960,
"kv": "fp16",
"kv_resident": 0,
"expert_slots": 12000,
"expert_cache_mib": 15820,
"expert_slots_primary": 12000,
"expert_cache_primary_mib": 15820,
"spec": 2,
"mtp_max": 1,
"lookup": 0,
"vram_free_mib": 74168,
"cvec": 0,
"arena_mib": 16579,
"pool_workers": 15,
"pcie_frac": "0.55",
"spec_min_p": "0.00",
"conversation_cache_mib": 0,
"conversation_cache_slots": 4,
"conversation_cache_min_free_mib": 2560,
"tail_role_token": -1,
"vram_elastic": 0,
"engine": "0.1.39"
},
"diagnostics": [
"strata decode timing: 1024 windows, avg T 1.00, 1.00 tokens/window, 9.53 ms/window = verify 8.73 (GPU-reach wait 7.11 + per-layer host 1.28 [plan 0.03 actq 0.02 jobs 0.00 CPU 1.15] + stage 0.01) + commit/emit 0.01 + draft 0.00; per layer-window: CPU experts 0.54 (0.54 entries), VRAM hits 9.24, PCIe 0.22",
"strata serve: resident RAM: 16.19 GiB of experts in RAM, 3802 exchanged with the VRAM tier, 0 blob reads from the file"
],
"output_token_ids_sha256": "b384e4b16b5bd1c41f4739d053eb50d100a9cbfdb18fc5b76e31d188015961be"
},
{
"rotation": 1,
"output_tokens": 1024,
"decode_tokens_per_second": 113.44249218975028,
"startup_seconds": 13.035045650991378,
"wall_seconds": 9.620121874992037,
"timings": {
"generated": 1024,
"prompt_tokens": 1024,
"prompt_ms": 592.7,
"decode_ms": 9026.6,
"finish": "length",
"drafts_accepted": 0,
"drafts_offered": 0,
"reused": 0,
"hits": 454042,
"lookups": 480592,
"ram_blobs": 41285,
"file_blobs": 0,
"file_mb": 0.0,
"prompt_read": 1024,
"offloaded": 10928
},
"rotation_active": true,
"rotated_blocks": 3802,
"resident_exchanges": 3802,
"avoided_memcpy_payload_bytes": 5255884800,
"engine_info": {
"version": "0.1.39",
"context": 40960,
"kv": "fp16",
"kv_resident": 0,
"expert_slots": 12000,
"expert_cache_mib": 15820,
"expert_slots_primary": 12000,
"expert_cache_primary_mib": 15820,
"spec": 2,
"mtp_max": 1,
"lookup": 0,
"vram_free_mib": 74168,
"cvec": 0,
"arena_mib": 16579,
"pool_workers": 15,
"pcie_frac": "0.55",
"spec_min_p": "0.00",
"conversation_cache_mib": 0,
"conversation_cache_slots": 4,
"conversation_cache_min_free_mib": 2560,
"tail_role_token": -1,
"vram_elastic": 0,
"engine": "0.1.39"
},
"diagnostics": [
"FileExpertSource: exchange buffer rotation enabled: 96 buffers, 1382400 bytes each; no host commit memcpy",
"strata decode timing: 1024 windows, avg T 1.00, 1.00 tokens/window, 8.82 ms/window = verify 8.41 (GPU-reach wait 6.83 + per-layer host 1.23 [plan 0.03 actq 0.02 jobs 0.01 CPU 1.10] + stage 0.01) + commit/emit 0.01 + draft 0.00; per layer-window: CPU experts 0.54 (0.54 entries), VRAM hits 9.24, PCIe 0.22",
"strata serve: resident RAM: 16.19 GiB of experts in RAM, 3802 exchanged with the VRAM tier, 0 blob reads from the file",
"strata serve: exchange rotation: 3802 blocks, 5255884800 host memcpy bytes avoided (cumulative payload)"
],
"output_token_ids_sha256": "b384e4b16b5bd1c41f4739d053eb50d100a9cbfdb18fc5b76e31d188015961be"
}
],
"validation": [
{
"name": "configure",
"exit_code": 0,
"seconds": 2.0173747100052424
},
{
"name": "build",
"exit_code": 0,
"seconds": 74.66783442400629
},
{
"name": "ctest",
"exit_code": 0,
"seconds": 0.36451481201220304
},
{
"name": "rotation-gpu",
"exit_code": 0,
"seconds": 0.4641803269914817
},
{
"name": "memcheck",
"exit_code": 0,
"seconds": 0.5145799079909921
}
],
"comparison_exit_code": 0,
"limitations": [
"One off/on pair; no general speedup or speculative-path token-parity claim.",
"GPU cache deliberately capped to exercise exchanges; Q2 can fit fully on this 96 GB card.",
"EOS ignored for exact output length; not an answer-quality measurement.",
"AMD and Windows GPU execution untested."
]
}
@@ -0,0 +1 @@
{"input":[248045,846,198,8441,8129,318,1856,10895,1590,198,760,50245,8129,7276,18541,13017,321,7262,13,198,760,50245,8129,7276,18541,13017,321,7262,13,198,760,50245,8129,7276,18541,13017,321,7262,13,198,760,50245,8129,7276,18541,13017,321,7262,13,198,760,50245,8129,7276,18541,13017,321,7262,13,198,760,50245,8129,7276,18541,13017,321,7262,13,198,760,50245,8129,7276,18541,13017,321,7262,13,198,760,50245,8129,7276,18541,13017,321,7262,13,198,760,50245,8129,7276,18541,13017,321,7262,13,198,760,50245,8129,7276,18541,13017,321,7262,13,198,760,50245,8129,7276,18541,13017,321,7262,13,198,760,50245,8129,7276,18541,13017,321,7262,13,198,760,50245,8129,7276,18541,13017,321,7262,13,198,760,50245,8129,7276,18541,13017,321,7262,13,198,760,50245,8129,7276,18541,13017,321,7262,13,198,760,50245,8129,7276,18541,13017,321,7262,13,198,760,50245,8129,7276,18541,13017,321,7262,13,198,760,50245,8129,7276,18541,13017,321,7262,13,198,760,50245,8129,7276,18541,13017,321,7262,13,198,760,50245,8129,7276,18541,13017,321,7262,13,198,760,50245,8129,7276,18541,13017,321,7262,13,198,760,50245,8129,7276,18541,13017,321,7262,13,198,760,50245,8129,7276,18541,13017,321,7262,13,198,760,50245,8129,7276,18541,13017,321,7262,13,198,760,50245,8129,7276,18541,13017,321,7262,13,198,760,50245,8129,7276,18541,13017,321,7262,13,198,760,50245,8129,7276,18541,13017,321,7262,13,198,760,50245,8129,7276,18541,13017,321,7262,13,198,760,50245,8129,7276,18541,13017,321,7262,13,198,760,50245,8129,7276,18541,13017,321,7262,13,198,760,50245,8129,7276,18541,13017,321,7262,13,198,760,50245,8129,7276,18541,13017,321,7262,13,198,760,50245,8129,7276,18541,13017,321,7262,13,198,760,50245,8129,7276,18541,13017,321,7262,13,198,760,50245,8129,7276,18541,13017,321,7262,13,198,760,50245,8129,7276,18541,13017,321,7262,13,198,760,50245,8129,7276,18541,13017,321,7262,13,198,760,50245,8129,7276,18541,13017,321,7262,13,198,760,50245,8129,7276,18541,13017,321,7262,13,198,760,50245,8129,7276,18541,13017,321,7262,13,198,760,50245,8129,7276,18541,13017,321,7262,13,198,760,50245,8129,7276,18541,13017,321,7262,13,198,760,50245,8129,7276,18541,13017,321,7262,13,198,760,50245,8129,7276,18541,13017,321,7262,13,198,760,50245,8129,7276,18541,13017,321,7262,13,198,760,50245,8129,7276,18541,13017,321,7262,13,198,760,50245,8129,7276,18541,13017,321,7262,13,198,760,50245,8129,7276,18541,13017,321,7262,13,198,760,50245,8129,7276,18541,13017,321,7262,13,198,760,50245,8129,7276,18541,13017,321,7262,13,198,760,50245,8129,7276,18541,13017,321,7262,13,198,760,50245,8129,7276,18541,13017,321,7262,13,198,760,50245,8129,7276,18541,13017,321,7262,13,198,760,50245,8129,7276,18541,13017,321,7262,13,198,760,50245,8129,7276,18541,13017,321,7262,13,198,760,50245,8129,7276,18541,13017,321,7262,13,198,760,50245,8129,7276,18541,13017,321,7262,13,198,760,50245,8129,7276,18541,13017,321,7262,13,198,760,50245,8129,7276,18541,13017,321,7262,13,198,760,50245,8129,7276,18541,13017,321,7262,13,198,760,50245,8129,7276,18541,13017,321,7262,13,198,760,50245,8129,7276,18541,13017,321,7262,13,198,760,50245,8129,7276,18541,13017,321,7262,13,198,760,50245,8129,7276,18541,13017,321,7262,13,198,760,50245,8129,7276,18541,13017,321,7262,13,198,760,50245,8129,7276,18541,13017,321,7262,13,198,760,50245,8129,7276,18541,13017,321,7262,13,198,760,50245,8129,7276,18541,13017,321,7262,13,198,760,50245,8129,7276,18541,13017,321,7262,13,198,760,50245,8129,7276,18541,13017,321,7262,13,198,760,50245,8129,7276,18541,13017,321,7262,13,198,760,50245,8129,7276,18541,13017,321,7262,13,198,760,50245,8129,7276,18541,13017,321,7262,13,198,760,50245,8129,7276,18541,13017,321,7262,13,198,760,50245,8129,7276,18541,13017,321,7262,13,198,760,50245,8129,7276,18541,13017,321,7262,13,198,760,50245,8129,7276,18541,13017,321,7262,13,198,760,50245,8129,7276,18541,13017,321,7262,13,198,760,50245,8129,7276,18541,13017,321,7262,13,198,760,50245,8129,7276,18541,13017,321,7262,13,198,760,50245,8129,7276,18541,13017,321,7262,13,198,760,50245,8129,7276,18541,13017,321,7262,13,198,760,50245,8129,7276,18541,13017,321,7262,13,198,760,50245,8129,7276,18541,13017,321,7262,13,198,760,50245,8129,7276,18541,13017,321,7262,13,198,760,50245,8129,7276,18541,13017,321,7262,13,198,760,50245,8129,7276,18541,13017,321,7262,13,198,760,50245,8129,7276,18541,13017,321,7262,13,198,760,50245,8129,7276,18541,13017,321,7262,13,198,760,50245,8129,7276,18541,13017,321,7262,13,198,760,50245,8129,7276,18541,13017,321,7262,13,198,760,50245,8129,7276,18541,13017,321,7262,13,198,760,50245,8129,7276,18541,13017,321,7262,13,198,760,50245,8129,7276,18541,13017,321,7262,13,198,760,50245,8129,7276,18541,13017,321,7262,13,198,760,50245,8129,7276,18541,13017,321,7262,13,198,760,50245,8129,7276,18541,13017,321,7262,198,3601,314,8129,13,271,2427,38453,494,220,16,13,8984,1132,23173,6572,24959,18101,539,12258,13,13262,24217,2980,279,2468,3798,13,2233,16308,11,1970,11,59429,466,15673,13,248046,198,248045,74455,198,248068,271,248069,271],"off":[16,220,17,220,18,220,19,220,20,220,21,220,22,220,23,220,24,220Line truncated
+19 -2
View File
@@ -8,10 +8,11 @@ endif()
# maintainers; gfx1101 (RX 7800 XT, #254) and gfx1200 (RX 9060 XT, #256) by their owners. gfx1102 (RX 7600) has the
# same LDS limit and dot4 instruction and passed ctest (#192), but no model run has been reported yet. RDNA2 gfx1030 (RX 6800 / 6900) has the
# same LDS limit and wave32 but an older dot4 instruction (v_dot4_i32_i8, hip_compat/intrinsics.hpp); a community
# report ran it (#311), the maintainers have not.
# report ran it (#311), the maintainers have not. gfx1151 (Ryzen AI Max / Strix Halo, Radeon 8060S, RDNA3.5) is the
# same wave32 / 64 KiB LDS / sudot4 family as gfx1100, on a unified-memory APU; experimental.
set(_strata_hip_validated gfx1100 gfx1201)
set(_strata_hip_community gfx1101 gfx1200)
set(_strata_hip_unvalidated gfx1102 gfx1030)
set(_strata_hip_unvalidated gfx1012 gfx1102 gfx1030 gfx1031 gfx1034 gfx1151)
# CMake hands HIP a ';' list, but a -DCMAKE_HIP_ARCHITECTURES typed by hand (or ROCm's own Windows tooling) may use
# spaces, which foreach(IN LISTS) would otherwise treat as one element.
string(REPLACE " " ";" _strata_hip_norm "${CMAKE_HIP_ARCHITECTURES}")
@@ -44,6 +45,13 @@ string(REPLACE ";" "," STRATA_HIP_ARCHS "${STRATA_HIP_ARCH_LIST}")
enable_language(HIP)
find_package(hip CONFIG REQUIRED)
find_package(hipblas CONFIG REQUIRED)
# Older distro hipBLAS has no workspace API. The compatibility shim uses
# rocBLAS directly for that version; newer hipBLAS keeps its existing path.
set(STRATA_HIP_BLAS_TARGETS roc::hipblas)
if(hipblas_VERSION VERSION_LESS "1.0")
find_package(rocblas CONFIG REQUIRED)
list(APPEND STRATA_HIP_BLAS_TARGETS roc::rocblas)
endif()
find_package(hipblaslt CONFIG QUIET)
if(NOT TARGET hip::host)
@@ -67,6 +75,15 @@ add_library(strata_hip_runtime INTERFACE)
target_include_directories(strata_hip_runtime BEFORE INTERFACE
"${STRATA_HIP_COMPAT_INCLUDE_DIR}" "${CMAKE_CURRENT_SOURCE_DIR}/include")
target_compile_definitions(strata_hip_runtime INTERFACE STRATA_USE_HIP=1 "STRATA_HIP_ARCHS=\"${STRATA_HIP_ARCHS}\"")
option(STRATA_GFX1012_PORTABLE_DOT "Use the portable signed-byte dot control on gfx1012" OFF)
if(STRATA_GFX1012_PORTABLE_DOT)
target_compile_definitions(strata_hip_runtime INTERFACE STRATA_GFX1012_PORTABLE_DOT=1)
endif()
# #313: the RDNA3 WMMA GEMM's intrinsics only compile for gfx11 targets and the host pass does not define
# __gfx11xx__, so the build supplies the macro for gfx11 configs (the kernels also gate on the device at runtime)
if(CMAKE_HIP_ARCHITECTURES MATCHES "gfx11")
target_compile_definitions(strata_hip_runtime INTERFACE STRATA_WMMA_GFX11=1)
endif()
target_link_libraries(strata_hip_runtime INTERFACE hip::host)
# The shim renames the CUDA runtime to HIP, force-included into every host and device source. On Windows the host
# compiler is ROCm's clang++ too (tools/hip/build_windows.bat: CMake refuses to mix cl.exe with Clang HIP), which takes
Binary file not shown.
+11 -7
View File
@@ -14,7 +14,7 @@ Work through the steps in order. Tell the user what you are doing in plain words
user the key. Never put a tunnel or port forward in front of a server without a key.
- Do not change the user's system beyond what setup does (setup installs Python for the user account if needed,
and everything else inside the Strata folder and its `Strata-data` folder). Ask before installing drivers.
- The model download is ~70 GB (up to 111 GB for Unsloth's 4-bit). Confirm the user is fine with that before you
- The model download is ~70 GB (94 GB for Unsloth's UD-IQ4_XS, 111 GB for UD-Q4_K_XL). Confirm the user is fine with that before you
start, especially on a metered connection.
- Setup is long-running (an hour or more on a slow connection). Run it in the background or with a long timeout and
poll its output; do not kill it because it is quiet for a while. It is resumable: running the same command again
@@ -44,11 +44,12 @@ Requirements (details: [INSTALL.md](INSTALL.md#what-you-need)):
- **GPU:** NVIDIA RTX 20, 30, 40 or 50 series, or AMD Radeon RX 7900 XT / XTX, RX 7800 XT / 7700 XT, RX 9060 XT,
RX 9070 / 9070 XT, Radeon AI PRO R9700, RX 6800 / 6900 series; 12 GB of VRAM or more (an NVIDIA card with 8 GB runs,
slowly). GTX 10 series and older, and integrated GPUs, are not supported.
slowly). GTX 10 series and older, and integrated GPUs, are not supported; Pascal / Volta cards (P40, V100) and some
older AMD cards have experimental paths the user opts into ([OLDER_GPUS.md](OLDER_GPUS.md)).
- **Driver:** NVIDIA 580 or newer. AMD on Linux: the kernel's amdgpu driver; on Windows: a current AMD Adrenalin
driver. If the driver is missing or too old, tell the user to update it (NVIDIA App / nvidia.com/drivers, or AMD
Software) and restart; do not install drivers yourself unless they ask.
- **RAM:** 32 GB or more (see step 3). **Disk:** ~80 GB free, ideally on an NVMe SSD. **CPU:** x86-64 with AVX2.
- **RAM:** 32 GB or more (see step 3). **Disk:** ~80 GB free, ideally on an NVMe SSD. **CPU:** x86-64 with AVX2. Without AVX2 (Xeon E5 v1/v2 and older) setup still installs, as an experimental and slow build it compiles on the PC (10-20 minutes): tell the user that before starting ([INSTALL.md](INSTALL.md#older-cpus-experimental)).
- **OS:** Windows 10/11 or Linux (Ubuntu 22.04/24.04 are fully automatic).
If the PC does not meet them, say which part is missing and stop.
@@ -72,7 +73,7 @@ By the PC's RAM (ask the user whether they mainly want it for code - then the Co
| 32 GB | `--family coder` (size IQ1_M) | for code; with a 24 GB card Q2_0 and IQ2_XS also run (setup picks the low-RAM mode) |
| 48 GB | `--family qwen --model IQ2_XS` | or `Q2_0` (fastest) |
| 64 GB | `--family qwen --model IQ2_XS` (recommended) | `IQ3_XXS` / `IQ3_S` are slower and a bit better |
| 96 GB+ | `--family qwen --model IQ3_S` | `--family unsloth --model UD-Q4_K_XL` is experimental: NVIDIA only, NVMe SSD, 7-8.5 tokens/s on 64 GB |
| 96 GB+ | `--family qwen --model IQ3_S` | `--family unsloth --model UD-IQ4_XS` (~4-bit, 94 GB, part of the experts read from the SSD under ~80 GB of RAM); `--model UD-Q4_K_XL` is experimental: NVIDIA only, NVMe SSD, 7-8.5 tokens/s on 64 GB |
`--family swift` (Swift 1.5, a fine-tune that thinks shorter; sizes Q2_0, IQ2_XS, IQ3_XXS) is the alternative to
`qwen`. With `--yes` and no `--model`, setup picks the recommended size for the RAM itself. More: [MODELS.md](MODELS.md).
@@ -92,7 +93,7 @@ The flags (all of them: `START-HERE.bat --help`):
| --- | --- |
| `--yes` | take the recommended answer to every question (no prompts) |
| `--family qwen\|swift\|coder\|unsloth` | the model version |
| `--model Q2_0\|IQ2_XS\|IQ3_XXS\|IQ3_S\|IQ1_M\|UD-Q4_K_XL` | the size (the Coder is IQ1_M, Unsloth UD-Q4_K_XL) |
| `--model Q2_0\|IQ2_XS\|IQ3_XXS\|IQ3_S\|IQ1_M\|UD-IQ4_XS\|UD-Q4_K_XL` | the size (the Coder is IQ1_M, Unsloth UD-IQ4_XS or UD-Q4_K_XL) |
| `--context N` | context in tokens; default by VRAM: 32768 under 14 GB, 65536 under 20 GB, else 131072 |
| `--vision yes\|no\|gpu\|cpu` | read pictures; `--yes` leaves images off. AMD cards: `cpu` |
| `--gpu N` / `--gpus 0,1` / `--gpus all` | one card, or several sharing the model (default: the card with the most VRAM) |
@@ -132,7 +133,7 @@ Notes:
## 6. Start the server
Setup prints the start script it wrote (`start script: run-<model>.bat`). The name is the family tag plus the size,
lower case: `run-iq2_xs`, `run-swift-iq2_xs`, `run-coder-iq1_m`, `run-unsloth-ud-q4_k_xl`.
lower case: `run-iq2_xs`, `run-swift-iq2_xs`, `run-coder-iq1_m`, `run-unsloth-ud-iq4_xs`.
```
Windows (PowerShell): Start-Process -FilePath ".\run-iq2_xs.bat" (opens its own window)
@@ -168,10 +169,13 @@ line when it is ready; the engine log is `strata-<model>.log` in the Strata fold
- **Any OpenAI-compatible app or agent:** base URL `http://127.0.0.1:8080/v1`, any API key (or the configured one),
any model name.
- **Anthropic-compatible apps:** `http://127.0.0.1:8080/v1/messages`.
- **Codex CLI and other Responses API apps:** `http://127.0.0.1:8080/v1/responses` (stateless; Codex's
`config.toml`: [DETAILS.md](DETAILS.md#the-responses-api-and-codex-cli)).
- **Claude Code:** `ANTHROPIC_BASE_URL=http://127.0.0.1:8080`, `ANTHROPIC_MODEL` set to a Claude model name it knows
(Strata ignores the name), and any `ANTHROPIC_AUTH_TOKEN` (or the configured key).
- **Thinking level:** `"reasoning_effort": "none" | "low" | "medium" | "high"` (default high).
- Strata answers one request at a time. API details: [DETAILS.md](DETAILS.md#using-it).
- Strata answers one request at a time; `"parallel": N` in the model's config (or setup `--parallel N`) decodes up to
N together ([BATCHING.md](BATCHING.md)). API details: [DETAILS.md](DETAILS.md#using-it).
## 9. When something fails
+105 -6
View File
@@ -5,7 +5,8 @@ covers the build on Linux (on Windows a ready-made engine, see [Windows](#window
RX 9070 / 9070 XT / Radeon AI PRO R9700 (RDNA4, gfx1201; see [RDNA4](#rdna4-gfx1201)). The RX 7800 XT / 7700 XT
(gfx1101) and the RX 9060 XT (gfx1200) were validated by their owners (see [Community-validated
cards](#community-validated-cards)); the RX 6800 / 6900 series (RDNA2, gfx1030) builds and runs too, reported by a community machine and not yet validated by the maintainers (see [RDNA2](#rdna2-gfx1030)). Setup chooses it by itself on a PC with no NVIDIA card Strata can use (`--backend hip` on a PC with both); the
install steps for users are in [INSTALL.md](INSTALL.md#amd-cards). Other AMD architectures, wave64, and mixed
install steps for users are in [INSTALL.md](INSTALL.md#amd-cards). gfx906 (Instinct MI50 / MI60, Radeon VII; wave64) has a separate
opt-in build, see [gfx906](#gfx906-instinct-mi50--mi60-radeon-vii-wave64-built-from-source). The Ryzen AI Max "Strix Halo" APU (gfx1151, RDNA3.5, unified memory) is built from source on Linux: see [STRIX_HALO.md](STRIX_HALO.md). Other AMD architectures and mixed
AMD/NVIDIA execution in one run are not supported.
The backend maps the CUDA-shaped runtime and BLAS calls to HIP/hipBLAS, uses
@@ -46,7 +47,9 @@ the kernel's amdgpu driver (no ROCm install needed):
architectures above; the engine is compiled for each of them (cards of two families, e.g. gfx1100 + gfx1201, need
a system ROCm 7: AMD's wheels hold one family). A split pays only when no single card holds the model's experts
(see RDNA4 below).
- **Limits for now:** images only through the CPU encoder (`--vision cpu`, 0.1.32), no calibration. The Monitor
- **Limits for now:** images only through the CPU encoder (`--vision cpu`, 0.1.32). Setup does not offer the tuning
(calibration) on AMD yet: its controls are being checked on HIP one at a time (#566). Since 0.1.39 a tuning run by
hand (`./setup.sh --calibrate`) is saved for the AMD card it ran on and reused when setup runs again. The Monitor
shows the card's load, VRAM, temperature and power from Linux sysfs (0.1.32).
The rest of setup is the same as on NVIDIA: the model download, the start script, the server.
@@ -61,9 +64,9 @@ On a PC with no NVIDIA card Strata can use, the AMD card is chosen by itself; wi
Adrenalin Edition](https://www.amd.com/en/support/download/drivers.html)). Nothing else: no ROCm or HIP SDK
install, no compiler, no admin rights.
- **Detection:** setup reads the display adapters Windows lists (their PCI ids; the VRAM size from the display
driver's registry entry). An integrated Radeon is listed as not supported.
driver's registry entry). An integrated Radeon is listed as not supported, except the Ryzen AI Max "Strix Halo" (Radeon 8060S / 8050S / 8040S, gfx1151: [STRIX_HALO.md](STRIX_HALO.md)); a Strix Point (890M / 880M, gfx1150), Krackan (860M / 840M, gfx1152) or Phoenix / Hawk Point (780M / 760M, gfx1103) Radeon is named as what it is and not supported.
- **Engine:** the ready-made `strata-windows-x64-hip.zip` from the release (built by `tools\hip\build_windows.bat`
for gfx1100, gfx1101, gfx1102, gfx1200, gfx1201 and gfx1030) goes into `engine\`. It carries the ROCm libraries the
for gfx1100, gfx1101, gfx1102, gfx1200, gfx1201, gfx1030 and, from 0.1.40, gfx1151 (Strix Halo, unvalidated on Windows)) goes into `engine\`. It carries the ROCm libraries the
engine loads (`engine\rocm\bin`: the HIP runtime, hipBLAS / rocBLAS / hipBLASLt with their kernels for these cards,
amd_comgr and the Microsoft C++ runtime; ROCm 10.2.0a20260930 from AMD's TheRock builds, licenses in
`engine\rocm\licenses`). The HIP runtime works through the AMD driver's own components, so the driver is the one
@@ -198,6 +201,13 @@ compositor fails with "Failed to pin framebuffer with error -12".
The installer supports this backend (see "Install with setup" above). Images run through the CPU encoder for now (`--vision cpu`).
Setup installs one AMD card, or several with `--gpus` (the engine's layer split; see RDNA4 below).
**Opt-in switches for RDNA2 prompts and hangs (0.1.40, off by default):** `STRATA_DENSE_MMQ=1` (#820, HIP) runs the
prompt's dense GGUF projections through the int8 MMQ kernels instead of dequantize + hipBLAS: on the RX 6900 XT a 4K
prompt read 436 -> 757 tok/s (greedy text diverges from the default after a few dozen tokens: the q8_1 activation
rounding). `STRATA_HIP_ADAPT_KERNEL_COPY=1` (#884) copies the adaptive tier's swaps with a kernel instead of the SDMA
engine, a workaround for the gfx1030 hang seen with the MMQ prompt path and adaptive swaps (untested on the reporter's
machine). Windows HIP: the doorbell kernels fence their store (#697), and the shared-expert fork is off on HIP (#816).
## RDNA4 (gfx1201)
The RX 9070 / 9070 XT and the Radeon AI PRO R9700 run the same kernels as gfx1100: wave32, 64 KiB of LDS per
@@ -345,8 +355,84 @@ run it; the report below is from a community machine: an RX 6900 XT 16 GB (gfx10
[AMD_HIP_PERFORMANCE.md](AMD_HIP_PERFORMANCE.md) cost 8.6 and 13.6 tok/s here (30 with the defaults): keep the
defaults on a 16 GB card.
- **hipBLASLt:** ROCm's hipBLASLt ships no gfx1030 kernels, so there is no table and the plain hipBLAS path runs.
- **Not validated:** gfx1031 / gfx1032 (the same `dp4a` path, no hardware report), setup's own build path and the
`gfx103X-all` wheels on gfx1030, images, answer-quality benchmarks.
- **Prompt GEMMs in FP16 (#835):** rocBLAS on gfx1030 has tuned kernels for FP16-in / FP16-out GEMMs only; the prompt
path's FP16-in / FP32-out and BF16 products ran generic kernels about 6.6x slower (5.6 and ~5.3 TFLOPS against 37.7 at
N = 10240, T = 7313, K = 2560). `STRATA_HIP_PROMPT_F16=1` (opt-in, off by default; the engine prints a tip on gfx103x) runs the 16-bit prompt GEMMs FP16 in and out;
unset is the old path. `STRATA_DBG_NAN=1` also counts the non-finite FP16 outputs (a sum past 65504). The output is not bit-identical to it (an FP16 rounding of each GEMM's output replaces the BF16 rounding of
the BF16 GEMMs' activations); what the distribution check showed and did not show is in
[bench/results/2026-10-04-rdna2-fp16-prompt](../bench/results/2026-10-04-rdna2-fp16-prompt/README.md). Measured on a
second community machine - 2x RX 6900 XT 16 GB (one card for these numbers), Ryzen 5 5600X (6 cores, AVX2), 128 GB,
Ubuntu 26.04, ROCm 10.0.0, engine 0.1.39 built by hand with `-DSTRATA_PREFILL_MMQ=ON` as setup does, GSQ-RCO IQ3_S:
a 9.4K / 34.7K / 105.8K-token prompt reads at 744 / 915 / 926 tok/s instead of 439 / 466 / 461; decode is unchanged.
On that machine the HIP ctest passes 60 of 65 with and without this change: 2 skipped (`hip_prompt_attn_wmma`, no
matrix cores; `hip_prefill_hipblaslt_gemm`, no hipBLASLt table) and 3 that fail for reasons outside the engine
(`ple_parity` needs a Q2_0 PLE file that is not on that machine, `expert_multi_test` refuses the CPU without AVX-512,
`platform_memory_test` cannot `mlock` at the shell's default `ulimit -l`).
- **gfx1031** (RX 6700 XT, #524): setup knows it (the `gfx103X-all` wheels, unvalidated); its reporter runs it daily
on one card.
- **Not validated:** gfx1032 (the same `dp4a` path, no hardware report), setup's own build path and the
`gfx103X-all` wheels on gfx1030, images, answer-quality benchmarks. RDNA1 (gfx1012, RX 5500 XT) builds by hand:
[OLDER_GPUS.md](OLDER_GPUS.md#amd-building-gfx906-and-gfx1012).
## gfx906 (Instinct MI50 / MI60, Radeon VII): wave64, built from source
gfx906 is wave64 and has no WMMA and no packed byte arithmetic, so the wave32 backend above refuses it. A separate opt-in build compiles the CUDA sources as HIP through a small compat layer
(`include/strata/platform/hip_compat/`), with a CUDA warp mapped to a logical half of the 64-lane wavefront
(32-wide shuffles, a ballot of its own half). The hot kernels have wave64 layouts of their own (below). Setup does
not build it yet: build by hand, and run `serve/server.py` with a config as on any other card.
**ROCm.** AMD's current ROCm releases no longer ship gfx906 libraries. The build and the measurements below used
HIP 7.14 from the community image [`mixa3607/rocm-gfx906:7.14-complete`](https://hub.docker.com/r/mixa3607/rocm-gfx906)
(rocBLAS/hipBLAS with gfx906 kernels), on the kernel's amdgpu driver (Ubuntu 24.04, kernel 6.8). hipBLASLt is not
used.
```sh
# inside the image, with this checkout at /src and llama.cpp at the pinned commit in third_party/llama.cpp
apt-get update && apt-get install -y cmake ninja-build git python3 build-essential
cmake -S . -B build-906 -G Ninja -DSTRATA_HIP_GFX906=ON -DCMAKE_HIP_ARCHITECTURES=gfx906 \
-DSTRATA_GGML_DIR=/src/third_party/llama.cpp \
-DCMAKE_C_COMPILER=/opt/rocm/llvm/bin/clang -DCMAKE_CXX_COMPILER=/opt/rocm/llvm/bin/clang++
ninja -C build-906 strata
```
`STRATA_HIP_GFX906` turns on the CUDA targets and refuses `STRATA_ENABLE_HIP` (one HIP build at a time). Run the
container with `--device=/dev/kfd --device=/dev/dri --group-add video --ipc=host --ulimit memlock=-1`; on some boards
`HSA_OVERRIDE_GFX_VERSION=9.0.6` is needed for the runtime to accept the card.
**What differs from the CUDA build** (all under `STRATA_HIP_GFX906`; each A/B switch restores the CUDA layout):
| | gfx906 | switch |
|---|---|---|
| grouped native experts | mode 7: signs as `dp4a(g ^ m, u) - dp4a(m, u)` (no byte SIMD), grids and activations in LDS, one weight load per group; the IQ formats only (Unsloth's K-quant / Q5_1 / Q8_0 experts take the CUDA layout) | `STRATA_EXP_MODE=2` (previous AMD layout) |
| MMVQ | one wavefront per row, 64-lane butterfly, no LDS | `STRATA_MMVQ_WAVE=0` |
| router top-10 | one wavefront, register argmax | - |
| verify-window routing (256-expert router) | the window's tokens in one multi-column BF16 projection + one top-k, ~3 ms of a ~55 ms window | `STRATA_ROUTE_PER_TOKEN=1` |
| hyper-connection read | norm loads in flight, 8 lanes per row for `up` | `STRATA_GR_FAST=0` |
| `gr_down_multi`, opt-in | split along K (8 rows x 5 slices of 2048), ~3% per window; sums in another order than the single-token read, so off by default | `STRATA_GR_SPLIT=1` |
| IQ4 table lookup | llama.cpp's `v_perm_b32` sequence (HIP's `__byte_perm` is a scratch array) | - |
| QSA prompt attention | the FP32 kernel (no tensor cores / WMMA) | - |
Every switch above was checked bitwise against the layout it replaces: the parity tests on synthetic data,
`native_expert_bench` on real GGUF rows (one mode against another), and the greedy text of a fixed request.
**Measured** on 2x Instinct MI50 16 GB (85 W power limit each, PCIe 3.0 x16 both, peer access between them),
Xeon E5-2666 v3 (10 cores, AVX2), 32 GB DDR4, SATA SSD; Coder IQ1_M (`--native` pack), 131,072-token context,
`--kv int8 --kv-resident 32768`, `--layer-split 27`, `--prefill 4096`, MTP `--spec 4 --spec-min-p 0.5`,
`--pcie-frac 0`, `STRATA_ARENA_MMAP=1`. Measured on the port this build was cut from (builds on the 0.1.30 and 0.1.33 bases, the same
kernels, plus the layer-split weight trim and the mapped arena, which are separate pull requests):
- all 12,288 experts held in VRAM across the two cards;
- a 17,043-token agent prompt (Claude Code's first request, tools included) read in 43.5 s (392 tok/s), decode
on it 57.7-59.3 tok/s;
- 25 GB of the 32 GB RAM available while serving;
- six repeats of that request with a 6,000-token output cap and a 900-second two-client load test run six times
(407 requests, 202,940 tokens): no stalls, no errors, no GPU faults.
For comparison on the same machine: llama.cpp (3cf0325, ROCm) on the same Coder IQ1_M measured 26.9 tok/s `tg128`.
**Not done:** setup.py detection and an automatic build, Windows, images (`--vision`), MI60 and Radeon VII (the
same gfx906 ISA; not run), a single-card run, the tensor-split experiment (the halves of every layer on two cards;
it works but is not part of this build).
## Tuning table
@@ -367,6 +453,19 @@ prompt speed with and without it before keeping it.
Shipped tables:
- `gfx1100-hipblaslt-100100.txt`, `gfx1100-hipblaslt-100200.txt`: RX 7900 XTX.
- `gfx1100-hipblaslt-100401.txt`: RX 7900 XTX, calibrated with the packaged ROCm 10.0.0
(`rocm/dev-ubuntu-24.04:10.0.0-full`, hipBLASLt 1.4.1) on a Ryzen 7 9800X3D, over the 26 dense GEMM
geometries of the shipped gfx1100 table. Without it, that stack reads a prompt through plain hipBLAS: on
Swift 1.5 IQ3_XXS at 262144 ctx, fresh-prompt prefill measured 682 -> 1,038 tok/s at 1.7K tokens and
857 -> 1,524 tok/s at 6.5K (medians of 3, decode unchanged), with `hip_prefill_hipblaslt_gemm` reporting
`fallbacks=0`. setup uses it only when the installed hipBLASLt reports 1.4.1.
- `gfx1100-hipblaslt-100500.txt`: RX 7900 XTX (gfx1100, 24 GB), calibrated with a ROCm 10.2.0a20261003 nightly
SDK (`libamdhip64.so.7.17.26392`, hipBLASLt 1.5.0, version number 100500). Same 16 dense GEMM geometries at
T=4096 and T=8192 as the other gfx1100 tables (32 rows), calibrated with `tune_hipblaslt` run against that
library. On this ROCm the engine refuses the 100100/100200 tables (version mismatch) and prefills on plain
hipBLAS; with this table a 0.1.38-lineage nightly engine prefills a 131071-token prompt at 1687 tok/s vs 926
without it (median of 3 clean runs each, greedy; decode unchanged). Per-run data and the calibration command:
the `2026-10-04-community-rx7900xtx-hipblaslt-100500` folder of PR #745.
- `gfx1201-hipblaslt-100500.txt`: Radeon AI PRO R9700 (gfx1201, 32 GB), calibrated with ROCm 10.2.0a20260914
(AMD's `gfx120X-all` nightly, hipBLASLt 1.5.0, library build `d3164197`). 16 dense GEMM geometries at T=4096 and
T=8192, 32 rows. setup uses it only when the installed hipBLASLt reports 1.5.0 (it is found in `/opt/rocm`
+114
View File
@@ -0,0 +1,114 @@
# Batched DMA submission (experimental)
With `--pcie-mode dma`, `STRATA_DMA_BATCH=1` submits a verifier group's independent
expert uploads through `cudaMemcpyBatchAsync` on CUDA 13.0 or later. The default
is `0`, retaining the existing per-expert copies. The normal kernel-copy mode
is unchanged. This is an experiment to reduce host submission overhead, not a
demonstrated increase in model tokens per second.
`STRATA_DMA_BATCH=2` also sets CUDA 13.4's
`cudaMemcpyFlagPreferOverlapWithCompute`. It is a scheduling hint, not a
guarantee of overlap. On CUDA 13.0–13.3 this mode behaves like `1`. Older CUDA
builds and HIP retain individual copies. A single upload or a group larger than
128 also uses individual copies.
The sources must be immutable pinned host blobs, and destinations must be
disjoint. `expert_source.cpp` already restricts the DMA group to pinned experts.
The helper resolves device-visible aliases for registered host allocations;
Windows registered host pointers need not equal their GPU aliases. No extra
VRAM or weight conversion is introduced. All copies remain on the existing
stream, before the callback that signals flag B. A submission error fails the
engine rather than signalling readiness for stale weights.
## Tests
With the normal CUDA build and `STRATA_BUILD_TESTS=ON`:
```sh
cmake --build build --target dma_batch_parity
ctest --test-dir build -R dma_batch --output-on-failure
```
The test checks bytes and cross-stream event ordering for 0, 1, 4, 17, 64, 128,
and 129 uploads, at 37 bytes, 4 KiB and 2 MiB per upload, in all three modes.
Windows also runs the same cases with `VirtualAlloc` plus `cudaHostRegister`.
It prints interleaved diagnostic timings separately for submission and total
transfer time. These times overlap; they must not be added. On Windows it also
reports submitting-thread cycles using `QueryThreadCycleTime`. Cycle counts
are neither whole-process CPU utilization nor an energy measurement.
The 63-case matrix passed with both host allocation methods on CUDA 13.4 and
13.0, and with the individual-copy fallback on CUDA 12.6, all using the same
RTX 4070 Laptop and driver 616.56. This checks the older-toolkit fallback;
it is not a HIP runtime test.
## Initial measurement
RTX 4070 Laptop 8 GB, Ryzen 9 7940HS, 64 GB DDR5-5600, Windows, CUDA 13.4:
16 independent registered-host uploads of 2 MiB each, five measured repetitions
after warmup, alternating mode order:
| Path | Median host submission | Median total transfer |
| --- | ---: | ---: |
| Individual copies | 0.0936 ms | 2.5589 ms |
| Batch | 0.0162 ms | 2.5248 ms |
| Batch with overlap hint | 0.0155 ms | 2.5206 ms |
Batch submission latency fell 82.7%; total transfer latency fell only 1.3%.
Submission latency is measured wall time around the API calls, not whole-process
CPU utilization or energy consumption. The initial full-model screen on Swift
1.5 IQ3_XXS at 64K allocated context did not establish a generation-speed gain:
one coding request measured 30.3 tokens/s with individual DMA and 30.0 with
batching. Its generated lengths differed, so total task times cannot isolate
the transfer change. This evidence does not justify changing the default.
A separate CUDA 13.4 repetition with submitting-thread cycle measurements gave
449,824 cycles for individual copies and 67,939 for the batch, about 84.9% fewer.
Submission wall time was 0.1127 vs 0.0170 ms, while total transfer time was
2.5584 vs 2.5309 ms (about 1.1% lower). These are separate runs from the initial
table, not measurements to combine into a larger speedup.
## Model comparison on the isolated patch
An A-B-B-A comparison used only this patch on v0.1.39, with `--pcie-mode dma`.
Each fresh engine ran the same nine requests, including tool use, prompt reuse
and an independently checked interval-merging implementation. The coding request
used high reasoning, seed 4242, temperature 1, top-p 0.95, top-k 20 and a
3,072-token output limit. Context allocation was 65,536, INT8 KV with 20,480
resident cells, 1,040 cached experts, 7 workers plus the caller, PCIe fraction
0.35, and MTP 5 with English drafting and min-p 0.5.
| Run | Batch mode | Code decode tok/s | Output tokens including reasoning | Request wall time |
| --- | ---: | ---: | ---: | ---: |
| A1 | 0 | 29.7 | 928 | 32.98 s |
| B1 | 1 | 31.4 | 1,478 | 48.86 s |
| B2 | 1 | 30.3 | 1,314 | 45.12 s |
| A2 | 0 | 30.2 | 1,078 | 37.45 s |
All 36 requests passed. All four implementations passed 155 independent cases
and generator, invalid-input and non-mutation checks. Outputs differed even
between the two controls, and draft acceptance also differed. These runs do
not isolate a model speedup or show better time to a verified answer. The
byte-parity tests establish transfer correctness, not deterministic generated
text or general agent reliability.
A fifth run with batching enabled passed the same nine requests plus a
57,750-token prompt, auxiliary-call restoration, and three 1,024-square image
color checks including image-state restoration. An OpenAI-compatible HTTP
smoke request also passed. The long prompt read took 78.414 seconds; restoring
57,745 tokens after the side call reduced prompt processing to 0.301 seconds.
This validates the existing state cache with the patch, not a new cache speedup.
The five clean-patch runs passed 53 recorded requests plus the HTTP check.
Observed free physical VRAM stayed at least 347 MiB via NVML, and available
system RAM stayed above 4.30 GiB. The engine's separate CUDA free-memory warning
reported zero, so the NVML figure must not be read as a verified WDDM budget.
Linux and HIP runtime validation remains untested.
## Design reference
The batching idea follows NVIDIA NCCL's
[copy-engine implementation](https://github.com/NVIDIA/nccl/blob/12df1a11afad322be5a204a2db890161cbf8131d/src/ce_coll.cc)
and the [CUDA `cudaMemcpyBatchAsync` contract](https://docs.nvidia.com/cuda/cuda-runtime-api/group__CUDART__MEMORY.html).
The helper and tests here are new;
no NCCL source was transplanted. NCCL's multi-GPU performance claims are not
used as predictions for this single-GPU path.
+224
View File
@@ -0,0 +1,224 @@
# Several requests at once (batch slots)
By default Strata serves **one request at a time**: the others wait in the server's queue. With `"parallel": N`
(the engine's `--batch N`, also spelled `--slots N`) the engine keeps up to N conversations open and decodes them
**together**: every verify window then carries one token of each conversation, so the dense weights, the shared
expert, the head and every routed expert two conversations share are read once per window for all of them.
Combined with a layer split across several GPUs and `--batch-groups`, the cards also work on different
conversations at the same time instead of waiting for each other.
It is opt-in and changes nothing when the options are absent (#465; the engine part is PR #559).
## Turning it on
One GPU: add `"parallel": 2` to the model's config (`strata-<model>.json`) and restart, or run setup with
`--parallel 2`. Setup recommends it only where it does not cost speed (below); any number you ask for is kept as
asked, with a note when it is more than setup would recommend.
```
"parallel": 2
```
On one GPU with MTP (`--mtp` and `--spec`), `--batch-mtp` (in the config's `args`, or `STRATA_BATCH_MTP=1` in the
server's environment) lets each batch slot verify one MTP proposal per window. It is opt-in; without it the batch
behaviour described below is exactly the one without MTP. It needs VRAM per slot for the draft state and buffers, so
check the engine's free-memory log before using it on a smaller card. If it cannot run (one slot, no `--mtp`, a layer
split or helper GPU) the engine says so and batches as usual. RTX PRO 5000 owners measured +31% to +39% total
throughput with 2 to 4 clients (a RX R9700 run too); it has not been validated with a layer split.
With a layer split, the engine options go into the config's `args`:
```
"args": [ ..., "--batch", "8", "--batch-groups", "4", "--trim-stage-weights" ],
"layer_split": "12,24,36"
```
| Option | What it does |
| --- | --- |
| `"parallel": N` / `--batch N` / `--slots N` (2..8 normally) | up to N conversations have batch slots; more requests wait for a free slot. Each slot gets its own state (a session carved like the stage's own: GDN recurrence, QSA K/V and indexer, PLE history) on every GPU of the split. With grouped MTP, more than 8 slots can rotate through eight-row windows if memory permits. |
| `--batch-groups G` | with a layer split: the N slots in G groups that flow through the GPUs as a pipeline (GPU k runs one group while GPU k+1 runs another). G must divide N. 1 = all slots in one window, GPU after GPU. |
| `--trim-stage-weights` | with an **explicit** `--layer-split` (e.g. `12,24,36`, not `auto`): every GPU loads only the dense weights of its own layers instead of the whole model's (the same as `STRATA_STAGE_TRIM=1`, PR #639). The VRAM this frees goes to the expert cache. Useful without `--batch` too. |
The engine never refuses a count it cannot run: it says so in its log and runs what it can - at most 8 slots by
default (a window holds 8 rows), as many as fit in VRAM, or none (one request at a time) when not two fit. The server reads
the count the engine reports (`INFO batch_slots=N`), and `GET /v1/status` says it (`concurrency.serving`).
### What a slot costs, and what setup recommends
Every slot's session takes VRAM that the expert cache would otherwise hold: 0.56 GiB at a 32K context with 8-bit
KV, more with a longer context unless the KV cache streams (`--kv-resident`: then only the attention's 32K window
stays in VRAM, and each slot's whole KV cache takes pinned RAM - 1.6 GB at 128K). On a card whose experts mostly run
on the CPU, a batch also reads about as many distinct experts as the requests one by one (different conversations
route to different experts), so the gain is in **latency** (nobody waits for a whole answer), and a request alone
runs slower (the smaller expert cache): 11-24 % on a 12 GB card, see the measurements below.
So setup recommends `"parallel"` **only where the experts mostly fit in VRAM**: the expert cache (each card's VRAM
less ~5 GB, every card of a layer split counted) must still hold at least half of the model's experts beside the
slots, and the slots may take at most a fifth of it, up to 4 slots. With Q2_0 at 32K that is 3 slots on a 24 GB
card, 4 from 32 GB or on a split such as 2 x 16 GB; IQ3_S needs 32 GB or a split. Everywhere else (any 12 or 16 GB
card alone) it stays at one at a time and setup says: "parallel N reduces waiting for several users but costs
about 10-25% speed per request on this card". `--parallel N` is honoured as asked either way.
## How the server uses the slots
- **One request alone** runs on the usual solo path (verify windows with MTP drafts): the fastest single stream.
- **When a second request arrives**, the first is stopped (`STOP`) and continues in a batch slot with its prompt
plus what it generated so far - the engine's prompt cache holds exactly that, so nothing is read again - and the
new request is admitted next to it. By default, a request in a slot decodes **without MTP drafts** (one token per window).
With `--batch-mtp`, each slot verifies one MTP proposal alongside its current token.
- **A request left alone in a slot** (the others finished, nobody waits) goes back to the solo path: the slot is
stopped, the engine copies its sessions back and decodes with MTP drafts again (at most twice per request; with
`--prompt-cache 0` it stays in the slot; `STRATA_PARALLEL_SOLO=0` turns it off). The draft layer's own K/V was
built for another conversation then, but measured it accepted as many drafts (140 of 172) as a draft layer that
read the conversation (140 of 173).
- **More requests than slots** wait for a free one (`/metrics` -> `live.slots` shows each slot: idle, reading or
decoding, its tokens and tok/s; `live.running` the requests in flight).
- **Each admission** reads the request's prompt through the usual prompt path (prompt cache and conversation
checkpoints included) and produces its first token there; the state is then copied into the slot. Admissions
are taken one at a time, and **the slots decode between the prompt's chunks**: after each chunk (`--prefill`,
2048-8192 tokens) they decode for half as long as the chunk took (`STRATA_BATCH_DECODE_SHARE`, default 0.5), so
a long prompt slows the others down instead of stopping them. The chunks are the ones one uninterrupted read
takes, so the prompt's arithmetic is unchanged.
- **A long prompt gives way to a short one** (#656's cooperative preemption): when a request with a prompt under
half as long is waiting, the server sends `BYIELD`; at its next chunk boundary the long read stops, the part read
so far is copied into a slot, the short request is admitted, and the long one then goes on from its slot with the
same chunks (at most twice per request).
- **Each slot is a conversation cache.** A finished slot keeps what it holds (the prompt, the answer, and the
checkpoint at the prompt's last turn boundary); the next turn of that conversation goes to that slot and the
engine copies its state back (50-60 ms for a short conversation) instead of reading the history again - also for
a client that drops the reply's thinking from the history (the checkpoint matches up to the new turn). A new
conversation takes an empty slot, else the one used longest ago.
- Slots are assigned so that consecutive requests land in different pipeline groups (`--batch-groups`).
- A client that disconnects stops its slot (`BSTOP`); the others go on.
## Exactness
A batch row's arithmetic is the single-token window's, so with greedy decoding **every conversation of a batch
produces exactly the tokens it produces alone** - verified token by token for 8 concurrent conversations of 150
tokens, with and without the pipeline (`tools/batch_test.py`), and on one RTX 5070 for 4 conversations, for a long
prompt read while two others decode, for a prompt that gave way and went on, for a next turn continued from its
slot (from all it holds, and from its turn checkpoint without the reply's thinking), and for a request stopped in
its slot and continued on the solo path (`tools/batch_interleave_test.py`). These
settings make the comparison exact:
- `STRATA_IQ_MT_MIN=1` (the multi-token CPU expert kernels for every group, as for the solo path's own
exactness tests: by default an expert's rows round differently alone than in a group, so the output depends on
how many rows of a window share an expert - which differs between a batch and a request alone),
- `--pcie-frac 0`: the PCIe share of the missed experts is chosen per window from the window's misses, so the
same expert can run on the GPU in one window and on the CPU in another, which rounds differently, and
- `--adapt-every 1000000` (the VRAM tier fixed), and `--no-prefill-borrow` while a prompt is read beside decoding
slots: their windows then see the expert cache without the slots the prompt borrowed (those experts run on the
CPU).
With the default settings the outputs stay coherent but drift apart after some tokens, as two solo runs whose
windows differ can. Measured on the RTX 5070 (Q2_0, 4 conversations of 200 tokens, `tools/batch_test.py` without
the settings above): one equal to its solo run, the others apart from token 0, 99 and 174 - the first token already
differed for one, because the adaptive VRAM tier had moved experts between the solo runs and the batch; all four
texts read as well as their solo runs.
Sampled requests (temperature, top_p, top_k, min_p, seed) are drawn row by row with the solo window's
counter-based draw (Philox(seed, position)).
## Limits (for now)
- By default, batch windows carry no MTP drafts: a conversation in a slot decodes one token per window (the solo
path keeps its drafts, which is why a request alone is not put in a slot, and goes back to it when left alone).
- Grouped MTP currently uses one proposal per slot and requires one GPU; it does not support a layer split.
- Repetition / frequency / presence penalties are not applied in batch windows.
- A prompt shorter than one chunk is read in one piece (the slots wait for it); a read gives way only at a chunk
boundary, and not for pictures.
- Admissions are one at a time: two new long prompts are read one after the other.
- `--batch-groups` needs every stage on its own GPU; a pipelined slot is not kept as a conversation cache.
- The slot sessions take VRAM (above) and, with KV streaming, pinned RAM.
## Measured
One RTX 5070 (12 GB), Ryzen 5 7600, 64 GB DDR5, Q2_0, 32K context, through the HTTP server: C different requests
sent at once (an 800-word essay each, 256 tokens per answer, greedy, thinking off), median of 3 rounds:
| Concurrent | Setting | Total tok/s | vs one at a time | Per request tok/s | First token: median / last of the round |
| ---: | --- | ---: | ---: | ---: | ---: |
| 1 | one at a time (default; what setup recommends on this card) | 74.3 | - | 83.6 | 0.4 s / 0.4 s |
| 1 | `"parallel": 2` | 67.3 | -9 % | 74.8 | 0.4 s / 0.4 s |
| 1 | `"parallel": 4` | 57.9 | -22 % | 63.8 | 0.4 s / 0.4 s |
| 2 | one at a time | 71.6 | - | 77.3 | 2.1 s / 4.1 s |
| 2 | `"parallel": 2` | 61.0 | -15 % | 32.6 | 0.5 s / 0.9 s |
| 2 | `"parallel": 4` | 54.6 | -24 % | 28.7 | 0.6 s / 0.7 s |
| 4 | one at a time | 70.7 | - | 79.6 | 6.0 s / 11.2 s |
| 4 | `"parallel": 2` | 61.2 | -13 % | 32.2 | 4.5 s / 9.3 s |
| 4 | `"parallel": 4` | 63.1 | -11 % | 16.9 | 1.0 s / 1.8 s |
On this card the slots buy **waiting time, not speed**: the fourth of four requests starts after 1.8 s instead of
11.2 s, but together they decode 11-24 % slower than one after the other, and a request alone loses 11 % (2 slots)
or 24 % (4 slots), because the slots' sessions (0.56 GiB each) come out of the expert cache and most experts run
on the CPU: a batch window over 4 conversations reads 24 CPU experts per layer against ~8 for one, so it costs about
what the 4 tokens cost one after the other (`strata batch:` in the engine log: 54 ms per 4-row window, ~20 ms per
1-row window). This is why setup leaves a 12 GB card at one at a time. Cards that hold most experts in VRAM, and a
layer split, are where the slots also add speed (below).
A 4-GPU layer split (4 x 16 GB, PCIe Gen3), IQ3_S, `--batch 8 --batch-groups 4 --trim-stage-weights`, through
the HTTP server, 400 tokens per answer, temperature 0.7 (PR #559):
| Concurrent requests | Per request | Total |
| ---: | ---: | ---: |
| 1 | 123 tok/s (solo path) | 120 tok/s |
| 2 | 57 tok/s | 113 tok/s |
| 4 | 51 tok/s | 205 tok/s |
| 8 | 45 tok/s | 360 tok/s |
With the patches below on engine 0.1.38 and parking on, through the service: 8 requests at temperature 0 -> 369
tok/s, at 0.7 -> 358 tok/s.
`--trim-stage-weights` alone raised the share of experts held in VRAM on that machine from 76-85 % to 84-100 %
per card.
## Together with conversation parking
`--conversation-cache-mib N --conversation-cache-slots K` (DETAILS.md) works with the layer split too: a request
whose conversation was parked is restored on every stage before its admission, so an agent and its sub-agents, or
several chats that alternate, come back without reading their history again. Measured on the same 4-GPU split,
two long conversations alternating through the HTTP server: the first turns took 3.9 s and 5.7 s to the first token
(their prompts read), the follow-ups 0.53 s and 0.46 s.
## Testing
Four scripts drive a built engine or a running server; each exits non-zero on a failure. `serve/test_parallel.py`
tests the server's side with a scripted engine (no GPU).
| Script | What it checks |
| --- | --- |
| `tools/batch_test.py` | the same prompts alone (`GEN`) and together in the batch slots (`BGEN`): every slot's greedy tokens equal its solo tokens; prints the aggregate rate. `--batch-groups` in `--extra` tests the pipeline, `--keys "temperature=0.7"` the sampled rows. |
| `tools/batch_interleave_test.py` | a long prompt read while two slots decode, a prompt that gives way (`BYIELD`) and goes on, and a next turn continued from its slot: each equal to its solo tokens. |
| `tools/parking_test.py` | a follow-up to a conversation decodes the same tokens whether its state stayed live or came back from the parking cache (with a layer split: every stage's image). |
| `tools/early_close_test.py` | a client that stops reading a streamed answer early (alone, and with a second request running) does not leave its tokens to the next request (server). |
For exact comparisons pass `--pcie-frac 0 --adapt-every 1000000` (and the scripts set `STRATA_IQ_MT_MIN=1`):
```
python3 tools/batch_test.py --exe engine/strata --config strata-<model>.json --batch 8 --n 8 \
--extra "--layer-split 12,24,36 --trim-stage-weights --batch-groups 4 --pcie-frac 0 --adapt-every 1000000"
python3 tools/batch_interleave_test.py --exe engine/strata --config strata-<model>.json \
--extra "--pcie-frac 0 --adapt-every 1000000 --no-prefill-borrow"
python3 tools/parking_test.py --exe engine/strata --config strata-<model>.json \
--extra "--layer-split 12,24,36 --conversation-cache-mib 8192 --conversation-cache-slots 4 --pcie-frac 0"
STRATA_KEY=<key> python3 tools/early_close_test.py http://127.0.0.1:8080
```
## Engine protocol (`--serve`)
On top of `GEN` / `GENI`:
| Line | Direction | Meaning |
| --- | --- | --- |
| `BGEN <slot> <max_new> [keys] <ids>` | in | read the prompt (as `GEN 1`), then continue in `<slot>` |
| `BGENI <slot> <max_new> [keys] <file> <ids>` | in | the same with images |
| `BADM <slot> <1/0>` | out | after the admission's `DONE`: 1 = it continues in the slot, 0 = it ended |
| `BT <slot> <id>` | out | a token of that slot |
| `BDONE <slot> <generated> <stop/length/cancel> <ms>` | out | the slot is free again (it keeps its conversation) |
| `BSTOP <slot>` | in | end that slot at its next window |
| `BYIELD <slot>` | in | the prompt being read gives way at its next chunk boundary; its part read waits in `<slot>` (the admission's own, or a free slot for a solo request) |
| `YIELDED <slot> <tokens>` | out | before the `DONE cancel` of a read that gave way: the request is sent again later and goes on from there |
| `INFO ... batch_slots=N` | out | the slots the engine runs (only with `--batch`) |
`tools/batch_test.py` drives the engine directly: the same prompts alone, then together, compared token by token,
and the aggregate rate.
+6
View File
@@ -15,6 +15,12 @@ and their limits. Report what you actually measured and label estimates separate
- [2026-09-30: RTX 5090, Core Ultra 9 285K, 64 GB RAM](../bench/results/2026-09-30-community-rtx-5090/README.md):
Strata 0.1.29, original Flash-Next IQ2_XS, 131,072-token context; three runs
each at 4,096, 32,768, and 128,000 prompt tokens, plus six recall checks.
- [2026-10-03: 2x AMD Instinct MI50 16 GB (gfx906), Xeon E5-2666 v3, 32 GB RAM](../bench/results/2026-10-03-community-2x-mi50/README.md):
the gfx906 build (#638) with #639 and #640, Coder IQ1_M, 131,072-token context, layer split across both cards;
three runs each at 4,096, 32,768, and 128,000 prompt tokens, plus six recall checks.
- [2026-10-04: 2x Intel Arc Pro B60 24 GB, Ryzen 5 5600, 64 GB RAM](../bench/results/2026-10-04-community-2x-arc-pro-b60/README.md):
the SYCL port at `6f32ec0` with two fixes, Coder IQ1_M and Flash-Next IQ2_XS, 8,192-token context, layer split across both cards;
short prompts and 2K-token prompts, plus one run on a single B60. Intel's SYCL engine, no 4,096/32,768/128,000-token sweep.
## What to record
+404 -27
View File
@@ -71,7 +71,7 @@ attention reads in VRAM (`--kv-resident 32768`), so more experts fit on the GPU.
(1,589 -> 3,872 experts in VRAM); at 128K about +6%. The attention reads exactly the same values (only where the KV lives changes); it
costs ~13.7 KB of RAM per context token (1.7 GB at 128K). Existing installs: run `START-HERE.bat --setup` once to turn
it on. Setup turns it on when the RAM has room for it; `--kv-streaming on|off` overrides that (on past the RAM test with a
note; never with `--kv k8v4` or under WSL, which cannot stream).
note; never under WSL, which cannot stream).
**4-bit KV cache (engine 0.1.8, optional):** `START-HERE.bat --setup` asks above 8K context (or pass `--kv q4_0`). It
halves the KV cache's memory with a Hadamard rotation before 4-bit rounding (PR #21), about 4% faster at 128K, but it
@@ -81,8 +81,8 @@ Details: [`bench/results/2026-09-27-kv-q4`](../bench/results/2026-09-27-kv-q4/RE
**Hybrid K8V4 KV cache (engine 0.1.25, optional, PR #120):** `--kv k8v4` (`START-HERE.bat --setup --kv k8v4`) keeps
the keys at 8 bits and stores the values as rotated 4-bit: 23% less KV memory than 8-bit, so more experts fit in
VRAM. RTX 3090, the Coder at 198K context: 99 instead of 85 tokens/s output, the same needle results, prompts 2-5%
slower. It does not stream its KV cache (KV streaming is on by default from 64K), so it pays off mostly on large
cards at long contexts.
slower. It streams its KV cache like the other formats (`--kv-resident N`): on an RTX 2060 SUPER 8 GB at 128K with
20,480 resident cells, +780 expert slots over resident K8V4, and it scores better than 4-bit KV on long documents.
**Reproducible greedy output (0.1.30, opt-in, `STRATA_IQ_MT_MIN=1`):** with the IQ models, the CPU computes an
expert for one token with ggml's dot product and for several tokens with Strata's multi-token kernels, which round
@@ -90,7 +90,9 @@ slightly differently. How many tokens share an expert depends on the drafts in a
at temperature 0 can end in a different (equally good) answer when the drafting, the cache state or a resumed
conversation differ (issue #152). `STRATA_IQ_MT_MIN=1` (in the config's `env`) uses the multi-token kernels for
every group: the answer then no longer depends on the drafting. Measured on a Ryzen 7600 (AVX-512): IQ3_S decode
-1..-3%, the other models the same; the default stays the fastest rule. Through the server, two more things carry
-1..-3%, the other models the same; the default stays the fastest rule. On an Intel CPU of Alder Lake or later
without AVX-512, where the AVX-2 kernel gathers the IQ3_S grid, `STRATA_IQ3S_MT1=1` (opt-in) gives IQ3_S the multi-token
kernel for one token, which is the faster one there; it changes a lone token's rounding, so it is off by default. Through the server, two more things carry
over from one request to the next (#410): the adaptive tier moves experts between RAM and VRAM (the GPU and the CPU
round an expert differently), and the prompt cache resumes a repeated prompt and reads only its tail through the
decode path. For byte-identical repeats add `--prompt-cache 0 --adapt-swaps 0 --pcie-frac 0` to the engine's args
@@ -107,11 +109,21 @@ English/code subset from before (40,525 ids, ~110 MiB less VRAM, English answers
almost no drafts). `--draft-vocab cyrillic` takes the English/code subset plus the whole Cyrillic script (58,963
ids): the shipped subsets hold 142 of the vocabulary's 18,580 Cyrillic tokens, so Ukrainian or Russian answers got
1.4 tokens a round; with it 2.1, and 83 -> 109 tokens/s (RTX 5090, the NVFP4 fork), English unchanged.
`--draft-vocab fr` (0.1.39, #597) takes the English/code subset plus the 5,686 tokens that cover 99% of a French
Wikipedia corpus (46,211 ids, `tools/draft_vocab.py --corpus`): 23.6% of French text's tokens were outside the
English/code subset, 0.7% are outside this one. Drafts accepted in French answers 0.51 -> 0.60 (IQ3_XXS, RTX 5070,
8 prompts x 2 passes; English 0.61 -> 0.63 and code 0.77 -> 0.78, no loss), and 141 -> 158 tok/s in French on an
RTX 5090 (IQ3_S, the reporter's measurement).
`tools/draft_vocab.py` builds and inspects subsets. When the start stops with "the draft head does not fit" (a
12 GB card with a long context, #474), the engine says how much the head needs, how much VRAM is free and which
smaller subset fits, and the server's start error repeats it; setup suggests `--draft-vocab en` on cards under
14 GB (only a suggestion: nothing changes unless you pass it).
**Serving without the draft layer (`--mtp` is optional):** `serve` runs without `--mtp`. Drafts then come from the
suffix/prompt-lookup drafter only (or one token per round), every token is still verified against the model, and the
draft layer's VRAM (~0.7-1 GiB with its head) goes to the expert cache: 1,000 -> 1,678 slots in one A/B on an 8 GB
card. The conversation cache (`--conversation-cache-mib`) stays on: a parked conversation then carries no draft K/V.
**Low-RAM mode (engine 0.1.26, chosen by setup):** normally all of a model's experts are copied into RAM (23-50 GB,
pinned) and the GPU holds a copy of the most-used ones. On a PC whose RAM cannot hold them beside the system (the
experts plus ~10 GB), setup instead maps them from one file in the model's folder (`--mmap-experts`, the pack's
@@ -131,6 +143,15 @@ other ~18 GB), a 32 GB PC with a 12-16 GB GPU the Coder; IQ3_XXS on a 32 GB PC s
as far as the RAM allows, their experts are kept in RAM too (so a prompt reads nothing from the SSD either).
- The cache still follows the conversation (`--adapt-every`): a swap copies the evicted expert back from VRAM into the
RAM place of the one that replaces it, so the RAM copy keeps holding exactly what the GPU does not.
- `--adapt-async 1` (opt-in, `--serve`): the swaps of a round advance between decode windows on a helper thread
(copy back, copy in, move into RAM) instead of one window waiting for the whole round. Not with `--batch` or
`--peer-device` (the blocking tier runs there). It is not bit-exact from run to run: which window first computes a
swapped-in expert on the GPU (which rounds differently from the CPU) depends on when its copy lands.
It stays on the blocking tier (said in the log) when the exchange buffers are not page-locked, and with
`--pipeline-windows`; the stats line reports ms per round.
- `STRATA_EXCHANGE_ROTATE=1` (opt-in): an adaptive swap hands buffer ownership over instead of copying the evicted
blob into the RAM copy (equal-size blobs, fully page-locked copy). Same tokens, fewer host copies; it works with
`--adapt-async 1` too. Details and the measurement: [EXCHANGE_ROTATION.md](EXCHANGE_ROTATION.md).
- The answers are the plain mapped mode's for the same expert placement: the bytes are the file's. With a page-locked
copy the GPU also takes its usual share of the misses over PCIe (`--pcie-frac`), as with enough RAM; `--pcie-frac 0`
(or `STRATA_RESIDENT_PIN=0`) gives the mapped mode's exact tokens.
@@ -143,7 +164,25 @@ other ~18 GB), a 32 GB PC with a 12-16 GB GPU the Coder; IQ3_XXS on a 32 GB PC s
(`--mmap-experts`): the cards together hold more of the experts, and two users measured it 1.3-1.6x faster than
one card, but the OS file cache can fill the RAM to 0 free during long prompts. `--yes` keeps one GPU. A config
with `--resident-experts` started with `--gpus` switches to `--mmap-experts` with a note, and the engine runs that
pair as `--mmap-experts` with a warning instead of refusing it.
pair as `--mmap-experts` with a warning instead of refusing it. **Since 0.1.40 (#642, #848)** setup requires an
engine that runs the resident variant on a split, so it keeps both cards, resident, as for any config:
[MULTI_GPU.md](MULTI_GPU.md#using-it).
**A mapped arena for small RAM (Linux, opt-in, 0.1.39, PR #640):** `STRATA_ARENA_MMAP=1` maps a native pack's expert
arena read-only from the pack's `experts.bin` instead of reading it into locked RAM, for a PC whose GPUs hold most
experts but whose RAM is small (2x 16 GB GPUs with 32 GB of RAM: ~1 GB -> 25 GB available while serving). The first
start writes `experts.bin` (when the drive has room for it), later starts map it; the pages of the experts a GPU holds
are handed back to the OS. Run it with `--pcie-frac 0` (the GPUs get no mapped alias). Without the variable nothing
changes.
**Releasing mapped expert pages on Windows (opt-in):** `STRATA_FILE_RELEASE=1` lets `FileExpertSource` trim the
full file-backed pages of experts after their GPU uploads complete, including the slots lent to prefill and then
refilled. It works with `experts.bin` and the direct GGUF views; shared boundary pages and private/pinned buffers
are left alone. Unset or `0` keeps the previous behavior. On one 32 GB Windows 11 laptop with an RTX 4080 Laptop
and a Thunderbolt RTX 3090, IQ2_XS with `--mmap-experts --layer-split 12 --trim-stage-weights` raised median available
RAM from 0.51 to 12.19 GiB across 1K/4K/16K prompt trials, with 3.7-4.4% lower prefill throughput. It did not reduce
committed memory or physical SSD reads. This is a working-set hint, not an unmap or a guarantee that the OS drops
its file cache. See the [configuration, measurements and limits](../bench/results/2026-10-04-windows-mapped-release/README.md).
**Low-RAM mode without `experts.bin` (engine 0.1.31):** for the native packs (IQ2_XS, IQ3_XXS, IQ3_S, the Coder, Swift,
Q2_0 packed by `tools/iq_pack.py`; not the canonical Q2_0 pack setup makes for AVX-512 CPUs) the mapped mode no longer
@@ -157,22 +196,64 @@ engine fetches a layer's missing experts on 8 threads (`STRATA_FETCH_THREADS`) w
**A RAM budget (engine 0.1.31, `--resident-budget-gib N`):** the resident variant for a model whose experts do not all
fit: the N GiB of experts the GPU cache does not hold that the expert profile ranks hottest are copied into RAM at
start (locked; page-locked when the driver allows the whole budget), and the rest are read from the files through the
OS file cache. It implies `--mmap-experts` and leaves 4 GB of free RAM (a larger N is clamped to that less 256 MiB,
with a message; #403: a clamped budget no longer fails the safety check that follows, and a budget that cannot be
kept at all is a warning, with every expert read from the files). Setup sets N with `--resident-budget-gib N`. With
start (locked; page-locked when the driver allows the whole budget), and the rest are read from the files.
It implies `--mmap-experts` and leaves 4 GiB of headroom. On Windows, available commit capacity also
limits the budget; a larger N is clamped to the smaller limit less 4 GiB and a 256 MiB margin, with a message.
A clamped budget no longer fails the safety check that follows (#403). A budget that cannot be kept at all is a
warning, with every expert read from the files. Setup sets N with `--resident-budget-gib N`. With
the GGUF read in place it also warms the next layer's likely experts: while the CPU works on a layer, a thread applies
the next layer's router to this layer's input and asks the OS for the pages of the predicted experts that neither the
GPU nor the RAM budget holds (only pages - the experts computed are the same; `STRATA_LOOKAHEAD=0` turns it off). This
is what runs [Unsloth's UD-Q4_K_XL](UNSLOTH_Q4.md) (72 GiB of experts) on a 64 GB PC: 7-8.5 tokens/s at N = 40 on an
RTX 5070, against ~3 tokens/s before these changes.
**Switches added in 0.1.40 (all off unless noted; none changes the default output):**
- `--kv-grow` (or `STRATA_KV_GROW=1`; `--no-kv-grow` turns it off): the K/V takes VRAM only for the cells the requests
reach, and the expert cache holds the rest, giving slots back as the context grows. It needs one GPU, an expert
profile, the whole K/V in VRAM (no KV streaming) and every expert in RAM (not the resident low-RAM mode); otherwise,
and with `--batch`, `--vram-elastic` or `--peer-device`, the engine says so and stays off.
- `--host-core last` (or `STRATA_HOST_CORE=last`, Windows): the host thread runs on the last physical core and the
workers take the first. Windows sends a GPU's interrupts to the first core, where a host spinning on the GPU's flags
waits for them (`--host-core first` is the default; the startup log names the cores).
- `STRATA_ADAPT_LAG=2` (#764): a window takes the adaptive tier's swaps once they are two windows old (default 1,
as 0.1.39). `STRATA_PREFILL_EQUAL=1` (#693): a prompt segment is read in chunks of equal size, not full chunks and a
short last one (changes the rounding). `STRATA_OWNED_PRICE=exact` (#796): the cache sizing prices the prompt path's
own buffers by their real allocation instead of 0.1.39's rule.
- `STRATA_Q2_BITPLANE=1`: a bit-plane row kernel for Q2_0 on CPUs with AVX2 and no AVX-512 (changes the last bits).
`STRATA_NO_AVXVNNI=1` turns the AVX-VNNI forms of the i-quant, IQ4_NL and Q2_0 rows off on CPUs that have them
(Alder Lake, Sapphire Rapids and later); they give the same bits as the AVX2 forms, 4-25% faster rows.
- Draft layer: `--lookup-chain-min M` (the shortest context match `--lookup-chain` extends, default 3),
`--mtp-hnorm stream` (one norm per stream, as llama.cpp's MTP graph) and `--mtp-draft-vocab FILE` (a token subset
of your own instead of `<mtp>/draft_vocab.bin`).
- Elsewhere: HIP `STRATA_DENSE_MMQ=1` and `STRATA_HIP_ADAPT_KERNEL_COPY=1` in [AMD_HIP.md](AMD_HIP.md#model-and-serving-configuration);
`STRATA_KEEP_EMPTY_TURNS=1` and `STRATA_TOPK_STREAM=0` in [TROUBLESHOOTING.md](TROUBLESHOOTING.md).
**Read-ahead at start (Linux):** the weights, the native dense matrices, the GPU cache's fill from the profile, the
resident RAM copy and the MTP draft files are asked for ahead of their reads (madvise / posix_fadvise WILLNEED in
128 KiB steps), so the drive sees a deep queue instead of one page fault at a time. Measured on a Gen3 NVMe (RTX 5090,
32 GB, Q2_0 resident at 262K): ready in 70 s instead of ~920 s; the fill went from 39 MB/s to 3.2 GB/s.
`STRATA_READ_AHEAD=0` turns it off; `STRATA_FILL_AHEAD=N` sets how many fill pairs are asked for ahead (default 256).
**How much came from where:** with `--stats` the engine prints the tiers of the decode (`expert tiers`: blobs from the
RAM copy, blobs and MB from the files, the time spent reading them; `routing prefetch`: how many of the file reads had
been warmed). The server log has the same per request (`expert tiers: GPU ... hits ...; RAM ... blobs, files ...
blobs ... MB read`), and `GET /metrics` lists `ram_blobs`, `file_blobs` and `file_mb` for each recent request (with
engine 0.1.31 or newer). It also lists each request's speculative drafts, `drafts_offered` and `drafts_accepted`
(`null` when the engine did not report them), and their sums since the server started in `totals` (#457).
Its `hit_rate` is the VRAM share of the experts looked up while answering: experts the GPU reads over PCIe
(`--pcie-frac`) are not in it, so a higher `--pcie-frac` raises it even when decoding gets slower. `pcie_share`
(engine 0.1.39 or newer, #588) is their share of all routed experts, and the server log and the Monitor tab show it
beside the hit rate.
**Where a decode window's time goes (profiling, #610):** start the server with `STRATA_DECODE_TIMING=1` (and
`STRATA_VERIFY_PROFILE=1` for the GPU's side) in the environment. After each request the engine log then has one
`strata decode timing:` line - windows, tokens per window, and per window the verify time split into the wait for the
GPU, the CPU expert pool (plan, activation quantization, jobs) and the stage, plus commit and draft - and one
`strata decode GPU stages (ms/window):` line with the GPU time of each stage (GDN and QSA layers, the VRAM expert
hits, the router, the head, ...). The GPU profile times every stage with events, so it slows the decode a little:
use it to compare, not to measure speed. This works with every pack; `--gpu-stages` (a one-token replay of
per-layer graphs) refuses a native (IQ) pack, which has no such graphs.
Time to first token is prompt length / prompt speed: with Q2_0 about 4 s at 4K, 25 s at 32K, under 2 minutes at 128K
and 4.5 minutes at 262K (engine 0.1.13 made long prompts about twice as fast, below).
@@ -208,7 +289,9 @@ card gains depends on its PCIe link (the experts stream over it), so they are st
More VRAM matters more than a faster GPU: every extra GB holds ~700 more experts, and every expert on the GPU is one the
CPU does not have to compute. A 3090's 24 GB takes most of the CPU work away. (Since 0.1.14 the expert profile ranks
all 24,576 experts; before, the cache stopped at 8,000, about 10-14 GB. `tools/make_profile.py` builds a profile from
your own prompts: run the engine once with `--dump-routing trace.bin`, see the tool's help.)
your own prompts: run the engine once with `--dump-routing trace.bin`, see the tool's help. The shipped profile already
ranks every pair, so a trace only changes the order with `--reorder` (0.1.39, #587): your trace's pairs first, then the
base's, then the rest.)
## Which model?
@@ -276,8 +359,8 @@ Everything else is installed for you the first time.
| --- | --- |
| GPU | NVIDIA **RTX 20, 30, 40 or 50 series**, **12 GB VRAM or more** (8 GB runs, slowly). Measured on an RTX 5070 and an RTX 3090; RTX 20 (Turing, since 0.1.27) was tested by a contributor on an RTX 2070. Or AMD **Radeon RX 7900 XT / XTX, RX 7800 XT / 7700 XT, RX 9060 XT, RX 9070 / 9070 XT, Radeon AI PRO R9700, RX 6800 / 6900 series**: [AMD_HIP.md](AMD_HIP.md). |
| RAM | **64 GB** recommended (see the table above). |
| CPU | x86-64 with AVX2 (any Intel/AMD desktop CPU from the last ~8 years). AVX-512 (Ryzen 7000/9000) is a bit faster. |
| Disk | ~70-80 GB free for the model, ~6 GB for the MTP layer (+1 GB with images). **Q2_0 on an AVX-512 CPU** also writes a one-time ~40 GB copy of its experts for the fast CPU kernel. An NVMe SSD is strongly recommended. |
| CPU | x86-64 with AVX2 (any Intel/AMD desktop CPU from the last ~8 years). AVX-512 (Ryzen 7000/9000) is a bit faster. Older CPUs without AVX2 are experimental and slow: [Older CPUs](INSTALL.md#older-cpus-experimental). |
| Disk | ~70-80 GB free for the model, ~6 GB for the MTP layer (+1 GB with images). **Q2_0 on an AVX-512 CPU** also writes a one-time ~40 GB pack for the fast CPU kernel (34 GB of experts in its layout, plus the dense weights). An NVMe SSD is strongly recommended. |
| OS | Windows 10/11, or Linux (Ubuntu 22.04/24.04 get everything installed automatically). |
What the first start installs: in this folder `.venv/`, `engine/` and `third_party/`; the model files (`models/`,
@@ -414,6 +497,18 @@ the OS file cache, so loading again takes seconds while that RAM is not needed e
16 GB with Q2_0 in the low-RAM mode: unloading takes ~0.3 s, and a request to an unloaded model answered after
4.6 s (text) or 14.7 s (a picture, image encoder on the CPU).
**Giving part of the VRAM back while it keeps serving (#533, opt-in, one NVIDIA GPU).** With `"vram_elastic": true`
in the config (the engine flag `--vram-elastic`), the expert cache is allocated in 512 MiB segments
(`"vram_segment_mib"`), and `POST /v1/vram` with `{"reserve_mib": 8000}` shrinks it between requests until that much
VRAM is free for another program; `{"reserve_mib": null}` grows it back towards its full size, keeping the reserve
the engine started with (`--vram-reserve-mib`), and `{"reserve_mib": 0}` takes all of it back. A request that is
running finishes first. The experts of the segments given back are computed on the CPU, like any expert outside the
cache, so answers keep coming, slower; growing back puts the same experts in the same slots. Nothing resizes on its
own. Not with a layer split, the helper caches, `--peer-device` or the resident low-RAM mode. Measured on an RTX 5070
12 GB with Q2_0 (4.8 GiB cache): `{"reserve_mib": 6000}` took 78 ms and freed 4.3 GiB (the cache keeps 0.5 GiB for
the prompt path), decode 44 -> 33 tok/s; growing back took 92 ms and the answers were token for token the ones before
the shrink. Without the flag nothing changes (the same answers as without it).
**Keep what the expert cache learned across restarts (opt-in, engine 0.1.36, #477):** a start fills the GPU's expert
cache from the shipped profile, and the adaptive tier (`--adapt-every`) then moves in the experts your requests use.
With `"expert_profile_save": "expert-profile-learned.bin"` in `strata-<model>.json` the engine saves that as a
@@ -435,9 +530,11 @@ The server listens on `http://127.0.0.1:8080` (change with `--port` in setup, or
| --- | --- |
| OpenAI Chat Completions (stream and non-stream, tools) | `POST /v1/chat/completions` |
| Anthropic Messages (stream and non-stream, tools) | `POST /v1/messages` |
| OpenAI Responses (stream and non-stream, tools; stateless, [below](#the-responses-api-and-codex-cli)) | `POST /v1/responses` |
| Model list / health | `GET /v1/models`, `GET /models`, `GET /health` |
| Model properties | `GET /props` (also accepts `?model=<loaded-model-id>`) |
| What the model is doing right now | `GET /status`, `GET /slots` (single slot, busy or idle) |
| Save / restore the conversation to a file (session files, below) | `POST /slots/0?action=save\|restore` |
| Everything the Monitor tab shows (engine, live state, last requests, hardware) | `GET /metrics` |
| The MCP servers, their state and tools ([below](#tools-from-mcp-servers)) | `GET /mcp` |
@@ -473,6 +570,32 @@ print(r.choices[0].message.content)
part of the thinking the client sees and counts as output tokens. `"reasoning_budget_tokens": N` in
`strata-<model>.json` sets it for every request; a request's own value wins, and `0` means no budget. Off by default;
Anthropic's `"thinking": {"budget_tokens": N}` still only chooses the level, as above.
- **A reply stuck on one token is ended (0.1.39, #606).** When a reply repeats the same token 256 times in a row, the
server ends it there with `finish_reason` `"length"` and says so in its window: a model in a loop, or a broken
state that answers one token forever (#606 saw 36,689 tokens of `!`). `"repeat_stop_tokens": N` in
`strata-<model>.json` sets the run length; `0` turns it off (for a request that really wants one token many times).
- **Repeated reasoning (opt-in, #728).** The single-token guard above does not see a model that repeats whole
passages. `"reasoning_loop_recovery"` in `strata-<model>.json` is `false` (the default), `"stop"` or `"recover"`
(`true` means `"recover"`). Every 512 output tokens, at a complete character and parser boundary, the reasoning is
measured over its last 2,000 words and punctuation marks (counting passages over the last 30,000 words). If at
least 25% belong to 12-word passages seen three times, `"stop"` ends the reply there as `"length"` and says so in
the server window. `"recover"` stops and drains that generation, then goes on once from all its generated token
ids with the template's low-effort sentence in place of the xhigh one in the first system message (a splice of
token ids: the rest of the prompt is not decoded or re-encoded). The task stays the same; no answer or `</think>`
is inserted, and both passes share the original output limit. On recovery only, temperature is raised to at least
1.0 and presence penalty to at least 1.5; a client's top-p, top-k and seed are never changed. `/metrics` records
`reasoning_recoveries` and the coverage. This is a policy, not a numerical engine fix or a guarantee of an answer.
`"recover"` needs the exact xhigh sentence in the first system message and skips requests with images; re-reading
the changed prefix costs prompt time. Either mode can mistake repeated useful code or checks for a loop, so keep
the recorded answer quality alongside the completion rate when testing it.
- **Changing the effort without re-reading the prompt (opt-in, 0.1.39, #458).** The effort's instruction is the
first thing in the prompt, so a request that only changes the effort (an agent's "think harder" switch, `none` for
a quick tool step) reads the whole conversation again. `"effort_position": "end"` in `strata-<model>.json` renders
every request's prompt start as the default effort's and puts a `low` / `medium` effort in a short system turn
right before the answer (no thinking: the empty thinking block, as always); the engine (0.1.39+, which the server
checks) keeps its checkpoint in front of that turn, so the next request reuses the conversation whatever its
effort. The default (`"start"`) prompt is unchanged. The model sees a level that is not the default in another
place than it was trained with; how well it follows it there is not measured yet.
- **Anthropic requests that don't ask for thinking (opt-in, 0.1.32, #278).** By default a `/v1/messages` request
with no `"thinking"`, effort or budget thinks as the model's template does. `"anthropic_thinking": "on_request"` in
`strata-<model>.json` renders such a request without thinking - Anthropic's own rule, and what Claude Code's short
@@ -482,6 +605,15 @@ print(r.choices[0].message.content)
long prompt the stream sends keep-alives, so agents do not time out; the server window prints progress every
15 s, and `GET /status` says what it is doing (`reading the prompt`, `answering`, tokens so far). Closing the
connection or pressing stop in your app really stops the model, so the next request starts at once.
- **Prefill progress in the stream (opt-in).** `"return_progress": true` puts that progress on the stream instead of
sending only the keep-alive, as one extra field on a chunk with an empty delta: `prompt_progress` with `total`,
`cache`, `processed` and `time_ms`. Those are llama.cpp's four fields and mean the same there (`time_ms` is the time
since the prompt started reading, and the work still to do is `(total-cache) - (processed-cache)`), so a client that
draws a prefill bar for llama.cpp draws one here too. It is off unless the request asks, as it is in llama.cpp.
The engine says one line per `--prefill` chunk, so that chunk is the step: measured on two RTX 3090s with
`--prefill auto` (8192 tokens), a 42,131 token prompt read in 15 s sent six of them. A prompt shorter than one chunk
sends nothing, and that is on purpose: its only line arrives once the prompt is read, because the last tokens go
through the verify windows rather than the batched path, so it stops up to `--short-read` tokens short of the end.
- **Chat apps.** Any app with an "OpenAI-compatible" provider works: base URL `http://127.0.0.1:8080/v1`, any API key.
- **OpenCode** (#543). A starting point for `opencode.jsonc` (in your project, or `~/.config/opencode/`); the field
names are OpenCode's, so check its config docs if your version differs:
@@ -520,6 +652,7 @@ print(r.choices[0].message.content)
- **Claude Code** (Strata 0.1.17 or newer): set `ANTHROPIC_BASE_URL=http://127.0.0.1:8080` and
`ANTHROPIC_MODEL` to a Claude model name it knows (it refuses names it doesn't; Strata ignores the name), plus any
`ANTHROPIC_AUTH_TOKEN` (or your `api_key`, if you set one).
- **Codex CLI** (0.1.39): see [the Responses API](#the-responses-api-and-codex-cli) below.
- **Context.** Chosen in setup (8K-262K). Requests longer than that are refused, never silently cut. A request whose
`max_tokens` would run past the context is refused too (400); agents that always ask for their full output cap
can instead get it shortened to the room left: add `"fit_max_tokens": true` to `strata-<model>.json` (or pass
@@ -527,6 +660,14 @@ print(r.choices[0].message.content)
- **Model aliases** (0.1.32). `"aliases": ["qwen", "local-model"]` in `strata-<model>.json` lists the model under
those names too in `/v1/models` (each with its own `id`, and in the model's `aliases`), like llama-server's
`--alias`; a request naming one is answered under that name. Any other name is still served, as before.
- **Model settings in the web page (0.1.39, #564).** The About tab's Model settings card shows and changes a few of
the keys above in `strata-<model>.json`: the `sampling` defaults (temperature, top_p, top_k, min_p),
`reasoning_budget_tokens`, `fit_max_tokens`, `anthropic_thinking`, `effort_position`, `aliases`, `idle_unload_s`,
`lazy_load`, `engine_silence_s`, `api_monitor`, `open_browser` and `--vram-reserve-mib`. An empty field removes the
key (its default). Every other key of the file stays as it is, the earlier file is kept as
`strata-<model>.json.bak`, and the model uses the change from its next start. Only Strata's own page can save
(JSON, the API key when one is set, as for the Chat settings); the network, key, MCP and program keys are not
editable there.
- **From other devices on your network.** The server listens on your PC only (`127.0.0.1`) unless you say otherwise:
run setup with `START-HERE.bat --setup --host 0.0.0.0 --api-key some-long-secret` (or add `"host": "0.0.0.0"` and
`"api_key": "..."` to `strata-<model>.json`). The server window then prints this PC's addresses
@@ -563,7 +704,8 @@ print(r.choices[0].message.content)
other page, and `Origin: null`, gets **403**. Clients that send no `Origin` (curl, the OpenAI and Anthropic SDKs,
other servers) are not affected. With
an API key, the key decides. `POST /unload` and `POST /load` take `Content-Type: application/json` from Strata's
own page (or no `Origin`), like `/settings`.
own page (or no `Origin`), like `/settings`. `POST /slots/0?action=save|restore` keeps the Host and API-key checks
and also takes only JSON from no `Origin`, Strata's own page or a trusted origin - also when an API key is set.
**Conversation cache.** A request that continues a chat reads only the part after what the engine already holds: the
live session, or one of the checkpoints it keeps in RAM (up to 6, ~118 MB each, taken at the start of each new
@@ -572,8 +714,15 @@ and pictures. The oldest checkpoint - in practice the end of the system prompt,
shares - is kept for good while the rest rotates by least recent use, so a NEW chat that shares that prefix starts
reading after it instead of from token 0. A prompt read from the start is also checkpointed at the end of its system
prompt when that is 2,048 tokens or more (engine 0.1.20; PR #62 + #65), so that root exists for agent clients with long
system prompts and tool lists. Engine options: `--prompt-cache N` (0 = off), `--prompt-cache-every N`,
system prompts and tool lists. Claude Code stamps its system prompt with a billing header that changes on every
request (`cch=...`) and every session (the 4th part of `cc_version=`); the server pins both stamps (to `f`s, as
llama.cpp does), so the system prompt and the tool list in front of it are the same prompt on every turn. Engine options: `--prompt-cache N` (0 = off), `--prompt-cache-every N`,
`--prompt-cache-root N` (0 = no system-prompt checkpoint), `--turn-token ID`.
A one-shot request that no later request continues (a classification call, a probe) can send
`"strata_checkpoint": false` in its body: it saves no checkpoint at its last turn nor every 16K tokens, so what
follows the reused prefix (or the root) is read in one run, and its session is not kept or parked for a next request.
It still starts from a checkpoint it matches, and still saves the system-prompt root when that reaches
`--prompt-cache-root`. Without the field (or with `true`) nothing changes.
**Multiple conversations (opt-in).** Add `--conversation-cache-mib 8192
--conversation-cache-slots 4` to the engine arguments to park up to four conversations
@@ -581,13 +730,20 @@ in a bounded 8 GiB host-RAM cache. This preserves controller/worker histories wh
their requests alternate; it does not execute requests concurrently. No client session
ID is required: only exact token/image prefixes with matching steering mode are reused.
The default budget is 0 (disabled); `--prompt-cache 0` also disables parking.
The initial shared-core integration supports a single session GPU: combining
enabled parking with `--layer-split` is rejected before model loading. Ordinary
upstream layer-split checkpoints remain available with parking disabled. FP16,
With `--layer-split`, a parked conversation holds one image per stage (each later stage's
session and its share of the conversation checkpoints), every image is validated before
anything is overwritten, and each is restored on its own GPU. Verified on a 4-GPU split:
a 9,276-token conversation restored in 51 ms, and its follow-up decodes exactly the tokens
it decodes when the conversation never left (`tools/parking_test.py`). FP16,
INT8, Q4_0 and identity-layout K8V4 snapshots are supported; the K8V4 draft ring
remains INT8, as in upstream. Windows/HIP and multi-GPU runtime coverage must be
reported separately from Linux/CUDA evidence.
The web page's Monitor tab has a **Conversation cache** card (0.1.39, #596): the parked conversations against the
slots and the RAM budget, how many were parked, restored and evicted, and the last switch (read from the engine's
log), and for every setup how many prompt tokens the cache gave back - in the last request and since the start.
`/metrics` has the same under `"conversation_cache"`.
Snapshots contain running state, checkpoints, used K/V pages, and draft-layer K/V.
They add host RAM, not another model or VRAM allocation. The byte budget also counts
an incoming snapshot during a switch. After a restore, unchanged K/V pages can be
@@ -612,12 +768,127 @@ rather than permission to continue with partial state. Indexer spare keys and th
moving spare row are preserved, including checkpoint rewinds.
The engine log reports parking, restoration, bytes, evictions, individual snapshot
sizes and K/V bytes reused during capture. `STRATA_SNAPSHOT_FULL_CAPTURE=1` disables
retention for diagnostic comparisons. Snapshots are not
persisted across restarts.
retention for diagnostic comparisons. Parked snapshots are not
persisted across restarts; the session files below are.
**Current limits (v1):** one request at a time, and one conversation cached at a time (switching between two chats
re-reads the other one unless the opt-in cache above is enabled); images only when set up with them (below); no video. **Temperature / top_p / top_k / min_p /
seed** are honored per request (OpenAI and Anthropic fields); with the default adaptive expert tier a sampled result
**Session files (disk).** The conversation the engine holds can be saved to a file and restored later, also after a
restart of the same engine version, so a long prompt is not read again. The server exposes the save and restore
requests of llama-server's slot API, for its single slot 0, when started with `--slot-save-path DIR` (also
`"slot_save_path"` in the config); NAME must be a plain file name inside DIR. There is no erase action and the file
format is Strata's own, not llama.cpp's:
```bash
curl -X POST "http://127.0.0.1:8080/slots/0?action=save" -H "Content-Type: application/json" -d '{"filename": "chat1.bin"}'
# {"id_slot": 0, "filename": "chat1.bin", "n_saved": 63025, "n_written": 1198691396, "timings": {"save_ms": 709.5}}
curl -X POST "http://127.0.0.1:8080/slots/0?action=restore" -H "Content-Type: application/json" -d '{"filename": "chat1.bin"}'
# {"id_slot": 0, "filename": "chat1.bin", "n_restored": 63025, "n_read": 1198691396, "timings": {"restore_ms": 898.2}}
```
DIR becomes one absolute path at start (`--slot-save-path` relative to the server's working directory, the config's
`slot_save_path` relative to the config's `"cwd"`), created with mode 0700 when missing. It should be private to the
user that runs Strata: the files hold the conversation's token IDs and state, are created with mode 0600 on Linux
(on Windows they inherit the folder's permissions), and nothing deletes them - about 1.2 GB per 63K-token
conversation. Before it writes, a save checks that the disk has room for the whole new file plus
`--session-min-free-mib` (an engine argument, in the config's `args`; default 4096, 0 = no check) - also when it
replaces a file, whose space comes back only after the rename. This is a preflight, not a quota or a reservation.
NAME may
not contain a path, a drive, a stream (`:`), a Windows device name (`NUL`, `CON.bin`, `COM1`...), a control character,
a leading dot or a trailing dot or space.
The request must be `Content-Type: application/json` (else `415`) and come from no browser page, Strata's own or a
trusted origin (another site's `Origin` gets `403`, also with an API key); the Host and API-key checks apply as
everywhere. Errors: `501` without `--slot-save-path` or with parallel requests; `400` for a slot other than 0, an unknown action, a refused
file name, or a file the engine refuses as invalid (not a session file, corrupt, another model or configuration, over
this session's limits; the session is as it was); `404` for a restore of a missing file; `503` while the model is not
loaded, when the RAM to read the file is not there or an allocation failed; `507` when the disk has no room (the
free-space reserve, or the OS reports no space or quota); `500` for any other I/O failure (permissions, read, write,
flush, rename) and when the engine ended (a restore transfer failure, below, an engine that said nothing for
`engine_silence_s`, or one that answered out of protocol) - the next request starts it again. The status follows the
engine's category (`error.kind`: `invalid`, `memory`, `storage`, `io`), never the words of the message. A save that
failed after its new file had replaced the old one says so with `error.published: true` (below). A save or restore waits for the running request
(the same queue), shows in `/status` and counts as activity for the idle unload. A later request whose messages
continue the restored conversation reuses the restored state or its checkpoint; a short tail may be read again (35
tokens in the measurement below). Only the deepest checkpoint is saved, so an edit further back reads more again.
Clients still send their messages (and images): the file holds engine state and token/image identity, not a chat
export. Underneath, `strata --serve` takes `SAVE <path>` and `RESTORE <path>` on stdin between requests and answers
`SAVED <tokens> <bytes> <ms>`, `RESTORED <tokens> <bytes> <ms>`, `SERR <kind> <published 0|1> <reason>` (failed,
the engine and the session as they were) or `FATAL <reason>` (then exits). On the way, `SESSION <done> <total>` follows
every block of the file that moved (at most 16 MiB, the last partial block and a small file included), and
`SWAIT <phase> <seconds>` comes before a step that blocks in one call (`fingerprint`, `capture`, `flush`, `publish`,
`validate`, `transfer`): that step is allowed those seconds - 60 plus one per 4 MiB it concerns, at most 3600 - by the
engine's watchdog and by the server, then it counts as stuck. The next line clears the allowance. The server checks
every line: a malformed or unknown one, counts that are negative or go back, an allowance outside 1..3600 or a time
that is not a finite number end the engine as out of step.
One file holds the running state, the deepest checkpoint, every QSA layer's K/V up to the conversation's length and
the draft layer's K/V. Format v1, little-endian, fixed-width integers, IEEE-754 floats (a big-endian build does not
compile): a 64-byte header (magic `STRSESS\x01`, version u32, header size u32, model fingerprint u64, config
fingerprint u64, payload length u64, two reserved u64 that must be 0, a hash of the first 56 bytes), the payload
(geometry, layer range, cvec flag, the live state, the checkpoints, the K/V layers; every array preceded by its u64
count), the payload hash and the end marker `STRSEND\x01`. The hash is a 64-bit function with xxHash64-style rounds,
not the standard XXH64 stream; it detects accidental corruption and does not authenticate a file: restore only files
this engine wrote. An unknown version is refused; a new format gets a new version number.
A file is bound to the model inputs and to the settings that change what the saved bytes mean. The model fingerprint
samples (size, first and last MiB) every file the engine loads, by its role: the GGUF shards (also
`--native-dense-gguf`, the head shards and `--embd-gguf`), the PLE shard, the pack's index/dense/embedding files and
`native_experts.txt`, every file the expert source resolved - the pack's `experts.bin`, or for a pack read in place each
layer's gate/up/down GGUF as `native_experts.txt` names it, also one outside the CLI shards - and the MTP's files; not
other files in those folders, and not the path, so a moved model folder still matches. A
change in the middle of a file that keeps its size is not seen: do not change model files while their sessions are
kept. The config fingerprint covers the engine version string (another version is refused; two builds of the same version
are not told apart - there is no build hash), CUDA or HIP,
`--kv`, `STRATA_KV_ROT`, `--kv-resident`, `--max-context`, `--mtp-window`, the resolved rope configuration (type,
base, factor, freq scale, original context, the YaRN knobs - the cached K is post-RoPE), the loaded control vector (a
digest of the tables uploaded: every file's content times its exact scale, the mode, the layer range and the
direction) and the arithmetic switches (`--native-*`, `--no-ple`, the A/B arms). Sampling, seeds, draft tuning and
the expert tier are not in it: they change what comes next, not what the saved cells hold.
A save writes a temporary file with a new hidden name beside `path` (created exclusively, never an existing file or
link), flushes it to the disk, renames it over `path` (`MoveFileExW` with write-through on Windows, which promises no transaction
on every filesystem) and, on POSIX, flushes the folder (Windows has no folder flush; the file flush and the
write-through rename are all it does). A save that fails before the rename keeps the old file at `path` and removes
only its own temporary file. Once the rename is done the old file is gone: if the folder flush then fails, the save
fails with `published` set - the new file's bytes are complete and flushed, but its name may not survive a power loss.
A filesystem that cannot flush a folder (`EINVAL`) is not a failure; the engine logs it. A restore opens `path` without following a symbolic link (or a Windows reparse point) and refuses
anything but a regular file with one name; it checks the size, the header, both fingerprints (before the payload is
parsed; the first 16 MiB block, header included, is already read), that the parse's peak (the image, the read buffer, the per-segment overhead) fits in RAM above the parking
floor (`--conversation-cache-min-free-mib`), the file's size against the largest this session can restore, the
geometry and layer range before any state array, and every count against the bytes left and this session's exact
limits (context and cells, checkpoints, layers, each running-state array, each K/V part) before allocating it, the payload hash
and then the usual snapshot validation - all before any device write, and a refusal leaves the current session as it
was. A transfer failure after the device writes began ends the engine (`FATAL`) rather than decode from a partial
state; the server reports `500` and starts it again. A restore does not park the outgoing session. Not supported with
`--layer-split`, `--peer-device`, `--batch` (the config's `"parallel"`, #465; the server answers `501`) or
`--prompt-cache 0` (the RAM conversation cache need not be on). On Linux the file
moves with `O_DIRECT` in 16 MiB blocks when the filesystem takes it (buffered I/O otherwise, or with
`STRATA_SESSION_BUFFERED=1`); on Windows with buffered I/O. The engine has been run on Linux/CUDA only. An earlier
revision's CPU file-I/O test passed as a 32-bit Windows executable under Wine; the current code has not been built
for Windows, and the Windows engine, HIP and AMD cards have not been run.
A session file saves conversation state, not all of the process's execution history. Exact future token replay
across restarts is not guaranteed: expert residency and CPU/GPU rounding can change later output. In a 63K test the
first 32-token continuation after a restore matched the process that kept running; on the next continuation three
restored processes agreed with one another but differed from that process from the 28th token on. A refused,
corrupted restore in between did not change the restored processes' continuation. The cause of this divergence has
not been isolated.
Measured on an RTX 4070 Ti (12 GB), Ryzen 9 5900X, 64 GB RAM, NVMe ext4, IQ3_XXS, a 63,025-token conversation
(engine 0.1.38 with this change, binary sha256 `3bbe4fc3...`): file 1.20 GB, save 0.65 s including the flushes to
disk (10 more saves of the 1.20 GB state after a further turn: 0.59-0.61 s replacing one file, 0.80-1.18 s to new
names), restore 1.05 s in a new engine process, then the next 32-token turn in 1.14 s with the same 32 output tokens as
the same turn without a restart (1.00 s); the cold first turn takes 25.5 s. The save reported 73 `SESSION` lines, never
more than 16 MiB apart. A symbolic link, a second hard link and a file with one flipped payload byte were refused
(the last also while a restored conversation was live, which then went on to answer); a fresh engine with another
`--rope-freq-base` refused the file and then answered; a save with a RAM floor above the machine's RAM was refused as
`memory` before any copy, the file it would have replaced untouched. The times are single runs.
**Current limits (v1):** one request at a time unless `"parallel": N` is set (opt-in batch slots, up to N requests
decoded together: [BATCHING.md](BATCHING.md)), and one conversation cached at a time (switching between two chats
re-reads the other one unless the opt-in cache above is enabled, or each conversation keeps its own batch slot); images only when set up with them (below); no video. **Temperature / top_p / top_k / min_p /
seed** are honored per request (OpenAI and Anthropic fields), and so are stop strings (OpenAI `stop`, a string or up
to 4; Anthropic `stop_sequences`): the answer ends before the first one, which is not sent, and the engine stops
there (`finish_reason` "stop"; `stop_reason` "stop_sequence" with `stop_sequence` set to the one found); with the default adaptive expert tier a sampled result
is not reproducible run to run - for seed-reproducible output add `--adapt-every 100000` (static residency) to the
engine arguments. The run config's optional `sampling` block sets the defaults for requests that leave the fields out
(`"sampling": {"temperature": 1.0, "top_p": 0.95, "top_k": 20}`); a request's own fields always win, and with no
@@ -631,6 +902,65 @@ without penalties, so more of its guesses are now rejected. Requests without pen
---
### The Responses API and Codex CLI
`POST /v1/responses` (0.1.39, #451) speaks OpenAI's newer Responses API, which Codex CLI uses (it no longer
speaks Chat Completions). It runs on the same path as `/v1/chat/completions`, so the thinking levels, the thinking
budget, the conversation cache and the same API key, Host and Origin checks apply.
It is **stateless**: nothing is stored, so the client sends the whole conversation in `input` every time (Codex does,
with `store: false`). `previous_response_id`, `conversation`, `background` and the retrieve/delete/cancel endpoints
are refused with an error that says so.
| Request | What Strata does |
| --- | --- |
| `input` as a string, or as items | `message` items (`user`, `assistant`, `system`, `developer`; text and images), `reasoning`, `function_call`, `function_call_output`, `custom_tool_call(_output)` |
| `instructions` | The system message (with leading `developer` messages; later ones become user messages, as on the chat path) |
| `tools` | `function` tools, `namespace` tools (the model sees `namespace.name`; calls come back with `namespace` and `name`), `custom` tools (one free-form `input` string). Hosted tools (`web_search`, `file_search`, ...) are left out: the model cannot run them |
| `tool_choice` | `"none"` hides the tools; anything else lets the model choose (it cannot be forced) |
| `reasoning.effort` | `none`/`minimal`, `low`, `medium`, `high`/`xhigh`; without it the model's default (high) |
| `max_output_tokens` | The output cap (thinking included). Running out ends the response `incomplete` (`max_output_tokens`) |
| `text.format` | `json_schema` and `json_object` use the [JSON response formats](#json-response-formats) (checked, not constrained) |
| `temperature`, `top_p`, `reasoning_budget_tokens`, ... | As on the chat path |
The model's thinking comes back as a `reasoning` output item with `reasoning_text` content (streamed as
`response.reasoning_text.delta`). This model writes no separate summaries, so `summary` is empty. With
`"include": ["reasoning.encrypted_content"]` the item also carries `encrypted_content`: an opaque string (base64, not
encrypted; the client already holds the text). Send the reasoning items back with the rest of the conversation, as
Codex does: their thinking goes back into the prompt, so it matches what the model wrote and the conversation cache
is reused.
With `"stream": true` the events are the official ones, in order: `response.created`, `response.in_progress`, then
for each output item `response.output_item.added`, its deltas (`response.reasoning_text.delta`,
`response.output_text.delta`, `response.function_call_arguments.delta`), its `...done` events and
`response.output_item.done`, and at the end `response.completed`, `response.incomplete` or `response.failed`.
While a long prompt is read, a `response.in_progress` event goes out every 15 s, which Codex counts as activity.
Errors before the answer starts have the Responses form (`{"error": {"message", "type", "param", "code"}}`); after
it started they arrive as `response.failed`.
**Codex CLI.** In `~/.codex/config.toml` (Windows: `%USERPROFILE%\.codex\config.toml`):
```toml
model = "strata" # any name; Strata answers with its model
model_provider = "strata"
model_context_window = 32768 # the context you chose in setup: Codex compacts before it gets there
show_raw_agent_reasoning = true # show the model's thinking (it writes no summaries)
# model_reasoning_effort = "medium" # none, low, medium or high; default: the model's (high)
[model_providers.strata]
name = "Strata (local)"
base_url = "http://127.0.0.1:8080/v1"
wire_api = "responses"
stream_idle_timeout_ms = 600000 # a first, long prompt can take minutes to read
# env_key = "STRATA_API_KEY" # only if the server has an api_key: the variable holding it
```
Then run `codex` (or `codex exec "..."`) as usual. Codex warns `Model metadata for ... not found` for a local model
name; that is expected. Measured with Codex CLI 0.160.0 and Q2_0 on an RTX 5070: its first prompt (instructions and
tool descriptions) was 9,443 tokens, read in 10 s; in a tool loop, each later turn reused about 96% of the prompt from
the cache and read only the new part in 1-2 s. On Windows, Codex's sandbox rejected every shell command in that test
until it was started with `-c 'windows.sandbox="unelevated"'` (a Codex setting, not Strata's).
## Tools from MCP servers
The chat page can give the model tools from [MCP](https://modelcontextprotocol.io) servers, as LM Studio and Claude
@@ -743,11 +1073,34 @@ helper (`strata-vision`, from llama.cpp's `mtmd` library) and adds it to your st
| Encoder on | Time per picture | Cost |
| --- | --- | --- |
| **GPU** (recommended) | **0.1-0.5 s** (up to 1,024 image tokens) | ~1.4 GB of VRAM is kept free for it, so the expert cache is smaller: text output is a few % slower (table below) |
| CPU | 10-30 s (pictures are scaled down to ~300 image tokens) | nothing on the GPU |
| CPU | about 3 s at 300 image tokens on 8 cores (6-13 s on 4 threads); more tokens take longer, in proportion (#767, #625) | nothing on the GPU |
A picture becomes up to 1,024 tokens of the context (a 640x480 photo: 300). The same picture sent again, as chat apps
do on every turn, is encoded only once.
**More image tokens (0.1.39, #625):** `--vision-tokens N` at setup (`START-HERE.bat --setup --vision cpu
--vision-tokens 768`) sets the most tokens a picture becomes - `"max_tokens"` in the `"vision"` section of
`strata-<model>.json`, which you can also edit by hand. More tokens keep more detail (small text, charts, screenshots)
and take longer to encode, on the CPU most of all; a setup run again keeps the value.
**A minimum number of image tokens (0.1.40, #767):** `"min_tokens": N` in the `"vision"` section of
`strata-<model>.json` (edit it by hand; a setup run again keeps it) is passed to the encoder as `--min-tokens N`
(mtmd's `image_min_tokens`): a small picture is scaled up to at least N tokens. llama.cpp's mtmd prints that Qwen-VL
models want at least 1,024 for grounding tasks (pointing, counting small items); the CPU default stays at 300 at most
and no minimum, because a larger minimum changes the image answers and costs encode time (about 3 s at 300 tokens on
8 cores, in proportion to the tokens).
**A Q8_0 encoder (#625):** `"mmproj"` in the `"vision"` section can point to another mmproj file of this model, for
example a Q8_0 one (llama.cpp's `convert_hf_to_gguf.py --mmproj --outtype q8_0` makes one): the encoder's library
reads quantized weights, the file is smaller (590 MiB against 865 MiB for the BF16 one, from `llama-quantize
mmproj-Qwen3.8-Flash-Next-BF16.gguf mmproj-Qwen3.8-Flash-Next-Q8_0.gguf Q8_0`), and on the CPU it uses less RAM
(about 280 MiB less in the encoder) and can encode faster than BF16 at the default 300 tokens. Setup downloads the
BF16 file, and a setup run again keeps a file of your own that still exists. **Recommended for `--vision cpu`.**
Accuracy against BF16, per image token: the cosine of the embeddings is 0.997-0.999 on average (#625's report, with
a community Q8_0 file, at 300, 768 and 1,024 tokens), and 0.9988 and 0.9987 on two pictures with the file made by
the command above (300 tokens; the worst single token 0.94-0.96). Encode time above 768 tokens is the same as BF16's
within about 3%. Numbers on more pictures (charts, small text) are welcome in #625.
**A spare GPU for the encoder (0.1.33, #408):** with a card the engine doesn't use, add `"cuda_device": 2` (numbered
like `nvidia-smi`) to the `"vision"` section of `strata-<model>.json`: the encoder then runs on that card alone. Lower
`--vram-reserve-mib` in `"args"` to 700 as well, so the engine's cards keep that VRAM for the expert cache. The
@@ -869,9 +1222,10 @@ the document, +0.4% on the chat. Details: `bench/results/2026-09-27-esp/`.
| Slower than the tables | The monitor plugged into the GPU and other GPU programs take VRAM from the expert cache; RAM running below its rated speed (enable EXPO/XMP in the BIOS) slows the CPU half. |
| `this server was started without the vision encoder` | The model was set up for text only: run setup again with `--vision gpu`. |
| A picture is refused or `cannot read the image` | The file is not a picture Pillow can open (JPEG, PNG, WebP, GIF, BMP, TIFF, AVIF work). |
| Pictures are slow (10-30 s) | The encoder runs on the CPU: run setup again with `--vision gpu` (needs ~1.4 GB of VRAM). |
| Pictures are slow (3-30 s) | The encoder runs on the CPU: run setup again with `--vision gpu` (needs ~1.4 GB of VRAM). |
| A request never finishes: "reading the prompt", GPU "100%" at low power | The GPU ran out of VRAM (engines before 0.1.9 could end with ~30 MiB free at large contexts). Run `START-HERE.bat` once to get engine 0.1.9 or newer; the log then says `... MiB of VRAM free with everything loaded` (a few hundred) and names the `--vram-reserve-mib` to add if it is low. |
| Generation stops mid-answer, GPU "100%", one CPU core busy | Fixed in engine 0.1.12 (issue #29, a race in the CPU expert pool on big-VRAM cards). Since then a request that stops moving ends with an error instead of hanging (after 2 minutes; 1 minute from 0.1.13): the log says `no progress for ... s ... (issue #29)` with where it stopped, and the next request starts the engine again. If you see that line, please open an issue with it. Engine 0.1.13 adds a stall report under it (what every expert-pool thread and the GPU handshake were doing, memory and page faults) and, on Windows, a `strata-stall-<pid>.dmp` file with every thread's stack: attach both. (`STRATA_WATCHDOG_S` sets the time in seconds; 0 turns it off.) Engine 0.1.14 fixes the stall those reports found (issue #31: with the IQ packs the host could wait forever inside the NVIDIA driver while copying experts in a verify window; the experts are now copied by a GPU kernel, `--pcie-mode dma` restores the old way). |
| `no progress for 60 s ... reading the prompt` on Linux, and the stall report says `threads waiting on the disk (state D): 16 ...` | The engine waits for the drive, not a deadlock: the n-gram table is read at random (`--ple-io direct`), which a rotational disk cannot keep up with (#605). The engine warns at start when the table is on one; `--ple-io ram` (Linux, needs RAM for the table) or the model on an SSD fixes it. Setup adds `--ple-io ram` itself on a rotational disk when the RAM holds the table (0.1.39). |
| `the engine said nothing for ... s during the request` or `... did not finish the request after it was stopped (STOP)` | Issue #481: the engine and the server lost step (the engine waits for its next command, the server for the request's end; GPU at 0 %, nothing in the log). The server ends the engine after 300 s without a line from it during a request (while a prompt is read: each chunk may take three times the previous one's time, the first one up to its tokens at 50 tok/s more), the request ends with an error and the next request starts the engine again. `"engine_silence_s": 600` in `strata-<model>.json` sets the time (0 = wait forever, as before). If you see it, please add the end of the engine log to #481. |
| `out of memory: cudaFuncSetAttribute` in the log (IQ3_XXS, long prompt) | Fixed in engine 0.1.15: CUDA loaded a kernel's code when it was first needed, and mid-prompt there was no VRAM left for it. Run `START-HERE.bat` (Windows) or `./setup.sh` (Linux) once to update. |
| Anything else | The engine log is `strata-<model>.log` in this folder. |
@@ -887,12 +1241,32 @@ the document, +0.4% on the chat. Details: `bench/results/2026-09-27-esp/`.
the conversation while you chat).
- **RAM:** all 24,576 experts, pinned. The CPU computes the experts that are not on the GPU **in place**, at the same time
as the GPU works on the cached ones (AVX-512 / AVX2 kernels, ggml's for the i-quants).
- **SSD:** the 28.8 GB n-gram table, read a few rows per token through the OS cache.
- **SSD:** the 28.8 GB n-gram table, a few rows per token, read unbuffered past the OS cache (`--ple-io direct`, the
default, made for SSDs; on a rotational disk `--ple-io ram` keeps the table in RAM, #605). The engine reads the table
in the format the GGUF has it: IQ4_NL (the default table), Q4_0, Q5_0, Q5_1, Q8_0, FP8 (E4M3 with a scale) or BF16.
Measured on 4,000 random rows against the checkpoint's own BF16 table, the mean per-row error is Q8_0 0.53%, FP8 2.64%,
Q5_1 3.78%, Q5_0 4.25%, IQ4_NL 7.60%, Q4_1 7.80%, Q4_0 8.55% (a Q8_0 table is 54 GB, an IQ4_NL one 28.8 GB). On the Q2_0 model, 700 teacher-forced tokens, the mean KL against the BF16 table is 0.0117 (Q5_0), 0.0121 (Q5_1), 0.0125
(Q8_0), 0.0128 (FP8), 0.0140 (Q4_0) and 0.0146 (IQ4_NL), with the perplexity within 1% of BF16's either way: any change to the
table moves the 2-bit model by about 0.012, so the formats are hard to tell apart. Setup does not offer another table;
it only changes which GGUF the engine is given.
- **Speculation:** the model's own MTP layer drafts up to 3 tokens; one pass over all 48 layers checks them. 2.4-3.2
tokens per pass on average. When the reply repeats the context (code edits, quoted text), **prompt lookup** (engine
0.1.7) drafts up to 5 tokens from the earlier copy, but only where its measured acceptance and cost say it pays:
code edits 6-11% faster, other text unchanged. The drafts are checked like the MTP's, so the output is the same.
- **Prompts** are processed in 2,048-token chunks with the experts streamed to the GPU over PCIe.
- **Prompts** are processed in chunks of up to 8,192 tokens (`--prefill auto`; 32,768 opt-in) with the experts
streamed to the GPU over PCIe. Since engine 0.1.39b (#583) the streamed ring is sized in bytes for the pack (a
native pack with bigger blobs gets fewer ring slots and keeps more cache slots) and `--prefill auto` picks the
largest chunk that keeps that ring full. It only gives up ring slots where 0.1.39's rule held the chunk at 4,096
tokens or less; from 6,144 on it keeps 0.1.39's ring and only looks for a bigger chunk beside it (shrinking the
ring there measured slower: the Coder -12%), and a prompt that fits 0.1.39's chunk keeps 0.1.39's ring (one
chunk: a smaller ring only slowed it). RTX 5070, 32K prompts against `STRATA_RING_BYTES=0`, 2-3 interleaved
rounds: IQ3_XXS +18.5% at a 1,500-slot cache and +9% in another memory state, the Coder +3%, IQ3_S unchanged; 4K
prompts unchanged (-0.7% to +1.1%, bit-identical). Q2_0 (a fixed `--prefill 2048`) is not affected. It changes
the bits of a long prompt against 0.1.39 (the experts go through a different
mix of cached and streamed groups). Measured quality, teacher-forced over the next 2,001 tokens of a long document
against the FP16 prompt path (IQ3_XXS, `--prefill auto`): 8K prompt KL 0.042 (0.1.39: 0.054), top-1 agreement
93.5% (92.2%); 32K prompt KL 0.020 (0.019), top-1 95.3% (95.0%) - the same band as before. `STRATA_RING_BYTES=0`
restores 0.1.39's ring, loan and chunk choice.
The full story, with measurements, bottlenecks and what comes next: **[docs/paper/Strata-Paper.pdf](paper/Strata-Paper.pdf)**.
@@ -940,7 +1314,10 @@ times out, the server keeps ownership and reports an error rather than claiming
`POST /v1/chat/completions` accepts `response_format: {"type":"json_object"}` or
`{"type":"json_schema","json_schema":{"name":"answer","strict":true,"schema":{"type":"object","properties":{"answer":{"type":"integer"}},"required":["answer"],"additionalProperties":false}}}`.
The schema must describe an object at its root. Local `#` references work; remote references are refused.
The schema must accept only JSON objects at its root: `"type":"object"`, or an `anyOf`/`oneOf` whose branches are all
object schemas (an `allOf` with an object member, or a local `$ref` to one, also counts), as apps written for
llama.cpp's `json_schema` send. A root that also allows an array, string, number, boolean or null is refused. Local
`#` references work; remote references are refused.
`json_schema` is checked with the Python package `jsonschema` when it is installed (`python -m pip install
"jsonschema>=4.23,<5"`; setup does not add it); without it the answer is only checked to be one JSON object, and the
server says so once.
+59
View File
@@ -0,0 +1,59 @@
# Avoid duplicate expert slots across helper and primary GPUs
## Why this helps
On a PC with two small GPUs, helper experts start out separate from the primary
cache. Later, the adaptive primary cache can promote an expert that the helper
already holds. Both cards then spend memory on the same expert.
We observed about 1,900-2,000 duplicate slots during sustained requests.
This opt-in change keeps helper-owned experts out of primary promotion candidates,
in both the server and command-line generation paths.
## Use
Set `STRATA_DISJOINT_ADAPT=1` before starting the engine. It is off by default.
It leaves the existing peer-tier check and expert arithmetic unchanged.
The check is live: each promotion candidate is asked of the helper's own cache (`RemoteExperts::holds`), so an expert
the helper's tier swaps in or out later is followed (a mask copied at load would go stale). Needs the helper-share
change (#854) for `holds`.
## Validation environment
- Two modified RTX 3080 20 GB cards; CUDA SM86, CUDA 13.1, Linux.
- Intel Xeon E5-2686 v4 (18 cores / 36 threads), about 94 GiB usable RAM.
- PCIe 3.0, no GPU peer-to-peer access; 17 expert-pool workers.
- Qwen3.8-Flash-Next IQ3_S, 131,072-token context, INT8 KV,
32,768 resident KV cells.
- Medium thinking: 2,048-token reasoning cap, 8,192-token output cap.
- Physical GPU power limits stayed at 250 W and 280 W. No power increase.
- Historical performance tests used an isolated engine based on official
v0.1.38 plus reviewed architectds/Strata changes (best, 05c0f36).
This PR contains only our change, ported to official main (99f3dbd).
The historical timings are NOT a measured speedup of this pure-upstream port.
## Historical benefit
Same binary and helper capacity, six complete JSON tasks per arm:
| 32K cached request | Original | Ownership guard |
| --- | ---: | ---: |
| Generation | 81.5 tokens/s | 94.1 tokens/s |
| Complete request | 12.997 s | 11.417 s |
Generation improved by 15.5%; request time fell by 12.2%.
The tasks returned complete 100-record arrays and correct source facts.
All 18 checks passed across the original, guarded, and larger-cache arms.
The larger-helper arm is not included in the comparison above.
These repetitive tasks do not represent all agent workloads.
## Validation and remaining work
- Standalone ownership test passes, including disabled, empty, unowned,
owned, and out-of-range entries, plus candidate filtering.
- Linux compile/link against pure official main (99f3dbd) passed. No engine
was started and no production settings were changed.
- Pure-upstream GPU performance and session/vision/cancellation regression
checks still need to be run. Keep this PR as a draft until then.
- Historical combined builds passed those safety checks, but that is not a
substitute for validating this independent port.
+139
View File
@@ -0,0 +1,139 @@
# Resident expert exchange buffer rotation
When the resident RAM complement exchanges an expert with the GPU cache, the
evicted expert lands in a temporary host buffer. The existing path then copies
those bytes into the promoted expert's old RAM slot. Rotation removes that last
host copy: the temporary buffer becomes resident, and the old RAM slot becomes
the next temporary buffer.
## Enable
Rotation is **off by default**. Set `STRATA_EXCHANGE_ROTATE=1` before starting the
engine with your existing arguments:
```bash
export STRATA_EXCHANGE_ROTATE=1
```
```powershell
$env:STRATA_EXCHANGE_ROTATE = '1'
```
Only the exact value `1` opts in. Set it to `0` or remove it to restore the copy
path. This setting does not select resident RAM mode or change any preset.
It applies only when all layers have equal-size expert blocks, the resident
complement is fully pinned and GPU-mapped, and the exchange buffers can also be
pinned and mapped. Other layouts keep the copy path and log why rotation could
not be enabled. Configurations without a resident complement do not use it.
The startup diagnostic confirms activation:
```text
FileExpertSource: exchange buffer rotation enabled: ...; no host commit memcpy
```
Serving diagnostics report the cumulative rotated block count and avoided
`memcpy` payload bytes. This counts the copied payload once, not read plus write
traffic, and does not include the GPU transfers, which still occur.
## Ownership and synchronization
The original allocations own their memory until `FileExpertSource::close()`.
Rotation updates slot ownership and keeps each host pointer paired with its GPU
alias. Atomic slot IDs let background residency queries observe those immutable
pairs. Compute readers and GPU transfers must still finish before the existing
serialized commit; rotation adds no overlapping compute or new synchronization
policy. Reserve the maximum exchange capacity before rotation starts: growing a
live exchange arena is rejected because it can now hold resident experts.
## Tests
The ownership test can run without CUDA:
```bash
g++ -std=c++20 -O1 -g -fsanitize=address,undefined -fno-omit-frame-pointer \
-pthread -Iinclude tests/core/exchange_storage_test.cpp -o /tmp/exchange_storage_test
/tmp/exchange_storage_test
```
With an existing CUDA build configured:
```bash
cmake --build build --target strata exchange_storage_test file_expert_source_test
ctest --test-dir build -R '^(exchange_storage_test|file_expert_source_test)$' --output-on-failure
./build/file_expert_source_test --rotation-gpu
compute-sanitizer --tool memcheck --error-exitcode 71 ./build/file_expert_source_test --rotation-gpu
```
The CPU test checks byte preservation, pointer/alias ownership, guards, invalid
operations and concurrent residency queries across 12,304 exchanges. The CUDA
fixture performs 64 exchanges in each of three modes: copy with pinned RAM,
rotation with pinned RAM, and requested rotation with pageable RAM (fallback).
It checks exact bytes, GPU alias reads, file fallback, capacity, and close/reopen.
The GPU fixture is explicit; the ordinary CTest invocation does not run it.
On 2026-10-04, the clean patch on upstream
`6f32ec070f23ced9f50e704d854d775da52591ab` built on Linux with GCC 15.2 and CUDA
13.3 for an RTX 4090. Both CTest tests, the ASan/UBSan ownership test, and the
CUDA fixture passed. Compute Sanitizer reported **0 errors**. AMD and Windows
GPU execution were not tested for this patch.
## One native token-identical A/B
Measured on 2026-10-04 using the clean rotation build at
`18a30ad775bce86e99a54e6dc552ccb72c66d16e`, on upstream
`6f32ec070f23ced9f50e704d854d775da52591ab`. The model is the standard
GSQ-RCO **Q2_0**, with its matching IQ4_NL PLE shard. Both shards were verified
against the publisher's SHA-256 hashes at the pinned revision in the receipt.
The native pack uses the existing `tools/iq_pack.py`; no compatibility conversion
or Q8 PLE reader extension is needed.
Both arms used the same Release binary (GCC 13.3, CUDA 13.2), RTX PRO 6000
Blackwell 96GB, Ryzen 9 7950X and 128GB installed RAM. A fresh engine per arm
generated exactly 1,024 tokens after the same 1,024-token counting prompt.
The context allocation was 40,960, with 12,000 GPU expert slots, a 16.19 GiB
pinned/mapped resident complement, FP16 KV, greedy target-only decoding and
zero offered drafts, reused prompt tokens or file-tier blob reads during
generation. Only `STRATA_EXCHANGE_ROTATE` changed. Transfer completion is
awaited before admission.
The GPU cache was deliberately capped at 12,000 slots (15.45 GiB) to exercise
resident RAM exchanges. Q2 can fit entirely on this 96GB GPU; this is a storage
path comparison, not the fastest full-GPU configuration. It does not change
the project's presets or enable resident mode for existing users.
| Rotation | Decode tokens/s | Resident exchanges | Avoided host-copy payload | Output IDs |
|---|---:|---:|---:|---|
| `0` | 104.90 | 3,802 | 0 bytes | 1,024, identical |
| `1` | 113.44 | 3,802 | 5,255,884,800 bytes | 1,024, identical |
Decode rates exclude startup and prefill. The observed throughput change was
+8.1% in this one pair; repeated timing or a general speedup is not established.
The out-of-vocabulary EOS sentinel `2147483647` forces the exact output length,
so this is not a model-quality evaluation. Speculative-path token parity,
AMD and Windows GPU execution remain untested.
The same Blackwell binary passed both CTest checks and the three-mode CUDA
fixture; Compute Sanitizer reported 0 errors. The
[receipt](../bench/results/exchange-rotation-ab.json) contains source, binary,
model, pack and profile hashes, arguments, timings and diagnostics. The
[raw IDs](../bench/results/exchange-rotation-token-ids.json) retain the input
and both outputs. To check the token comparison without a model:
```bash
python - <<'PY'
import hashlib, json
from pathlib import Path
p = Path('bench/results')
r = json.loads((p / 'exchange-rotation-ab.json').read_text())
raw = (p / r['token_ids_file']).read_bytes()
assert hashlib.sha256(raw).hexdigest() == r['token_ids_file_sha256']
d = json.loads(raw)
assert len(d['input']) == len(d['off']) == len(d['on']) == 1024
assert d['off'] == d['on']
for name, arm in zip(('off', 'on'), r['arms']):
sha = hashlib.sha256(json.dumps(d[name], separators=(',', ':')).encode()).hexdigest()
assert sha == arm['output_token_ids_sha256']
print('1,024 identical output token IDs')
PY
```
+46 -8
View File
@@ -5,9 +5,9 @@ AMD graphics card. The short version is in the [README](../README.md#install); a
this for you with [AI_SETUP.md](AI_SETUP.md).
> **On this page:** [What you need](#what-you-need) · [Windows](#windows) · [Linux](#linux) ·
> [AMD cards](#amd-cards) · [Several cards](#two-or-three-cards) · [Docker](#docker-linux) · [Updating](#updating) ·
> [Where things are stored](#where-things-are-stored) · [Setup's questions](#setups-questions) ·
> [Tuning](#tuning-for-your-pc) · [All options](#options-without-questions)
> [AMD cards](#amd-cards) · [Several cards](#two-or-three-cards) · [Docker](#docker-linux) ·
> [Older CPUs](#older-cpus-experimental) · [Updating](#updating) · [Where things are stored](#where-things-are-stored) ·
> [Setup's questions](#setups-questions) · [Tuning](#tuning-for-your-pc) · [All options](#options-without-questions)
## What you need
@@ -15,7 +15,7 @@ this for you with [AI_SETUP.md](AI_SETUP.md).
| --- | --- |
| **GPU** | **NVIDIA** RTX 20, 30, 40 or 50 series, **12 GB VRAM or more** (8 GB runs, slowly). Measured on an RTX 5070 and an RTX 3090; RTX 20 (Turing, since 0.1.27) was tested by a contributor on an RTX 2070. **AMD** Radeon RX 7900 XT / XTX, RX 9070 / 9070 XT and Radeon AI PRO R9700 (validated), RX 7800 XT / 7700 XT and RX 9060 XT (validated by their owners), RX 6800 / 6900 series (community-reported), with 12 GB of VRAM or more. See [AMD cards](#amd-cards). |
| **RAM** | Enough for the size you pick ([which model](MODELS.md#pick-by-ram)); **64 GB** runs every size. A big GPU makes up for less RAM - the [low-RAM mode](MODELS.md#a-big-graphics-card-and-little-ram). |
| **CPU** | x86-64 with AVX2 (any Intel/AMD desktop CPU from the last ~8 years). AVX-512 (Ryzen 7000/9000) is a bit faster. |
| **CPU** | x86-64 with AVX2 (any Intel/AMD desktop CPU from the last ~8 years). AVX-512 (Ryzen 7000/9000) is a bit faster. Older CPUs without AVX2 are experimental and slow: [Older CPUs](#older-cpus-experimental). |
| **Disk** | ~70-80 GB free for the model, ~6 GB for the MTP layer (+1 GB with images). **Q2_0 on an AVX-512 CPU** also writes a one-time ~40 GB copy of its experts for the fast CPU kernel. On Linux with an AMD card, ROCm takes ~10 GB more when setup installs it. An NVMe SSD is strongly recommended: it makes the first start much faster. |
| **OS** | Windows 10/11, or Linux (Ubuntu 22.04/24.04 get everything installed automatically). |
| **Driver** | **NVIDIA:** a current driver, version 580 or newer ([nvidia.com/drivers](https://www.nvidia.com/drivers) or the NVIDIA App). **AMD:** on Linux the kernel's amdgpu driver (no ROCm install needed); on Windows a current AMD Software: Adrenalin Edition driver ([amd.com/support](https://www.amd.com/en/support)). |
@@ -69,8 +69,8 @@ On Linux setup uses a system ROCm 7 when there is one, or installs ROCm into `.v
sudo), and compiles the engine on your PC for the card (10-20 minutes, once; it needs a C++ compiler and git:
`sudo apt install build-essential git`). Several AMD cards share the model with `--gpus`, as on NVIDIA.
What differs from NVIDIA for now: pictures are read by the image encoder on the CPU (`--vision cpu`, 10-30 s per
picture), `--calibrate` is NVIDIA-only, and Unsloth's 4-bit model needs an NVIDIA card. Measurements per card, the
What differs from NVIDIA for now: pictures are read by the image encoder on the CPU (`--vision cpu`, about 3 s per
picture at the default 300 image tokens on 8 cores), `--calibrate` is NVIDIA-only, and Unsloth's 4-bit model needs an NVIDIA card. Measurements per card, the
build by hand and the tuning tables: [AMD_HIP.md](AMD_HIP.md).
## Two or three cards
@@ -121,6 +121,35 @@ The same idea, in a container (NVIDIA cards).
to a network. The image has a `HEALTHCHECK` on `/health`, so `docker ps` shows the container
healthy once the model is loaded, and `GET /v1/status` says what it is running.
## Older CPUs (experimental)
Strata's ready-made engine needs AVX2 (Intel Haswell 2013, AMD Zen 2017 or newer). Since 0.1.39 an older CPU - AVX
only (Sandy Bridge / Ivy Bridge, Xeon E5 v1/v2, AMD Bulldozer) or SSE4.2 only (Nehalem / Westmere, Xeon X5600) - runs
as an **experimental** build that setup compiles on that PC. Run setup as usual: it says the CPU has no AVX2, warns
that this is experimental and slow, and compiles the engine (10-20 minutes, once; Windows needs the Visual Studio Build
Tools and the CUDA Toolkit, which setup offers to install). It does not stop. The ready-made engine still refuses such
a CPU, and says so.
What differs in that build (`STRATA_ISA_FLOOR=avx` or `none`, chosen from the CPU):
- ggml-cpu is compiled for AVX or SSE4.2 instead of AVX2, and the CPU's share of the experts runs on its kernels.
Strata's own AVX2 / AVX-512 kernels stay in the program but are used only on a CPU that has them.
- Only the i-quant models' packs run (every model setup installs on such a CPU); the AVX-512 Q2_0 pack does not.
- Speed: the GPU part is unchanged and the CPU part is slower, so a GPU with more VRAM (more experts cached there)
helps most. With the older paths forced on a Ryzen 5 7600 + RTX 5070 and the expert cache held at 1500 slots, greedy
decode went from 26 to 11-17 tok/s with the AVX build and to 3.5-3.8 tok/s with the SSE4.2 build (Coder and IQ3_XXS,
10 prompts each, one run). An old Xeon with DDR3 will be slower than these.
- Untested on a real old CPU by us. It was checked here by forcing the older paths on a Ryzen 5 7600
(`STRATA_FORCE_ISA=avx` or `sse`): the answers were sane and passed the engine's checks, but not the same token for
token as the normal build (ggml-cpu rounds the CPU experts differently, as the AVX2 kernel does from the AVX-512
one). Contributors ran earlier versions of the same approach on a Xeon E5-2680 (2.87 tok/s with a GTX 1080 Ti)
and on Xeon E5-2687W / X5690 machines.
- AMD cards: Linux only (setup compiles the AMD engine there anyway); NVIDIA on Windows and Linux.
- Two-socket Xeons: memory on the other socket is slow; keep the RAM on one CPU if you can.
By hand: `cmake ... -DSTRATA_ISA_FLOOR=avx` (or `none`) builds it; `STRATA_ISA_FLOOR=avx` set for setup builds it on
any PC (to try it). Problems and results on real hardware are welcome as GitHub issues.
## Updating
**`UPDATE.bat`** (Linux: `./update.sh`) updates Strata without starting the model - for when the GPU is busy with
@@ -145,12 +174,19 @@ says so and starts the engine you had.
kept, only their names. Another browser or a private window starts empty; clearing the site's data deletes them.
- **How the model starts:** `strata-<model>.json` in the Strata folder (context, GPUs, host, API key, ...), written
by setup; next to it `run-<model>.bat` / `.sh`, the log `strata-<model>.log` and, when you use "Use for other
apps too", `strata-<model>.shared-settings.json`.
apps too", `strata-<model>.shared-settings.json`. Running setup again for the same model (another context, say)
rewrites the keys setup writes (`exe`, `args`, `port`, `gpu`, `host`, `api_key`, `vision`, ...) and keeps the
ones you added (`sampling`, `mcp_servers`, `mcp`, `cors_origins`, ...); the earlier file is kept as
`strata-<model>.json.bak` (0.1.39). Engine options you added to `"args"` by hand are not carried over: setup names
them, and you add them again.
- **The model files** (`models/`, `packs/`, `mtp/`, 70-120 GB): in **`Strata-data` next to the Strata folder**, or
wherever `--data-dir` put them.
- **Where that data folder is:** `%APPDATA%\Strata\settings.json` on Windows, `~/.config/strata/settings.json` on
Linux ([details](DETAILS.md#before-you-start)).
- **The program itself:** `.venv/`, `engine/` and `third_party/` in the Strata folder.
- **Where github.com cannot be reached:** a build from source downloads llama.cpp's source (ggml, gguf-py) from
GitHub at the commit `LLAMA_CPP_COMMIT` in `setup.py` names. Put that commit's tree in `third_party/llama.cpp`
yourself (from a mirror or another PC) and setup uses it instead (#585).
## Setup's questions
@@ -161,7 +197,7 @@ says so and starts the engine you had.
covering factor; `--rope-scaling`/`--rope-scale` override) ([details](DETAILS.md): "Context extension past
262K").
- **Images?** Whether it should also read pictures. The image encoder (0.9 GB) runs on the GPU (0.1-0.5 s per
picture, ~1.4 GB of VRAM kept free for it) or on the CPU (10-30 s per picture, nothing on the GPU); with AMD cards
picture, ~1.4 GB of VRAM kept free for it) or on the CPU (about 3-13 s per picture, nothing on the GPU); with AMD cards
on the CPU for now. [Details](DETAILS.md#images-vision).
- **Experimental speed projection?** Off unless you say yes - [read what it does](DETAILS.md#experimental-speed-projection-experimental-off-by-default)
first. It changes how the model answers, and only the original model offers it.
@@ -187,6 +223,8 @@ START-HERE.bat --port 8081 another port
START-HERE.bat --gpu 1 another GPU (setup picks the one with the most VRAM)
START-HERE.bat --gpus 0,2 several GPUs sharing the model
START-HERE.bat --vram-reserve-mib 2048 leave 2 GB of VRAM free for other programs (remembered)
START-HERE.bat --no-browser do not open the chat page when the model is ready (remembered;
--browser undoes it)
START-HERE.bat --setup --backend hip the AMD engine on a PC that also has an NVIDIA card
START-HERE.bat --setup --host 0.0.0.0 --api-key <secret> reachable from other devices, with a key
START-HERE.bat --calibrate tune the engine for this PC (about 5-10 minutes), then start
+652
View File
@@ -0,0 +1,652 @@
# Strata on an Intel Arc
Strata's engine is CUDA. On an Intel Arc it runs as **Strata's own engine, ported to SYCL** (`sycl/`, the
section "The engine itself on Intel" below), behind the same Strata server: OpenAI and Anthropic APIs, streaming,
tool calls, MCP and the web app all unchanged. llama.cpp's SYCL backend is the comparison point in the tables (it
runs the same GGUF at about a third of the speed).
Everything Intel-specific lives in `sycl/`; no shared file of upstream's is changed, so upstream merges stay clean.
`sycl/setup_intel.py` and `sycl/serve/server_intel.py` wrap upstream's `setup.py` and `serve/server.py` from
outside.
Written for and measured on an **Arc Pro B70 (32 GB)** running the Coder (IQ1_M) on Ubuntu 24.04, in a
PCIe 3.0 x8 slot (the card trains at Gen3 x8 there; it can do Gen5 x16).
## What you get (2026-10-01)
| | NVIDIA (Strata engine) | Intel Arc, Strata SYCL port | Intel Arc, llama.cpp |
|---|---|---|---|
| models | all four | the Coder IQ1_M; the original IQ2_XS too (experts beyond VRAM in the host mirror: 58-64 tok/s) | the Coder (fits VRAM) |
| model files | the same GGUFs | the same GGUFs + a native pack | the same GGUFs |
| where the model lives | experts in RAM, hot ones on the card | every expert in VRAM (`--stream-experts`: no host copy), shard 2's lookup table read from the SSD by row | all of shard 1 on the card, shard 2 paged from disk |
| RAM needed | 32-64 GB | little (23 GB is fine) | little (23 GB is fine) |
| decode, Coder IQ1_M | 44-51 tok/s on an RTX 5070 | **78.2 tok/s** (19-token prompt, 256 greedy tokens), **75.7** after a 2,184-token prompt, 69 after 40K, 66.6 after 128K, 55.9 after 256K; 48-72 through the API | 23-25 tok/s |
| prompt reading | ~1,870 tok/s | **790 tok/s** at 2,184 tokens, 1,117 at 40K, 888 at 128K, 757 at 256K | ~150 tok/s (424 at 105K) |
| speculative decoding (MTP) | yes | yes (the base checkpoint's draft layer; 70-85% of drafts accepted on code) | no |
| images | yes | not yet | not yet |
| context | up to 262K | 256K measured, 56 tok/s decode there (`--kv-resident`: the KV in pinned host memory, the attended window in VRAM) | 131K measured ceiling |
Decode speed with speculative decoding depends on the text: code drafts well, prose less so (see "Speed depends
on the text"). The SYCL numbers are greedy runs of the engine (as in "How to run it by hand" below, 256 new
tokens) on engine 0.1.31-sycl (2026-10-01; 0.1.32-0.1.38-sycl reproduce them exactly);
"Decode round 2" below lists what each change bought.
## Setup
Build the engine and its runtime image first ("How to build it" below), then:
python3 sycl/setup_intel.py [setup.py's options, e.g. --model IQ2_XS --context 32768 --port 8085]
This is upstream's `setup.py`, run with the Intel steps swapped in (it imports setup.py and replaces those steps;
setup.py itself is unchanged). The model choice, download, pack, tokenizer, MTP draft layer and the context and KV
questions are setup's own. What changes:
- **GPU check:** the Arc is found in sysfs (vendor 8086 under `xe` or `i915`) and offered through setup's AMD
path. That is the path that builds locally and has no images.
- **Engine step:** it uses the SYCL build (`build-sycl-aot/strata`, run in the `strata-sycl-dev` image by
`sycl/serve/strata-sycl.sh`) instead of compiling CUDA or HIP.
- **RAM rule:** this does not apply. The CUDA engine keeps every expert in RAM; the port streams them from the
GGUF into VRAM (`--stream-experts`), so RAM only decides the KV streaming. `--check` lists what fits by VRAM.
- **The config:** it uses the container's paths and `"backend": "sycl"`. The VRAM reserve is 1,024 MiB up to
32K and 2,048 MiB with 4,096-token prompt chunks above that. KV streaming (`--kv-resident 32768`) is on from
64K up when the RAM holds the KV. A `model_switcher` or `sampling` block from an earlier config is kept.
`run-<model>.sh` starts `sycl/serve/server_intel.py`.
If a future `setup.py` drops a step this relies on, it stops with a message instead of writing a wrong config.
The models, packs and checkout must sit under the folder `strata-sycl.sh` mounts at `/work` (the one above the
checkout, or `STRATA_SYCL_ROOT`).
Then `run-<model>.sh` (or `sycl/setup_intel.py` again) starts the model. The first start of a 30 GB model
takes about two minutes. `--port N` and `--host 0.0.0.0` work as in upstream's setup.
## Things that matter on this GPU
- **`SYCL_CACHE_PERSISTENT` must be 0.** The persistent JIT cache segfaults on Xe2 during the first
compile. The start script sets it; if you run llama-server by hand, do too.
- **The whole model goes on the card** (`--n-gpu-layers 999`), except `per_layer_token_embd.weight`,
the single 28.8 GB tensor of shard 2. `--override-tensor per_layer_token_embd=CPU` keeps it in host
memory, mmapped and paged from the SSD by row, which is how the model's authors serve it. Host RSS
stays around 2 GB.
- **Thinking.** The model reasons before it answers. Strata's web app has the setting; for an API
client, put `{"reasoning_effort": "none"}` in `strata-<model>.shared-settings.json` next to the
config, or send `chat_template_kwargs: {"enable_thinking": false}` per request. Without one of
those a short `max_tokens` is spent entirely inside the think block and the answer looks empty.
- **`/health` says 503 while loading**; the server polls `/props` instead.
## How much context fits
The architecture keeps this small: only every fourth layer is full attention (12 of 48); the other
36 are gated-delta-net layers with a fixed-size recurrent state that does not grow with context.
Per token, the 12 attention layers keep 2 KV heads x 256 x (K + V) at q8_0, plus the
sparse-attention indexer's keys. **Measured: about 20 KiB per token** (VRAM grows 0.6 GB per 32k
of context with everything else unchanged).
| context | VRAM in use, model loaded | headroom on 32 GB | status |
|---|---|---|---|
| 32,768 | 28.4 GB | 3.5 GB | measured, the default |
| 65,536 | 29.0 GB | 2.9 GB | measured, loads |
| 98,304 | 29.6 GB | 2.3 GB | measured, loads |
| 131,072 | 30.3 GB | 1.5 GB | **measured, the practical ceiling**: a 104,798-token prompt (79% of the window) read in 247 s at 424 tok/s, then answered |
| 163,840 | ~31.0 GB | ~0.9 GB | not attempted: under the 1.2 GB safety margin |
| 262,144 | ~32.6 GB | none | **does not fit - asking for it took the host down** |
**Do not ask for more than fits.** On this driver a GPU allocation past VRAM does not fail: the xe
driver evicts buffers into host RAM, the kernel runs out of memory, and the machine livelocks until
its hardware watchdog resets it. A 262K request did exactly that on 2026-09-29, and llama.cpp's own
"failed to fit params" check fired too late to prevent it. Compute the KV size first and leave
1.5 GB free.
Prompt reading gets faster with size, because the work batches better: ~150 tok/s on a 2.7k-token
prompt, 424 tok/s on a 105k-token one. So a full 128k window costs about five minutes to read,
not fifteen.
## The engine itself on Intel: the SYCL port (`sycl/`)
Everything above runs the model through llama.cpp. This is the other half: Strata's own engine - the 50 CUDA
kernels and the host code that drives them (streams, events, graph capture, pinned memory) - built for the
Arc with oneAPI. It is a migration of the tree, not a new backend: the engine has no backend seam to slot
into.
**Layout.** `sycl/src` and `sycl/include` mirror the tree and hold only the files the port changes; the
SYCL build (`sycl/CMakeLists.txt`) takes every other source from the original location. No upstream file
is edited. `sycl/include/dpct/` is the vendored SYCLomatic helper library, so the port builds without the
migration tool.
**How it was made, so it can be redone.**
1. `sycl/tools/Dockerfile` - the dev image: the llama.cpp SYCL image plus SYCLomatic (`dpct` 2025.3),
ninja, and the CUDA 12.8 headers that `sycl/tools/get-cuda-headers.sh` pulls out of NVIDIA's pip wheels
(dpct parses CUDA; it needs the headers, not the toolkit).
2. `sycl/tools/migrate.sh` - writes a compilation database for the 86 CUDA-touching translation units and
runs dpct over them. 85 migrate; dpct reports no line it could not migrate, and about 1,400 advisory
notes.
3. `sycl/tools/fixups.py` - what dpct got wrong or could not do, as an idempotent script with a reason per
item. The ones that mattered:
- CUDA's null stream means the default stream; dpct turned it into a null `sycl::queue*`. Every
stream cast now goes through `strata::q_of()` (`sycl/include/strata/sycl_queue.hpp`).
- `__ldg((const float*) p)` came out as `*p`, reading one byte of a float scale (two sites, s_gemv).
- `__fadd_rn(a, b ? c : d)` lost its parentheses (two sites).
- the ggml lookup tables were threaded through kernel parameters with the wrong table per template;
they are plain `static const` arrays read from device code, as ggml-sycl does.
- helper headers renamed since dpct 2025.3 (`entangle`, `chunked_partition`), graph introspection and
`cudaGraphUpload` (no SYCL equivalents), `%globaltimer` (the stage profiler reads zeros).
4. `sycl/tools/build.sh` - configure + build with icpx inside the image. Two compiler flags are load-bearing:
`-fp-model=precise` (icpx defaults to a fast FP model) and `-cl-fp32-correctly-rounded-divide-sqrt`
for the device compiler - the Arc's fp32 divide is not correctly rounded by default (OpenCL allows
2.5 ulp), and Strata's quantizers are byte-exact against ggml through `amax / 127`. Measured: without
it `quantize_act_parity` has 303k mismatches, with it none.
**Where it stands (2026-09-29).** The whole tree builds and links: the `strata` binary and 22 kernel
parity tests. On the B70, 19 of the 22 pass byte-for-byte or within their tolerances; the other three
need model fixtures (`iq_parity`, `ple_parity`) or the tensor-core kernel below (`qsa_prompt_attn`).
| parity test | result |
|---|---|
| bf16_gemv, cvec, dequant_s2, elementwise, gdn, gr, kv_q4, kv_q8, kv_stream, qsa, quantize_act, rope, router_top10, s2_gemv, s2_gemv_q8, s_gemv, s_gemv_q8k, sampler, shared_expert | pass |
| iq_parity, ple_parity | need fixtures (a `logs/iq_fixture` directory, a Q2_0 shard) |
| qsa_prompt_attn_parity | needs the tensor-core kernel (below) |
| kv_hybrid_parity (0.1.27, hybrid K8V4 KV) | appends, gathers and attention pass; its last step is that same tensor-core kernel |
**The engine end to end (2026-09-29, later the same day).** It generates. Prompt "Write a Python function
that returns the n-th Fibonacci number", greedy, 64 tokens:
> `<think>` The user wants a Python function that returns the n-th Fibonacci number. Let me consider the
> options: 1. **Recursive approach** - Simple but O(2^n) time complexity, very slow for large n. 2.
> **Iterative approach** - O(n) time, O(1) ...
Measured on the same card, same 2,185-token prompt, 64 greedy tokens (AOT build, `--spec 2`,
`--no-prefill-borrow`), against llama.cpp's SYCL build serving the same GGUF:
| | llama.cpp SYCL | Strata SYCL port |
|---|---|---|
| prompt reading | 138-319 tok/s | **560 tok/s** (693 at 2,000 tokens, 180 at 300) |
| decode, suffix drafter only (`--spec 2`) | 24.2-26.0 tok/s | 20.2-20.8 tok/s |
| decode with the MTP draft layer (`--spec 4 --mtp`) | - | **42.7 tok/s** at the 2,185-token context (81% accepted, 2.9 tokens/round); 45.3 on a short prompt |
Prompt reading is where Strata's design pays (oneMKL GEMM over dequantised experts, the whole model on
the card). Decode is at ~80%: a speculative round costs ~53 ms whatever its size and the suffix drafter
is accepted 9% of the time, so nearly every round yields one token; `--spec 4` (16.9 tok/s) was worse
than `--spec 2` (20.8) for that reason. The kernels themselves run at 130-160 GB/s of weights on a card
that streams 600 GB/s (`sycl/probe/bw.cpp`, incompressible data) - the same class llama.cpp reaches -
and five variants of the hot Q6_K matvec (lanes per row, rows per warp, unroll, 16-byte loads, software
pipelining; `mmvq_bench`) all landed in that band. The draft source was the lever: the base
Qwen3.8-Flash-Next checkpoint's MTP layer (`tools/mtp_fetch.py fetch`, `mtp_pack.py --experts q2_0`,
`mtp_rt.py`; 4.9 GB downloaded, 809 MiB of VRAM) drafts for the Coder fine-tune at 87% acceptance:
3.4 tokens per 53 ms round, 45 tok/s, the same greedy tokens. The `mtp.cpp` path ran on the first try
under the same flags.
Without AOT the runtime JIT-compiles every kernel on first use, ~47 s the first time a process runs;
`SYCL_CACHE_PERSISTENT=1 SYCL_CACHE_DIR=<dir>` keeps that across runs (15 MB).
How to run it by hand (a greedy test run, the way every number in this section was measured; inside the
`strata-sycl-dev` image, AOT build in `build-sycl-aot/`):
```
STRATA_VERIFY_DEVICE_PLAN=1 STRATA_VERIFY_NO_HOST=1 \
build-sycl/strata --pack <iq pack> --native <shard1> --ple-gguf <shard2> \
--expert-profile data/expert-profile-coder.bin --expert-cache auto --stream-experts \
--prefill auto --spec 4 --spec-min-p 0.5 --max-context 8192 --tokens <ids> --max-new 64 --greedy
```
- `--stream-experts` (this port): no resident host copy of the experts; every one of the 12,288 goes from
the GGUF into the VRAM cache through a small staging ring (`GgufExpertSource`). Upstream needs 32 GB of
RAM for this model; with the flag the engine runs on 23 GiB.
- `STRATA_VERIFY_DEVICE_PLAN=1`: the GPU plans each layer itself (upstream's E-6; off by default there).
- `STRATA_VERIFY_NO_HOST=1` (this port): the host waits for the whole window graph instead of per-layer
rings. Only valid with every expert resident, which is the case on a 32 GB card.
- `--no-prefill-borrow`: the prompt path must not lend expert slots (a lent expert is served from the host
behind a flag the GPU does not see reliably here; long prompts hung without it).
What the port had to get right beyond compiling (each is an entry in `sycl/tools/fixups.py` or a flag):
- **Host-mapped flags.** Strata's decode is a GPU/CPU handshake through host-mapped memory. `volatile`
device loads do not bypass the caches on Intel; system-scope atomics do, and they are the only thing
measured to work (`sycl/probe/doorbell.cpp`, six variants). Host-to-device visibility *during* a kernel
stays unreliable on this platform, which is why the all-resident path waits for the window instead.
- **Bounded spins.** A device spin that never sees its flag is not a hang of one process: the xe driver
times the queue out and resets the GT node by node - a window graph has 2,400 - and the card stays
wedged until a reboot (twice). Every spin is capped (`kSpinMax`).
- **32-lane sub-groups.** The kernels are written for warps; dpct pinned 134 of 289 launches, the rest
would run at Xe2's default 16 (`-fsycl-default-sub-group-size=32`).
- **Synchronous copies.** `cudaMemcpy` blocks; dpct's default-queue `memcpy` did not wait. With the
streaming ring that filled the expert cache from overwritten buffers: non-deterministic residuals, NaN by
layer 5. Found with the per-layer residual ladder (`STRATA_VERIFY_DEBUG=1` prints R after every layer).
- `native_expert_parity` hand-ported: the GPU native expert kernel matches ggml's float reference on real
IQ1_M rows (rel 1.1e-2, the same class as the CPU path).
Profiling: `sycl/tools/Dockerfile.unitrace` builds the dev image with Intel's unitrace; `unitrace -d` around
the engine plus `sycl/rank_kernels.py` gives device time per kernel. `mmvq_bench` and `native_expert_parity
NATIVE_BENCH=1` time the two hot kernel families in isolation (warm the clocks first: a 5 ms run measures
the ramp, not the kernel).
**After the 0.1.25-0.1.27 merge (2026-09-30, same card, same flags, AOT).** Fibonacci prompt (19 tokens),
64 greedy tokens: decode 46.5 tok/s (85% of drafts accepted). The 2,184-token prompt: 566 tok/s prompt,
38.3 tok/s decode (77% accepted on that text); the draft layer's prompt pass is now upstream's batched one
(38.6 ms for 2,184 tokens, from 105 ms for 19 before it). Parity: 19 of 22, byte-exact `quantize_act` back.
**Speed work, 2026-09-30 (after the 0.1.27 merge).** What moved the numbers, measured on the B70 with the
same prompts as above (outputs identical before and after each change):
| change | effect |
|---|---|
| every window graph and the drafter's graphs captured at load (`STRATA_WARM_GRAPHS=0`: as before) | the first request no longer pays 250-290 ms of captures |
| the commit graph left running while the drafter's round runs on its own queue (`Verifier::commit(wait=false)` + `commit_finish`) | decode 38.3 -> 43.0 tok/s at the 2,184-token context, 46.5 -> 49.8 short |
| upstream 0.1.29 merged (GDN recurrence pipelining, the block-scores read, sampler) | 2,184-token prompt 765 -> 792 tok/s, decode at that context 43.1 -> 45.2 |
| the draft layer's batched prompt pass takes prompts under 64 rows (it fell back to per-6-token graphs) | its prompt cost on 19 tokens 106 -> 16 ms |
| one device module per kernel (`-fsycl-device-code-split=per_kernel`) | the first launch in the prompt path (the embedding gather) 245 -> 1 ms |
| the expert dequant writes each thread's run of FP16 values as one vector store (it wrote them one 2-byte store at a time) | dequant per expert 0.085 -> 0.030 ms; prompt 496 -> 575 tok/s at 2,184 tokens, 720 -> 841 at 8,000 |
| SWAR sign compare/subtract in the expert dots, a local-memory resident-plan kernel, a split-K fused down kernel (bit-identical to the single-token one) | parity-clean, no measurable decode change; kept |
| the PLE row reader's default queue depth 16 -> 64 blocking O_DIRECT threads (`STRATA_IO_THREADS`; 128 and 256 change nothing: the drive tops out near 85k IOPS on the 27k random 4 KB reads a 2,184-token prompt needs) | the rows of that prompt 466 -> 330 ms |
| a short first prompt chunk (`STRATA_PREFILL_FIRST`, default 256 tokens) so the GPU starts while the remaining rows are still being read | 2,184 tokens: 711 -> 765 tok/s, 8,000: 857 -> 986; time to first token about 150 ms less. The chunk boundary moves rounding in the expert GEMMs, so a long greedy continuation can diverge late (token 45 of 64 on the test prompt) |
Draft policy sweep (2,184-token prompt, 128 tokens): windows of 6 (`--spec 4`) at 41.6-43.5 tok/s; `--spec 2`
33.6, `--spec 6` 35-37; `--spec-min-p` 0.3-0.7 within noise. Profiles (unitrace, `strata-sycl-dev:metrics` with
Intel's metrics libraries and `dev.xe.observation_paranoid=0`): the expert dot kernels are ALU-bound (77% XVE
active), the dense projections memory-latency bound (92-128 GB/s at 84-93% occupancy), a decode round is 80-85%
kernel time and ~5 us of launch gap per node over 2,400-2,600 nodes.
**XMX.** oneMKL's FP16 GEMMs already run on the XMX units (30-60 TFLOP/s in `xmx_gemm_bench`); the prompt path is
bound by the dequant that feeds them, not by the products. Two joint_matrix kernels were written and are correct
but lose to the existing paths on this card, so both stay opt-in: `xmx_gemm_iq` (a fused dequant + GEMM straight
from the quantized rows, 4-5x slower than dequant + oneMKL) and `qsa_prompt_attn_xmx` (the port of the mma.sync
prompt attention; `qsa_prompt_attn_parity` passes at 1e-6 of scale, 3x slower than the FP32 fallback per chunk;
`STRATA_PROMPT_ATTN_XMX=1`). Where the prompt time goes now (2,184 / 8,000 tokens): dequant 28% / 27%, GEMMs
22% / 20%, the per-layer host grouping 9.5% / 9%, attention 7% / 14%, embeddings + PLE rows 13% / 5%.
A trap worth knowing: with a native pack `--prefill-until N` does not feed the rest of the prompt through the
token loop (it is skipped for native packs), so the tokens after N are dropped and the model free-runs. Compare
output tokens between paths, never only timings.
**An 80,000-token prompt (2026-09-30).** Where the time goes changes completely at this scale:
- **The VRAM plan.** `--expert-cache auto` fills the card down to `--vram-reserve-mib` *before* the KV state
(about 1 GB in INT8 at 81,920 cells) and the prompt chunk buffers (about 2 GB at `--prefill 8192`) exist. With
the 1,536 MiB default that put 30.95 of the B70's 32.6 GB in use, the driver started migrating buffers and the
run never finished. `--max-context 81920 --kv int8 --vram-reserve-mib 3072 --prefill 4096` peaks at 28.8 GB.
(80k ids also exceed Linux's 128 KB single-argument limit: `--tokens-file`.)
- **Streamed experts.** The reserve costs cache slots (10,348 of 12,288 at 3 GB), and the prompt path streams the
missing experts from the GGUF for every chunk. Two port bugs sat on this path and are fixed: the stream plan
held `GgufExpertSource::blob()` pointers across ~1,900 reads of a 512-slot ring (so blobs were overwritten
before they were copied: the 75 s / 1,065 tok/s measured first was computed partly with the wrong experts), and
the stream-all walk hangs on this card in its first large chunk (the copy engine stops on a barrier). The stager
threads now read the blobs themselves, and the port uses the per-layer routed-only walk (`STRATA_PREFILL_RING=8`
upstream, the port's default; `STRATA_PREFILL_STREAM_ALL=1` restores the other for debugging).
- **Correct result: 80,000 tokens in 101 s = 790 tok/s.** GPU time: expert down GEMM 26.0 s, attention 15.4 s,
dequant 12.7 s, QSA block selection 9.3 s (0.2 s at 8k: it scans every block of the context per query), gate/up
GEMM 8.9 s, gather 6.1 s, the per-layer grouping sync 5.2 s, GDN recurrence 4.8 s. The PLE rows are free at this
scale (97.6% row-cache hits).
- **Decode after such a prompt, and the fix.** Without lending cache slots to the prompt path (`--no-prefill-borrow`,
which the port used from the start) the reserve evicts ~1,900 experts for good and decode after the prompt runs at
2 tok/s. Borrowing lends ~940 slots to the prompt path and refills them in about a second afterwards: 80,000
tokens in 75 s = **1,062 tok/s, decode 35-40 tok/s** after it, at `--vram-reserve-mib 2048 --prefill 4096`.
It costs ~1 s on short prompts (2,184 tokens: 610 vs 792 tok/s), so the port borrows by default only above a
32K context (`--prefill-borrow` / `--no-prefill-borrow` decide explicitly).
**Long contexts (2026-10-01, engine 0.1.32-sycl, setup's flags: `--kv-resident 32768 --vram-reserve-mib 2048
--prefill 4096`, the prompt path borrowing cache slots - the default above 32K).** Same text repeated to length,
then 256 greedy tokens:
| context | KV | prompt | decode after | peak VRAM | experts in the host mirror |
|---|---|---|---|---|---|
| 128K | int8 | 888 tok/s (144 s) | **66.6 tok/s** | 29.9 GB | none |
| 128K | q4_0 | 848 tok/s (151 s) | 62.8 tok/s | 29.7 GB | none |
| 128K | k8v4, KV in VRAM | **1,006 tok/s** (127 s) | **66.8 tok/s** | 30.7 GB | 103 (0.2 GB) |
| 256K | int8 | 757 tok/s (338 s) | **55.9 tok/s** | 30.2 GB | none |
| 256K | q4_0 | 737 tok/s (347 s) | 51.7 tok/s | 30.0 GB | none |
| 256K | k8v4, KV in VRAM | 823 tok/s (311 s) | 52.2 tok/s | 30.9 GB | 865 (1.7 GB) |
Borrowing keeps every expert in VRAM (the prompt buffers live in lent cache slots, refilled after the prompt).
Without it (`--no-prefill-borrow`) the buffers keep their own VRAM and ~1,000-2,300 experts move to the pinned host
mirror: the prompt is 2-5% faster, decode after it 10-23% slower (0.1.31 without borrowing: 128K int8 934 / 59.5,
q4_0 887 / 53.4, k8v4 1,026 / 54.4; 256K int8 784 / 48.7, q4_0 763 / 43.2, k8v4 838 / 42.3 tok/s).
KV streaming (`--kv-resident`) keeps the whole KV in pinned host memory and only the attended window in VRAM, so
the KV pushes no experts out; setup.py turns it on from 64K up and keeps INT8 (the faster of the
two at every size here). Without it the KV pushes experts out and decode after the prompt fell to 4-9 tok/s at
128K-256K (2026-09-30). `--kv k8v4` does not support streaming yet: setup keeps its KV in VRAM, which pushes up to
more experts out; those come from the pinned host mirror over PCIe (planned item 2), so decode holds at 42 tok/s at
256K (it was 3.8 before the mirror). k8v4 reads prompts fastest (no streaming copies) and works through the FP32
attention fallback (its dedicated prompt kernel is the XMX one below).
**XMX prompt attention v2.** 64-cell chunks, vector-packed K^T and V, hi+lo Q in one accumulator per scale group,
one accumulator update per chunk: 1.4-1.5x faster than v1 and correct in all modes (`qsa_prompt_attn_parity`,
and `kv_hybrid_parity` passes completely with it), but still ~2x slower than the FP32 fallback (13.4 vs 5.5 ms per
chunk, INT8, 32K context). This attention is gather-bound: each query position selects its own ~2,000 cells, so
the K/V fetch dominates and only 12 of the 16 matrix rows are real heads. Opt-in: `STRATA_PROMPT_ATTN_XMX=1`
(64-cell chunks, 120 KB of local memory) or `=32`.
**XMX v2 in the full matrix, and why a grouped kernel is not next (2026-09-30).** With `STRATA_PROMPT_ATTN_XMX=1`
(stage timing on, ~8-10% overhead) the long prompts run 23-33% slower than with the FP32 attention: 128K int8 666 vs
960 tok/s, k8v4 596 vs 983; 256K k8v4 506 vs 752, int8 536 vs 718 (q4_0 does not use the XMX kernel). The obvious
fix, gathering the union of neighbouring positions' cells once, depends on how much their selections overlap.
Measured on the last chunk of an 80K prompt (first QSA layer, `STRATA_DUMP_SEL=<file>`): the union of 8 consecutive
positions is 3.3x one position's 2,051 cells (16: 5.1x), and only 12% of a selection is shared by all 8. Grouping
would cut the K/V gather to ~40% but multiply the arithmetic by 3-5x: at best 5-8 s of an 80K prompt. Not built.
**Serving the port (2026-09-30).** `serve/server.py --engine strata` runs the SYCL engine unchanged through
`sycl/serve/strata-sycl.sh`, an `exe` that starts the binary inside the oneAPI runtime image with the serve pipes
attached (paths in the config's `args` are the container's, the data root mounted at `/work`). A config:
```json
{"engine": "strata", "exe": "<repo>/sycl/serve/strata-sycl.sh",
"args": ["--pack", "/work/pack", "--native", "<shard 1>", "--ple-gguf", "<shard 2>",
"--expert-profile", "data/expert-profile-coder.bin", "--expert-cache", "auto", "--stream-experts",
"--prefill", "auto", "--spec", "4", "--spec-min-p", "0.5", "--mtp", "/work/mtp/rt",
"--max-context", "32768", "--kv", "int8", "--vram-reserve-mib", "1024"],
"sampling": {"temperature": 0.6, "top_p": 0.95, "top_k": 20, "repetition_penalty": 1.05}, ...}
```
Measured through the OpenAI API with those sampling defaults: 36 tok/s decode on a first turn, 33 on a follow-up
(which reuses the conversation's cached prompt), against 24-26 for llama.cpp on the same card. After the decode work
(2026-10-01 build): 48-72 tok/s on a ~190-token answer, prompt included (the first request after a start is the slower
one); 55-59 tok/s over 5,000-18,000-token answers on the 2026-09-30 build. The reserve matters:
with `--stream-experts` there is no host copy of the experts, so any expert left out of VRAM is read from the SSD
and computed on the CPU whenever it is routed. At 1,536 MiB the cache came up 128 experts short and decode fell to
5-10 tok/s; 1,024 MiB fits all 12,288 with 2 GB of VRAM still free.
**Logprobs (2026-10-02).** `/v1/chat/completions` takes OpenAI's `"logprobs": true` and `"top_logprobs": 0..20`,
streamed or not, through `sycl/serve/server_intel.py`; the reply carries `choices[0].logprobs.content[]` (`token`,
`logprob`, `bytes`, `top_logprobs`), and with thinking on the thinking tokens under `reasoning_content`. The engine
(the port only) gets `logprobs=K` on its GEN line and writes an `LP logprob id:logprob ...` line after each `T`
line, from the verify window's head logits - the distribution the token was taken from, before temperature,
penalties and sampling. Upstream's server ignores the lines, and an engine never asked writes none. Each token
costs one 1 MB logits row copy when asked. Measured (IQ2_XS, "capital of France"): "Paris" at -0.0019 (p 0.998), the
same answer and no measurable time without it. Not on `/v1/completions` or the Anthropic endpoint yet.
**The Monitor tab on Intel.** `sycl/serve/server_intel.py` is `serve/server.py` with two additions made at run
time:
- **GPU readings.** When NVML has no card, it plugs `sycl/serve/xe_telemetry.py` into `serve/telemetry.py`'s
`gpu_reader`. That reads the Arc through sysfs (temperature, power and its cap, the PCIe link the card
trained at). Load and VRAM come from `/run/gpustat.json`: xe reports them per client in `/proc/*/fdinfo`,
which only root can read, so a small root sampler writes them (`sycl/tools/gpustat.py`; install it with its
`gpustat.service` as its header says).
- **A Model menu.** A run config may name a `model_switcher` (an RPC taking `{"mode": m}`) for a host that swaps
models on one card. The web app's header then gets a Model menu (`sycl/serve/web/switcher.js`, injected into
the page).
**Speed depends on the text.** Decode with speculative decoding tracks how often the draft layer guesses right.
The served engine at 32K context: 45-51 tok/s on the test prompt (continuing a Fibonacci function, 77-85%
of drafts accepted), 30-39 tok/s on a chat answer with prose (55-72%); sampling and the repetition penalty cost
nothing measurable. The engine used to abort at exit in serve mode (a queue wait in a destructor after the
runtime's teardown began); it now exits directly once its requests are done.
**Planned (2026-09-30), in order.** Ranked by payoff on this card; the first three were taken first.
1. Expert dot products on XMX in integer mode: a decode window (up to 6 tokens) fits one INT8 DPAS (1-8 rows), the
i-quant grids decode to small integers, the activations are already INT8. Today: scalar dp4a, ALU-bound (77%).
**Parked**: three `joint_matrix` versions (opt-in `STRATA_EXPERT_XMX=1`, do not enable) were 1.4x and 2-3x slower
than dp4a, and the third (B filled in place with `joint_matrix_apply`) hung the GPU. At 1-6 rows the grid decode
and the packed-B layout cost more than the DPAS saves.
2. Experts missing from VRAM read from pinned host memory over PCIe instead of the SSD (the 256K decode collapse).
**Done**: at start-up every expert without a VRAM slot is read into a pinned host mirror (`STRATA_MIRROR_MIB`, by
default free RAM less 4 GiB), and the device-built verify plan points the expert kernels straight at it. Before,
that plan dropped non-resident experts, so output with misses was wrong. Forced test (`--expert-cache 8000`,
1,879 experts / 3.6 GiB out of VRAM): decode 2.6 -> 40.9 tok/s (full cache 43.3), prompt 451 -> 640 tok/s
(776), output tokens identical to the full-cache run. About 3.6 GiB of VRAM can go to context for ~6% of decode.
3. KV streaming (`--kv-resident`) from 64K up: only the attended window of the KV in VRAM. **Done**: 256K decodes at
31 tok/s (from 4-5).
4. QSA block selection on XMX: every query against every pooled block, a dense product that grows with the context.
5. The hot decode kernels re-tuned for Xe2's native 16-wide sub-groups (twice the registers per thread).
**Tried, no gain**: SIMD16 builds of the multi-column mmvq and the wide Q6_K kernel (`STRATA_MMVQ_SG`: 16 all,
1 IQ4_XS only, 2 short outputs only; default 32). Alone (`mmvq_sg_bench`) SIMD16 is up to 1.45x faster on IQ4_XS
and 1.3x on the 640-row shared-expert projections, 10-25% slower on the large K-quant ones; in the engine no
setting beats SIMD32 beyond run-to-run noise (SIMD32 itself lands at 39 or 45 tok/s). Output tokens identical.
The same bench shows the real headroom: the dense decode kernels stream 100-280 GB/s of a 608 GB/s card
(Q6_K, the most common type, ~146 GB/s at 2-4 columns).
**Found and fixed: misaligned loads.** A Q6_K block is 210 bytes, so every block's `ql`/`qh` runs start only
2-byte aligned, and the B70 splits a misaligned 16-byte load into pieces. The same weights repacked at a
224-byte stride ran 2.3-4.7x faster (`q6k_align_bench`). Without changing the layout, the wide Q6_K kernel now
does two aligned 16-byte loads and a shift (`load16_a2`; both stay inside the block) and takes every Q6_K
shape and window width (it used to be gated to n_out >= 4096, 1-4 columns): 2560->10240 at 1/2/4/6 columns
160/145/141/101 -> 493/432/331/273 GB/s, the 248K-row head 150 -> 407 GB/s at 1 column. Output tokens
identical. **Coder decode 44.9 -> 54 tok/s** (2k prompt, greedy). `STRATA_MMVQ_A2=0` restores the old path.
Still misaligned: IQ4_XS (136 B, 8-aligned, ~100 GB/s), IQ4_NL (18 B), Q8_0 (34 B); Q4_K/Q5_K are aligned
but use 4-byte loads in the multi kernel (230-280 GB/s).
**Decode round 2 (2026-09-30), from a unitrace of 256 decoded tokens.** Each step measured on its own, 19-token prompt,
256 tokens, two runs each, output tokens identical throughout:
| | decode |
|---|---|
| after the Q6_K alignment fix | 53.9 tok/s |
| + `resident_plan` grouping in parallel (thread 0 alone took 73 us per layer, ~3.5 ms a round) | 57.1 |
| + the GR down kernel reads the activations directly (the 60 KB SLM tile per group capped occupancy) | 58.7 |
| + wide 16-byte-load kernels for Q4_K, Q5_K, IQ4_XS (codebook in registers) | 62.6 |
| + wide kernels for Q8_0 (MTP dense, shexp down of layer 0) and IQ4_NL (shexp down); Q6_K's activation loads aligned | 65.6 |
| + the GR down projection sliced by columns (each group stages one 128-column slice of xn, 4 lanes per row) + a fixed-order reduce | 70.6 |
| + IQ4_NL expert dots (the down projection in 39 of 48 layers): codebook in registers, weights as aligned 4-byte loads, bitwise the same ints (2026-10-01) | 76.1 |
| + GR norm per (token, stream), bitwise the same; aligned weight loads in the IQ3_XXS/IQ3_S/IQ2_S gate/up dots | **77.9** |
The last step changes the output: identical for 146 tokens, then a near-tie after a comma goes the other way (the new
kernels sum in a different order). Q8_0 kernels 4-6x (189 -> 31 us at 2560 x 10240, 2 columns), IQ4_NL 640 -> 2560
28 -> 6.6 us. The aligned-load helper only loads its second chunk when the address is unaligned, so it never reads a
16-byte chunk without a needed byte and cannot cross a page at the end of an allocation. `STRATA_MMVQ_WIDE_32=0`
restores the old Q8_0/IQ4_NL kernels. The sliced GR down kernel (GR read 108.6 -> 76.5 us at 6 tokens, sums equal to
~1e-7) flips that near-tie back: its output is the original one. `STRATA_GR_DOWN_SLICED=0` restores the direct kernel.
Kernel level: IQ4_XS 107 -> 323 GB/s at 2 columns (61 -> 212 at 6), Q4_K/Q5_K 1.3-1.6x; the GR read 135 -> 110 us at
6 tokens (bitwise equal). Switches to the old paths: `STRATA_PLAN_PARALLEL=0`, `STRATA_GR_DOWN_DIRECT=0`,
`STRATA_MMVQ_WIDE_K=0`. (`iq_parity` reports 10 "missing fixture" failures with and without the change: the oracle
fixtures are not in the port's tree.)
**Two-speed runs, explained (2026-09-30).** Identical greedy runs decode at either ~45 or ~39 tok/s. A per-gather
trace of the PLE reader (`STRATA_PLE_TRACE=1`) shows the slow runs pay one 226 ms PLE read stall in the first
decode round, after the window graphs are captured; every other read and round matches the fast runs. Prompt time
plus decode time is the same in both modes (4.95-5.17 s): the stall lands either in the prompt's PLE wait or in the
first decode round, so it is a once-per-process cost, not lost throughput. Ruled out: NVMe APST, the I/O scheduler
(`none`), CPU starvation (93% idle during decode), I/O thread count. Compare runs on time to first token + decode.
6. INT8 prompt GEMMs: experts dequantized to INT8, oneMKL/oneDNN INT8 on XMX (half the dequant bytes, 2x rate).
7. Fewer graph nodes per decode round (~2,500 at ~5 us): norm+rope, scores+top-k, gate+quantize fused.
8. Wider speculation (two draft branches per verify window): the kernels are latency-bound, so it is nearly free.
9. A model bigger than VRAM: the original Qwen3.8-Flash-Next IQ2_XS (35.5 GB of experts; the same path suits Q2_0
and Swift 1.5) on the B70 with 23 GB of RAM. ~24 GB of experts in VRAM, the rest (~10-12 GB) in the pinned host
mirror the device plan reads over PCIe (item 2). The port has the IQ2_XS/IQ3_XXS/Q2_0 expert kernels. Open: whether
a 10+ GB pinned mirror fits beside everything else in 23 GB, and decode with that share of experts on a Gen3 x8
link (3.6 GB mirrored measured 40.9 tok/s; expect less). Needs the 68 GB download.
**Done (2026-10-01)**: shard 1 downloaded (39.2 GB; shard 2 is byte-identical to the Coder's, so a hard link),
a native pack (`tools/iq_pack.py`, 6 s), the original model's expert profile, the same MTP draft layer. 18,329 of
24,576 experts in VRAM (24.6 GiB), 6,247 in the pinned host mirror (8.4 GiB, 9 s to fill), host RAM never below
12 GB free. **Decode 50.8 tok/s** on the 19-token prompt and **60.6** after 2,184 tokens, prompt 549 tok/s;
coherent, correct answers on both. Q2_0 and Swift 1.5 (similar size) should behave the same; IQ3_XXS/IQ3_S
(43-50 GB of experts) would need 19-26 GB mirrored, more than 23 GB of RAM allows.
**Keeping up with upstream.** A merge of upstream `main` into `b70` leaves the copies in `sycl/` behind
wherever upstream touched a file they mirror. They are refreshed by re-migration, not by hand (done for
0.1.25-0.1.27, 2026-09-30):
1. Merge upstream into `b70`. No shared file should conflict: the Intel code is all in `sycl/`. Then check that
`sycl/setup_intel.py --check` and `sycl/serve/server_intel.py --engine mock` still run against the new
setup.py and server.py.
2. Migrate the *old* upstream tree (a `git archive` of the pre-merge commit) with `migrate.sh` into
`sycl-base`, and the merged tree into `sycl-new`; about 3 minutes each in the dev image.
3. `sycl/tools/normalize.sh <dir> <commit>` on both: dpct's file names, the unchanged files, the
message serials, then `fixups.py`. Two runs of dpct on the same source now differ only in the kernel
name hashes it generates.
4. For every file present in both: `git merge-file sycl/F sycl-base/F sycl-new/F`. Files upstream did not
change are left alone. Copies dpct never produced (`verify.cpp`, `mtp.cpp`, `remote_experts.cpp`
include no CUDA header directly) take upstream's diff by hand; new parity tests are copied in and
listed in `sycl/CMakeLists.txt`.
5. A fixup whose pattern upstream changed shows up as a compile error (the PTX gate became an `#elif`
under upstream's `__HIPCC__` guard); extend the fixup, re-run it, rebuild.
**The 0.1.31 merge (2026-10-01).** Upstream 0.1.29 -> 0.1.31 (132 commits) by the steps above: 45 migrated files
changed for real (27 more only in dpct's kernel-name hashes), 38 conflicts resolved, verify.cpp/mtp.cpp/
conversation_state.cpp ported by hand. A symbol audit (the port's feature identifiers counted before and after) is
part of the procedure now: it caught a dropped mirror hook and four doorbell waits that had lost their spin bound
(upstream renamed the loops; `fixups.py` covers both spellings). What the merge needed beyond conflicts:
- **The expert kernels.** Upstream sends the i-quant experts to new multi kernels (one warp per row, 8 rows a
group); the port's grid is sized for its own kernels (8 lanes a row, 32 rows a group), so only a quarter of each
expert's rows were written and the output was end-of-text tokens. The port's kernels stay the default (they also
serve upstream's new Q4_K/Q5_K/Q5_1/Q8_0 experts); `STRATA_EXPERT_SPLIT=1` runs upstream's, with their grid.
- **Prompt-slot borrowing hung** in the first chunk of any prompt with it on (40K, 128K; GPU busy) - fixed after
the 0.1.32 merge, see "Prompt-slot borrowing: the hang" below.
- **Upstream's new tests found three dpct mistranslations from the first migration.** dpct writes `__fadd_rn(a, b)`
as `a + b` without parentheses, so `__fadd_rn(sum, c ? x : y)` became `sum + c ? x : y` (replaced, not added): in
the native QSA indexer's per-token pooled key (the decode path: blocks completed while generating got wrong
pooled keys; prompt blocks come from the batched kernel and were right), in the native QSA scores (an opt-in
path) and in the s2 activation rounding (every value 0; the Q2_0 s2 pack, unused here). Fixed, and `fixups.py`
now parenthesises. `qsa_parity`'s batch-vs-sequential test passes; decode output after long prompts changed.
- **Upstream's new kernels against the port's on the B70** (decode tok/s, Coder 19 / 2,184-token prompts, IQ2_XS
19): port defaults 78.1 / 75.7 / 58.6; `STRATA_EXPERT_SPLIT=1` 65.6-69.5 / 67.2 / 49.1 (-11 to -16%);
`STRATA_GR_V3=1` 73.2 / 72.4 (-4 to -6%). Both stay opt-in.
- **Lanes per row of the port's expert kernels** (`STRATA_GU_LANES` / `STRATA_DOWN_LANES`, 4/8/16/32 at run time):
gate/up 8 or 16 x down 4 or 8 all decode in 37.7-38.6 ms per verify round (2 runs each, Coder, both prompts); tok/s
differences between them are draft acceptance (each split rounds differently). The default (8 / 8) stays.
- **Speed after the merge:** Coder 78.2 / 75.7 tok/s (as before), IQ2_XS 58.6 / 64.2 (from 50.8 / 60.6).
- **Still failing:** (`iq_multi_parity` on IQ2_XS: fixed later, see "Two model-specific bugs"), `s2_expert_grouped_parity` (the s2 path), `kv_hybrid_parity`'s last step
(needs the unported tensor-core prompt kernel). `ple_parity` and `native_expert_parity` need model files.
**The 0.1.32 merge (2026-10-01).** Upstream 0.1.31 -> 0.1.32 (89 commits) by the same steps; the hash-only
differences are now canonicalized before `git merge-file` (the port's kernel-name hashes are kept), which left 10
files with real conflicts. What it needed:
- **Upstream's async commit** (`set_commit_async` / `wait_commit`) maps onto the port's own deferred commit
(`commit(n, err, false)` + `commit_finish`); `wait_commit` is `commit_finish`.
- **Upstream's new hyper-connection read variants** (split / staged, chosen per card by a bit-for-bit self-test at
start) are not used by the port, which keeps its sliced down / split norm read; the self-test segfaulted on the
B70, so on SYCL it runs only with `STRATA_HC_CHECK=1` (open). The port's split-norm kernel was renamed
(`gr_norm_split_port_kernel`): upstream now has one of the same name.
- Two kernel names collided after hash canonicalization (renamed), and fused_gr's per-block shared-memory query is
a fixup now.
- **Result:** every output identical to 0.1.31 (Coder 19 / 2,184-token prompts and IQ2_XS, 256 greedy tokens),
same speeds (Coder 77.7-78.1 / 75.7 tok/s, IQ2_XS 58.5), 40K prompt 1,201 tok/s then 65.1 tok/s decode.
`kv_hybrid_parity` and `qsa_prompt_attn_parity` pass (the tests now turn on the XMX prompt attention they
check); `iq_multi_parity` (IQ2_XS) and `s2_expert_grouped_parity` as before.
**The 0.1.33 merge (2026-10-01).** Upstream 0.1.32 -> 0.1.33 (17 commits): no shared file conflicted (the Intel
code is all in `sycl/`); six engine files changed upstream and were merged by the same re-migration (only those six:
the other files dpct produced differently were left alone). Two conflicts: `--resident-cpu-experts` (upstream's new
`resident_cpu_explicit` beside the port's `--stream-experts`) and the prompt attention's compute-capability check
(the port keeps its XMX dispatch). `sycl/setup_intel.py` and `sycl/serve/server_intel.py` ran unchanged against the
new setup.py and server.py. Every output identical (Coder, IQ2_XS, the 40K prompt with borrowing); `gr_parity` and
`qsa_prompt_attn_parity` pass.
**Prompt-slot borrowing: the hang (fixed 2026-10-01).** From 0.1.31 on, a prompt that borrowed cache slots stopped
in its first full chunk with the GPU at 100% and the host waiting on the compute queue. It was never a borrowing
bug: borrowing is just the only way the Coder streams experts during a prompt (every expert is resident otherwise),
and the streamed path's stager deadlocked. Its threads read experts into a ring of 16 page-locked buffers and, to
reuse one, waited on the copy queue's event for the DMA that last read it (the CUDA form, `cudaEventSynchronize`).
Under the Level Zero v2 adapter that event, once a host thread had waited on it, no longer released the other
queue's barrier that also listed it, and the GPU waited forever - only once a layer streamed more than 16 experts
(the ring wrapped). The v1 adapter (`SYCL_UR_USE_LEVEL_ZERO_V2=0`) and a 256-buffer ring both ran; so did a sync
after every phase (`STRATA_PREFILL_SYNC=1`, kept as a debug switch). The fix: the copy queue writes a sequence
number into page-locked host memory after each DMA and the stager polls it - no host thread waits on a queue's
event. Output is bit-identical to the run without borrowing; borrowing is the default above 32K again.
**Two model-specific bugs found with Swift 1.5 (fixed 2026-10-01).** UkisAI's Swift 1.5 IQ2_XS (setup's `swift`
family, the same GSQ-RCO layout) decoded token 0 forever: NaN logits from layer 13 on.
- **`--stream-experts` read the wrong shard.** Since upstream 0.1.31, `ExpertLayout::gguf_file` is per layer AND role
(`3 * layer + role`; a shard boundary can fall inside a layer). The port's `GgufExpertSource` still indexed it by
layer, so a layer in shard 2 was read from shard 1 at shard 2's offsets: garbage IQ1_M scales, infinities after the
fp16 dequantization. The original model keeps every expert in shard 1 and never noticed; Swift's layers 13-47 are
in shard 2. `STRATA_DBG_NAN=1` now also reports the experts' fp16 GEMM inputs (activations, dequantized gate/up).
- **IQ2_XS products were garbage** (the long-failing `iq_multi_parity` IQ2_XS case): ggml's `vec_dot_iq2_xs` reads
its four 16-bit codes through a `uint16_t*` to an `int2`, type punning the SYCL device compiler does not honour;
the dequantizer, which reads the codes directly, was exact. The codes are extracted by shifts now (the dot and the
expert kernels' `Split<17>`); the test checks against a CPU decode from ggml's tables too and passes. No model here
uses IQ2_XS tensors, Swift included - this was not Swift's bug, only found on the way.
Swift 1.5 IQ2_XS after both: correct answers, 51-118 words of thinking on short questions, 48 tok/s raw decode.
**Load time (2026-10-02).** Starting a model is mostly the expert cache's fill from the GGUF (`--stream-experts`):
the Coder's 23.4 GiB of experts took 64-76 s, the IQ2_XS's 24.9 GiB 91 s, because the fill read each expert's
three slices, copied the blob and waited for the copy before the next read. It is a pipeline now: the slots are
admitted in profile order first (the same placement), the reads go in file order by up to 8 threads into
page-locked batches of 64, and a batch's copies run while the next one is read. Cold page cache: 76.2 s -> 18.8 s
(0.33 -> 1.34 GB/s); a whole Coder start from the engine's launch to its first token 82 s -> 26 s, the IQ2_XS
120 s -> 41 s (its 8.2 GB host mirror is ~9 s of the rest). Output identical. `STRATA_FILL_SERIAL=1` is the old fill.
**The stream-all prompt walk is back (2026-10-03).** It copies every expert the VRAM cache does not hold, layer by
layer ahead of the compute, instead of only the routed ones. It hung on the B70 until the stager and PLE fixes of
2026-10-02 and runs since. It now runs by default when the VRAM holds more than 90% of the (layer, expert) pairs;
past that it would copy several times the routed experts (the IQ2_XS keeps a quarter of them in the RAM mirror: a
2,184-token prompt fell from ~560 to 254 tok/s with it). `STRATA_PREFILL_STREAM_ALL=1` / `=0` force it on or off.
Coder, outputs identical either way:
| prompt | routed-only | stream-all |
|---|---|---|
| 40K int8 | 1,106 tok/s | 1,160 |
| 128K int8, KV streaming | 897 | 920 |
| 128K k8v4 | 1,006 | 1,041 |
| 256K int8, KV streaming | 760 | 780 |
Measured alongside: the 2,184-token prompt reads 784-799 tok/s on both the 0.1.35+PR and the 0.1.38 build (three
runs each), so the 825 seen once on 0.1.35 was session variance, not a regression. The prompt path at 40K (Coder,
GPU timeline): expert dequant 15.7%, expert GEMMs 23.4%, host grouping 6.5%, gather 5.7%, combine 2.4%, prompt
attention 19.2%, QSA select 6.8%, DeltaNet recurrence 5.8%, hyper-connection reads 4.6%. Upstream's sm_90 cluster
kernels (greedy sampler, QSA top-k) would save next to nothing here: the sampler reads ~6 MB a round against a 13 ms
token. Q4_0 tensor-core prompt attention (#452) is not worth porting: the port's XMX prompt attention is 2-3x slower
than the plain kernel it uses for every KV format.
**The 0.1.38 merge (2026-10-03).** Upstream 0.1.35 -> 0.1.38 (83 commits) merged the seven PRs the port had taken
early, so their header forks in `sycl/include` are gone; the merge base for the 3-way merge was the port's own "main +
PRs" migration. New in the port with it: the DeltaNet output norm without its dead FP32 store, two prompt-path
fixes (the fused layout's buffers, the streamed walk's resident lookup), `STRATA_GR_DOWN_MAX4=1` (opt-in; neutral on
the B70: 2,184-token prompt 793 vs 788 tok/s, decode 76.2 vs 76.3). Stubbed in the port: `--peer-device` (a second
GPU as an expert-cache tier: upstream's code is CUDA calls; `open` refuses with a message) and the fused int8 prompt
kernels (#136, part of the MMQ library the port does not build). Off on SYCL: the sm_90 thread-block-cluster greedy
sampler and QSA top-k (no SYCL counterpart; the callers take the plain kernels). `STRATA_GDN_KEYHEAD=1` stays opt-in
(still 8% slower here: 727 vs 788 tok/s). `qsa_prompt_attn_parity` now also tests upstream's Q4_0 tensor-core mode
(#452), which the port does not have: those cases report "refused" (the int8 and fp16 XMX cases pass); a `--kv
q4_0` prompt takes the older kernel, as before. Outputs identical (Coder, IQ2_XS, 40K); 128K int8 KV streaming
892 / 66.8 tok/s.
**0.1.35 and seven open upstream PRs (2026-10-02).** Upstream main 0.1.33 -> 0.1.35 merged as before, then seven open
PRs ported into `sycl/` ahead of upstream (one dpct run of main + all of them, 3-way merged into only the files they
touch; `sycl/tools/merge_upstream.py`). Their header changes live in `sycl/include` until upstream merges them.
| PR | what | on the B70 |
|---|---|---|
| #385 (sergqwer) | stager: a buffer's first job of a generation waits for the previous DMA from it | race fix; same output |
| #463 (constantindjonkam) | decode waits for an adaptive swap before reading the residency table | determinism fix |
| #453 (architectds) | the drafter's batched K/V on the KV-streaming ring | 128K int8: 888 -> 895 tok/s prompt, 66.6 -> 67.0 decode |
| #374 (sergqwer) | the first chunk's PLE rows read beside layer 0 | part of the 2,184-token prompt's 784 -> 825 tok/s |
| #363 (BlueKingMuch) | the PCIe expert call: group stride, fused SwiGLU + q8_1 | IQ2_XS decode +1.7%; re-done on the port's lane kernels |
| #407 (sergqwer) | `--adapt-tuned` (opt-in) | neutral here; stays opt-in |
| #413 (BlueKingMuch) | DeltaNet recurrence per key head | bit-identical but 8% slower prompt here: off (`STRATA_GDN_KEYHEAD=1`) |
#374 needed two port fixes: its next-chunk read assumed every chunk is the full chunk length (the port's first chunk
is 256 tokens: the second chunk's rows were read from the wrong place), and its host wait on the PLE upload's event,
now inside the layer loop, deadlocked the prompt past ~4K tokens under the Level Zero v2 adapter (the stager's bug
again): the upload is marked by a polled sequence number now. #413's gate is an NVIDIA SM-count rule and its parity
test an SM-holding NVIDIA bench (not built). Outputs identical to 0.1.33 (Coder 19 / 2,184 tokens, IQ2_XS, 40K).
**Not ported yet (2026-10-01).**
- Three kernels carry inline PTX (`mma.sync` tensor-core matrix ops, `ldmatrix`, `cp.async`):
`qsa_prompt_attn`, `qsa_select`'s block scores, `native_qsa_score`. The SYCL build takes the "older card"
fallback the CUDA build uses below sm_80. `qsa_prompt_attn` also has an XMX version (`joint_matrix`, opt-in
`STRATA_PROMPT_ATTN_XMX=1`): correct, but slower than the fallback (see "XMX prompt attention v2").
- The ggml MMQ prefill path (`moe_mmq.cu`) needs llama.cpp's ggml-cuda sources; not built.
- AOT device code is what runs: `AOT=bmg-g31 BUILD_DIR=.../build-sycl-aot` (the JIT build costs ~47 s of
compiling on the first window).
## Not done
- Images, on both Intel engines. Strata's vision path encodes with `strata-vision` into embeddings the CUDA
engine reads; neither the SYCL port nor the llama.cpp config wires it yet.
- Speculative decoding on the llama.cpp path (the GGUF carries no draft layer llama.cpp can use). The SYCL
port has it (MTP draft layer, `--mtp`).
## Measured, 2026-10-01, Arc Pro B70, Coder IQ1_M, 32K context, INT8 KV: the SYCL port (engine 0.1.31-sycl)
| | |
|---|---|
| decode | 78.2 tok/s on a 19-token prompt, 75.7 after a 2,184-token prompt (256 greedy tokens, MTP + suffix drafts) |
| prompt | 780 tok/s on 2,184 tokens; 934 tok/s at 128K, 784 at 256K |
| through the API | 48-72 tok/s on a ~190-token chat answer, prompt included (the first request after a start is the slower one) |
| VRAM | all 12,288 experts resident, ~1.9 GB free with everything loaded (`--vram-reserve-mib 1024`) |
| RAM | no host copy of the experts (`--stream-experts`) |
## Measured, 2026-09-29, Arc Pro B70, Coder IQ1_M, 32K context, q8_0 KV: llama.cpp
| | |
|---|---|
| load | ~100 s |
| VRAM | 28.4 of 32 GB |
| decode | 23-25 tok/s; GPU 92% busy at 165 W, CPU idle |
| prefill | 149 tok/s on a 2,701-token prompt; 424 tok/s on a 104,798-token prompt at 131k context |
| quality | correct code on every test; matches the NVIDIA path token for token in spirit, not measured |
+145
View File
@@ -0,0 +1,145 @@
# Intel Arc (experimental)
Strata 0.1.39 includes an **experimental Intel Arc engine**: Strata's own engine ported to SYCL (Intel oneAPI).
maxfridbe wrote it in [#423](https://github.com/Niko1221/Strata/pull/423), with fixes from the people testing it.
The code is in `sycl/`, and the port's own notes, measurements and maintenance procedure are in
[docs/INTEL.md](INTEL.md). This page covers what you need, how to build it, and what has been tested.
**Experimental means:** the Strata maintainers have no Intel GPU. We compile the port and run its kernel tests, but
we have not run it on an Arc. Every result on Arc hardware below comes from the community. The NVIDIA and AMD
engines are unchanged: the Intel build is a separate CMake target, off by default.
There is **no ready-made Intel engine** in the release zips. You build it from source on Linux.
## What has been run, and by whom
| | Hardware | Result | Reported in |
|---|---|---|---|
| Port author | Arc Pro B70 32 GB, Ubuntu 24.04 | Coder IQ1_M: 70-78 tok/s decode, ~790 tok/s prompt; IQ2_XS: 51-64 tok/s; 256K context measured | #423, [INTEL.md](INTEL.md) |
| Community | 2x Arc Pro B70, `--layer-split` | Flash-Next IQ3_XXS 66 tok/s decode, 394 tok/s prompt (with the `stage_room` fix that is now in 0.1.39) | #423 |
| Community | Arc Pro B50 16 GB | Coder IQ1_M ~23 tok/s, IQ2_XS ~25-27 tok/s, up to 128K | #423 |
| Community | Arc B580 12 GB, WSL2 | IQ2_XS ~21 tok/s, Coder ~15 tok/s; **device loss also seen** | #423 |
| Community | 2x Arc Pro B60 24 GB, `--layer-split` | Coder IQ1_M: 56.6 tok/s decode, 428 tok/s prompt (2,129 tokens); Flash-Next IQ2_XS: 58.6-61.3 tok/s decode, 436 tok/s prompt; all experts in VRAM; 8K context | [bench/results/2026-10-04-community-2x-arc-pro-b60](../bench/results/2026-10-04-community-2x-arc-pro-b60/README.md) |
| Community | one Arc Pro B60 24 GB | Coder IQ1_M with 4,042 of 12,288 experts mirrored in RAM: 11.9 tok/s decode (needs the ring-wait fix from the same report) | same |
| Strata maintainers | no Arc | compile check and kernel tests on a CPU device only (below) | this release |
The 0.1.39 port re-migrates 0.1.38's port onto the 0.1.39 engine sources (the #606 NaN fix, the #649 verify
trace, the new prompt paths). 0.1.39's `sycl/` did not compile against 0.1.39's own engine sources (#784: the
`ThreadAffinity` type of #626 and the layer range of `NativeDense::load` from #559), and its ring waits never saw a
slow layer as still running (#866, #867). Both are fixed in 0.1.40, and the two B60 reports below were measured with
exactly those two fixes on top of 0.1.39. **No one has run an unpatched 0.1.39 port on an Arc**, and the other rows were
measured on earlier versions.
The two B60 rows ran `6f32ec0` plus two small `sycl/` fixes (the compile fix and the ring-wait fix), AOT `bmg-g21`, on Ubuntu 24.04 with
`xe`, Level Zero V2, NEO 26.09.37435.12 and oneAPI 2026.1.1, without Docker. Host: Ryzen 5 5600, 64 GB RAM, PCIe 3.0 x8 per card.
Full flags, per-request timings and engine logs are in the report linked in the table.
## What was tested here (0.1.39)
- **Compiles:** Ubuntu 24.04 (WSL2), Intel oneAPI DPC++ 2026.1.1 + oneMKL 2026.1. The whole `sycl/` project
builds with 0 errors: the `strata` engine plus all 157 targets (the kernel parity tests and benches). The build is
SPIR-V (JIT). The AOT build (`STRATA_SYCL_AOT`) was not built here, because it needs `ocloc`.
- **Kernel parity tests on a CPU** (`ONEAPI_DEVICE_SELECTOR=opencl:cpu`, Intel's OpenCL CPU runtime, AMD Ryzen 5 7600):
14 of 25 pass. That is the same set that PR #423's own 0.1.38 port passes on that device: the failures are tight
float tolerances on the CPU's math (rel 3e-6 against a 1e-6 limit), model fixtures that are not present, and two
tests that time out on a CPU. This is a check that the kernels compile and compute, not a test of an Arc.
- **Setup:** `--backend sycl` warns, then hands over to `sycl/setup_intel.py` on Linux (unit tests:
`tools/test_setup_sycl.py`).
- **Unchanged:** the CUDA engine's greedy output, checked byte-identical against the gated 0.1.39 build (Q2_0 and Coder).
The HIP build is not affected (the option is off by default).
**Not tested by anyone yet:** Windows (no native build path; see below), Arc on WSL2 for the 0.1.39 port, the Alchemist
A-series, integrated Arc GPUs (the B390 / Panther Lake in #515; the shared-memory planning does not exist yet), and
images (not wired on Intel).
## What you need
- **Linux**, e.g. Ubuntu 24.04, with Intel's GPU driver (the `xe` or `i915` kernel driver plus the compute runtime /
Level Zero; on Ubuntu, `intel-opencl-icd libze1 libze-intel-gpu1`, or Intel's
[client GPU guide](https://dgpu-docs.intel.com/driver/client/overview.html)).
- **Intel oneAPI**: the DPC++ compiler (`icpx`, 2025.3 or newer; 2026.1 is what was built here) and **oneMKL**. About 5 GB.
- `cmake` 3.24+, `ninja`, `git` (the build fetches ggml unless you point `STRATA_GGML_DIR` at a llama.cpp checkout),
Python 3.
- For `setup --backend sycl` today: **Docker**. `sycl/setup_intel.py` runs the engine in the `strata-sycl-dev`
image built from `sycl/tools/Dockerfile`. Note that the Dockerfile starts from a community llama.cpp SYCL image
(`ghcr.io/snailium/...`), not an Intel or Strata image.
- VRAM: the port keeps the experts on the card (`--stream-experts`). A 32 GB card holds the Coder IQ1_M or IQ2_XS.
Smaller cards mirror part of the experts in RAM and are slower.
## Build (Linux)
Install oneAPI from Intel's apt repository (this is what was used here):
```sh
wget -qO- https://apt.repos.intel.com/intel-gpg-keys/GPG-PUB-KEY-INTEL-SW-PRODUCTS.PUB \
| sudo gpg --dearmor -o /usr/share/keyrings/oneapi-archive-keyring.gpg
echo "deb [signed-by=/usr/share/keyrings/oneapi-archive-keyring.gpg] https://apt.repos.intel.com/oneapi all main" \
| sudo tee /etc/apt/sources.list.d/oneAPI.list
sudo apt update
sudo apt install intel-oneapi-compiler-dpcpp-cpp intel-oneapi-mkl-devel ninja-build cmake git
```
Then build the engine. Either command works: the first goes through the top-level CMake option, the second
configures the `sycl/` project directly.
```sh
source /opt/intel/oneapi/setvars.sh
cmake -S . -B build-sycl -G Ninja -DCMAKE_C_COMPILER=icx -DCMAKE_CXX_COMPILER=icpx -DSTRATA_ENABLE_SYCL=ON
# or: cmake -S sycl -B build-sycl -G Ninja -DCMAKE_C_COMPILER=icx -DCMAKE_CXX_COMPILER=icpx
cmake --build build-sycl --target strata
```
Options:
- `-DSTRATA_SYCL_AOT=bmg-g31` (Arc Pro B70) or `bmg-g21` (B580 / B570 / Pro B60) compiles the GPU code ahead of
time. This needs `ocloc` (Intel's `intel-ocloc` package). Without it, the first start JIT-compiles every kernel,
which takes about 47 s.
- `-DSTRATA_SYCL_PARITY=OFF` skips the kernel tests (on by default). Run them with
`ctest --test-dir build-sycl` on the card.
`setup_intel.py` looks for `build-sycl-aot/strata` or `build-sycl/strata` in the checkout. With the
top-level option the engine is at `build-sycl/sycl/strata`, so either use `-S sycl` or set `STRATA_SYCL_BIN`.
## Setup and running
```sh
./setup.sh --backend sycl [setup's usual options, e.g. --model IQ2_XS --context 32768]
```
This prints the experimental warning and continues with `sycl/setup_intel.py`. That script finds the Arc in sysfs,
uses the SYCL engine you built, and writes the config and `run-<model>.sh`. It still downloads and packs the model
the usual way. To run the engine by hand (no Docker), see "How to run it by hand" in [INTEL.md](INTEL.md).
Things that matter on an Arc (details in INTEL.md):
- **Do not ask for more VRAM than the card has.** On the `xe` driver, an allocation past VRAM can push buffers into
RAM until the machine runs out of memory and stalls. Leave about 1.5 GB free.
- `SYCL_CACHE_PERSISTENT=0`: the persistent JIT cache crashed on Xe2 during the first compile.
- Two cards: `ONEAPI_DEVICE_SELECTOR=level_zero:*` (the image pins `level_zero:0`; `strata-sycl.sh` now passes the
variable through) and `--layer-split`.
- **Arc Pro B60:** the PCI id is `8086:e211` (`lspci -nn`, the kernel's `xe` id list files it with the BMG-G21 cards), `sycl-ls` prints
`Intel(R) Arc(TM) Pro B60 Graphics 20.1.0`, and `ocloc ids bmg-g21` prints 20.1.0, so `-DSTRATA_SYCL_AOT=bmg-g21` is the right target.
`setup_intel.py` knows both B60 ids, `e211` and `e221` (both seen on B60 cards). On a `--layer-split` the startup line "N experts are neither in VRAM nor mirrored"
counts the other card's layers; the lines `100% of the experts resident` that follow are the ones to read. Set `STRATA_MIRROR_MIB=0` there,
or the pinned mirror is allocated and not used.
- **`setvars.sh` and `set -u`:** `source /opt/intel/oneapi/setvars.sh` in a shell with `set -u` stops at `OCL_ICD_FILENAMES: unbound variable`
(oneAPI 2026.1.1). Source it before `set -u`, or run `set +u` around it.
- **Without Docker:** `sycl/serve/strata-sycl.sh` needs Docker. Natively, `source setvars.sh`, export `SYCL_CACHE_PERSISTENT=0`,
`ZES_ENABLE_SYSMAN=1` and `STRATA_VERIFY_DEVICE_PLAN=1`, then run `build-sycl-aot/strata` or `sycl/serve/server_intel.py` with an `exe` that does this.
A normal user is not in the `render` group on Ubuntu, and then `sycl-ls` shows only the CPU device.
- `STRATA_VERIFY_NO_HOST=1` (set by `strata-sycl.sh`) is only valid when every expert is in VRAM. On smaller cards
that path is the one that has hung, and #667 found the likely reason: the GPU does not see the CPU's flag
stores without a system fence.
## Windows
There is no Windows path yet. `setup --backend sycl` on Windows stops and points here. oneAPI exists for Windows,
but `sycl/CMakeLists.txt` uses GCC-style flags (`-mavx512f`, `-fp-model=precise`, `-qmkl`) and the runner is a
bash/Docker script, so a native Windows build would need work. Nobody has tried it. WSL2 with an Arc has been
used by one tester (the B580 row above), but setup cannot detect the card there, because it reads `/sys/class/drm`,
which WSL2 does not have.
## Reporting a problem
Open an issue with: the card, the driver version, `sycl-ls` output, the oneAPI version, the model and flags, and the
engine's stderr. Results from real cards are what move this from experimental to supported.
+57
View File
@@ -0,0 +1,57 @@
# Overlap streamed KV uploads with prefill computation
## Why this helps
When long-context KV lives partly in system RAM, prefill uploads each attention
layer's cached prefix. Waiting for every upload on the compute stream leaves
the GPU idle.
This opt-in change starts the next attention layer's upload on a separate stream
while the current layer continues computing. It reuses the existing staging
pool; it does not allocate another KV buffer.
## Use and ordering
Set `STRATA_KV_PREFETCH=1` before starting the engine. It is off by default.
A release event protects the staging pool's last reader; a ready event orders
the next attention consumer. Early returns drain pending copies before the caller
can refill borrowed expert slots. Relayout and destruction also drain the stream.
## Validation environment
- Two modified RTX 3080 20 GB cards; CUDA SM86, CUDA 13.1, Linux.
- Intel Xeon E5-2686 v4 (18 cores / 36 threads), about 94 GiB usable RAM.
- PCIe 3.0, no GPU peer-to-peer access; 17 expert-pool workers.
- Qwen3.8-Flash-Next IQ3_S, 131,072-token context, INT8 KV,
32,768 resident KV cells.
- Medium thinking: 2,048-token reasoning cap, 8,192-token output cap.
- Physical GPU power limits stayed at 250 W and 280 W. No power increase.
- Historical performance tests used an isolated engine based on official
v0.1.38 plus reviewed architectds/Strata changes (best, 05c0f36).
This PR contains only our change, ported to official main (99f3dbd).
The historical timings are NOT a measured speedup of this pure-upstream port.
## Historical benefit
ABBA comparison, two observations per arm, all 20 complete JSON checks passed:
| Cold input | Serial prefill | Overlapped prefill | Serial request | Overlapped request |
| --- | ---: | ---: | ---: | ---: |
| 8K | 5.013 s | 5.025 s | 6.727 s | 6.726 s |
| 32K | 16.989 s | 16.082 s | 18.468 s | 17.559 s |
| 64K | 32.604 s | 31.259 s | 34.354 s | 32.775 s |
Prefill time fell by 5.3% at 32K and 4.1% at 64K.
There was no useful 8K improvement and no decode speedup.
Profiling showed roughly 939 ms / 1,553 ms of serial KV-stage wait moving off
the critical path. The copies still execute; they are not eliminated.
## Validation and remaining work
- Linux compile/link against pure official main (99f3dbd) passed. No engine
was started and no production settings were changed.
- No claim of a pure-upstream GPU speedup or bitwise numerical parity yet.
- The standalone historical prototype did not run the full vision/120K/cancel
safety suite. Later combined builds did, but they do not validate this port.
- Keep draft until pure-upstream long-context, cancellation, and image-history
tests pass. HIP and other GPUs are unvalidated.
+1 -1
View File
@@ -111,7 +111,7 @@ The command is `python <Strata folder>/tools/strata_mcp.py`. The transport is st
| --- | --- | --- |
| `strata_status` | Whether a model is running: the model, engine version, context, busy or idle, VRAM and RAM in use. What is installed: models, start scripts, engine version, data folder, downloads. An install in progress. This PC's hardware: OS, CPU, RAM, GPUs, page file, free disk. The model and context setup recommends for this PC. | `port`, `hardware` (true/false) |
| `strata_models` | Every family and size setup offers, with download size, RAM needed, whether it fits this PC, and whether it is installed. Also the context lengths and the recommendation. | - |
| `strata_install` | Runs setup without questions, in the background (see below). Without `confirm: true` it only returns the plan. A second call shows the progress. `cancel: true` stops the install. | `family` (qwen, swift, coder, unsloth), `model` (Q2_0, IQ2_XS, IQ3_XXS, IQ3_S, IQ1_M, UD-Q4_K_XL), `context` (8192 ... 524288), `vision` (no, yes, gpu, cpu), `kv` (int8, q4_0, k8v4), `gpu` (a number) or `gpus` ("0,2", "all"), `backend` (auto, cuda, hip), `low_ram`, `port`, `data_dir`, `confirm`, `cancel` |
| `strata_install` | Runs setup without questions, in the background (see below). Without `confirm: true` it only returns the plan. A second call shows the progress. `cancel: true` stops the install. | `family` (qwen, swift, coder, unsloth), `model` (Q2_0, IQ2_XS, IQ3_XXS, IQ3_S, IQ1_M, UD-IQ4_XS, UD-Q4_K_XL), `context` (8192 ... 524288), `vision` (no, yes, gpu, cpu), `kv` (int8, q4_0, k8v4), `gpu` (a number) or `gpus` ("0,2", "all"), `backend` (auto, cuda, hip), `low_ram`, `port`, `data_dir`, `confirm`, `cancel` |
| `strata_start` | Starts an installed model in the background and waits until it answers. If it is not ready after `wait_seconds` (default 50), it returns "loading". | `model` (an id from `strata_status`, e.g. `q2_0`, `coder-iq1_m`; default: the most recently used), `port`, `gpu`, `wait_seconds` (0-600) |
| `strata_stop` | Unloads the model through the server's own API, then ends the server it started. | `port`, `force` |
| `strata_logs` | The last lines of the server log, the engine log or the setup log. | `source` (server, engine, setup), `model`, `lines` (1-500) |
+66
View File
@@ -0,0 +1,66 @@
# Reuse long chat history when the last user message is edited
## Why this helps
An agent can keep the same long history but replace the final user message.
The final cached state no longer matches, and the nearest periodic checkpoint
may be far behind. The engine then rereads history it already processed.
This opt-in change saves one checkpoint at the previous turn boundary, before
the final short message. It uses the existing bounded cache and leaves normal
token and image-prefix validation in charge of reuse.
## Use and limits
Set `STRATA_CACHE_MESSAGE_BOUNDARY=1` before starting the engine.
It is off by default, requires prompt caching and a valid turn token, and
only selects prefixes of at least 8,192 tokens with a tail of at most 1,024.
Layer-split execution is excluded (`multi_gpu`); helper-expert GPUs still work.
It can split prefill chunks and add snapshot cost to a fresh request.
## Validation environment
- Two modified RTX 3080 20 GB cards; CUDA SM86, CUDA 13.1, Linux.
- Intel Xeon E5-2686 v4 (18 cores / 36 threads), about 94 GiB usable RAM.
- PCIe 3.0, no GPU peer-to-peer access; 17 expert-pool workers.
- Qwen3.8-Flash-Next IQ3_S, 131,072-token context, INT8 KV,
32,768 resident KV cells.
- Medium thinking: 2,048-token reasoning cap, 8,192-token output cap.
- Physical GPU power limits stayed at 250 W and 280 W. No power increase.
- Historical performance tests used an isolated engine based on official
v0.1.38 plus reviewed architectds/Strata changes (best, 05c0f36).
This PR contains only our change, ported to official main (99f3dbd).
The historical timings are NOT a measured speedup of this pure-upstream port.
## Historical benefit
Same-binary ABCCBA comparison, two repetitions per variant:
| Metric | On-demand boundary + 16K chunks | Anticipatory boundary + 32K chunks |
| --- | ---: | ---: |
| First complete 100-record edit | 17.236 s | 11.527 s |
| Three complete edits | 41.266 s | 35.802 s |
| Cold setup plus three edits | 58.688 s | 53.146 s |
| Eleven-case wall-time sum | 133.844 s | 127.983 s |
First-edit time fell by 33.1%; reused prefix grew from 22,016 to 32,831 tokens.
This is a COMBINED boundary-policy and chunk-size comparison, not the isolated
benefit of adding this checkpoint to pristine upstream.
Hot generation was unchanged (97.55 vs 97.60 tokens/s).
The mixed-suite reduction was 4.4%; ordinary cold 8K requests cost about 0.2 s more.
The historical comparison passed 77 task/cache checks and cancellation/recovery.
A separate safety arm for the adopted 32K choice passed 22 task/cache checks,
including 120K, image-history isolation, and cancellation/recovery.
## Validation and related work
- Standalone selector test passes all 21 cases, without assertions that disappear
in Release builds.
- Linux compile/link against pure official main (99f3dbd) passed. No engine
was started and no production settings were changed.
- PR #614 checkpoints an already-reached chunk near the tail without splitting it.
This proposal chooses a message boundary, which can split chunks. They solve
related cases with different cost and numerical tradeoffs.
- Chunk geometry can change floating-point results. No pure-upstream bitwise
parity or isolated upstream speedup is claimed. Keep draft until validated.
+21 -4
View File
@@ -14,7 +14,7 @@ The installer recommends one for your PC; this page explains the choice. Back to
| **32 GB** | **Coder** | it fits 32 GB; best at code, weaker at everything else ([why](#coder)). With a 24 GB card, Q2_0 and IQ2_XS run too ([low-RAM mode](#a-big-graphics-card-and-little-ram)) and are the better pick for general use |
| **48 GB** | **IQ2_XS** (or Q2_0, the fastest) | the larger sizes do not fit |
| **64 GB** | **IQ2_XS** (recommended), or IQ3_XXS / IQ3_S | every size fits (IQ3_S with little else open) |
| **96 GB or more** | **IQ3_S**, or [Unsloth's 4-bit](#unsloth-ud-q4_k_xl-experimental) (experimental) | room for the largest sizes |
| **96 GB or more** | **IQ3_S**, or [Unsloth's UD-IQ4_XS](#unsloth-ud-iq4_xs) (~4-bit) | room for the largest sizes |
Not sure? Take **IQ2_XS**. The **Coder** is the one that fits a 32 GB PC, but it keeps only 256 of the 512 experts,
chosen on code data, so it is weaker outside code and in languages other than English. For general use, or whenever
@@ -71,8 +71,9 @@ fetches the MTP draft layer (~6 GB, +1 GB with images).
Shard 1 is the part of the model that gets loaded when it starts: its experts go into your **RAM**, the rest onto
your graphics card (the second shard, a 29 GB lookup table, stays on the SSD). So it fits when your **RAM is at least
shard 1 + about 10 GB** for Windows and your other programs. With 64 GB of RAM every size fits (IQ3_S with little
else open); with 48 GB, Q2_0 and IQ2_XS. A bigger graphics card makes it faster, but it doesn't lower the RAM needed
the experts + about 10 GB** for Windows and your other programs - the experts are most of shard 1: 34 GB for Q2_0,
35.5 for IQ2_XS, 43 for IQ3_XXS, 50 for IQ3_S, 23 for the Coder (the dense weights in shard 1 go to the graphics
card). With 64 GB of RAM every size fits (IQ3_S with little else open); with 48 GB, Q2_0 and IQ2_XS. A bigger graphics card makes it faster, but it doesn't lower the RAM needed
- except in the low-RAM mode below.
### A big graphics card and little RAM
@@ -116,9 +117,22 @@ More: [details](DETAILS.md#or-swift-15-a-fine-tune-that-thinks-shorter).
START-HERE.bat --setup --family swift --model IQ2_XS
```
### Unsloth UD-IQ4_XS
**Unsloth's UD-IQ4_XS** (~4-bit) is the fourth version in setup's menu (`--family unsloth`, its first size), a
regular choice from 0.1.39: a 94 GB download with 59.5 GB of experts (IQ3_S and IQ4_NL), between IQ3_S and UD-Q4_K_XL
in quality. Strata keeps your RAM minus 24 GB of the experts in RAM and reads the rest from the SSD while it answers:
on a 64 GB PC part of them come from the SSD (slower; an NVMe SSD helps), from ~80 GB of RAM all of them stay in RAM.
It needs 48 GB of RAM or more and engine 0.1.38 or newer; images are an option, as with the other models.
Details: [UD-IQ4_XS](UNSLOTH_Q4.md#ud-iq4_xs-setup-from-0139-621).
```
START-HERE.bat --setup --family unsloth --model UD-IQ4_XS
```
### Unsloth UD-Q4_K_XL (experimental)
**Unsloth's 4-bit UD-Q4_K_XL** (experimental) is the fourth version in setup's menu (`--family unsloth`): the closest
**Unsloth's 4-bit UD-Q4_K_XL** (experimental) is the second size of the same family (`--family unsloth`): the closest
to the full model, but a 111 GB download whose 77 GB of experts do not fit in RAM. Strata keeps your RAM minus 24 GB
of them in RAM and reads the rest from the SSD while it answers: 7-8.5 tokens/s on a 64 GB PC with a 12 GB GPU, several
times slower than the sizes above, and long prompts are slow. It needs 48 GB of RAM or more, an NVMe SSD and one
@@ -128,6 +142,9 @@ NVIDIA GPU (no images yet). Details and measurements: [UD-Q4_K_XL](UNSLOTH_Q4.md
START-HERE.bat --setup --family unsloth --model UD-Q4_K_XL
```
**Unsloth's 6-bit UD-Q6_K_XL** (experimental, not in setup's menu) trades more bytes per expert for less precision
loss against the same files: 169.2 GB together, its PLE table Q8_0 (read by the engine since 0.1.38). High RAM
machines run it without a RAM budget. Manual workflow: [UD-Q6_K_XL](UNSLOTH_Q6.md).
### OrcaRouter Uncensored IQ3_XXS
For **OrcaRouter's Flash-Next Uncensored IQ3_XXS**, see the [manual compatibility setup](ORCA.md). It needs an
+99 -1
View File
@@ -56,6 +56,11 @@ Or edit an existing config (`strata-*.json`), then restart:
"layer_split": "auto"
```
`"layer_split"` is `"auto"` (placed by each card's free VRAM) or the **first layer of each later card**: one rising
number per card after the first, not a count of layers per card. With 4 cards and a 48-layer model, `"24,36,42"`
(or `[24, 36, 42]`) puts layers 0-23 on the first card, 24-35 on the second, 36-41 on the third and 42-47 on the last.
The server checks it before the start and says what is wrong (0.1.39, #644).
**Skip the split when the first card holds everything** (opt-in, 0.1.31): `"split_skip_if_fits": true` in the config
(engine flag `--split-skip-if-fits`, with `--layer-split auto`) runs on the first card alone when it holds every
profiled expert plus the context's KV, the draft layer and the reserve, and says so in the log; otherwise the split
@@ -71,11 +76,56 @@ VRAM keep its own prompt buffers - the same output as 0.1.31, measured on an R97
differs from the default's (stable and coherent); `STRATA_SPLIT_OWN=auto` does that only where the buffers are at
most 12% of each card's VRAM.
**The idle card can help one-chunk prompts (opt-in, `STRATA_PREFILL_HELP=1`).** A prompt that fits one chunk runs the stages one after the other, so while
one card reads its layers the other idles. With it on, each stage hands a share of its streamed experts to the idle card: it
streams them over its own PCIe link into its own (lent) prompt buffers, computes their rows on the MMQ path and sends
them back - `--peer-device`'s peer streaming, without P2P (the activations and the rows go through mapped host memory,
read by copy kernels, so they do not queue behind the expert blobs on either card's copy engine). The share falls with
the prompt (0.41 of the streamed experts at 1.5K tokens, 0.32 at 3K) and is off from ~3.3K tokens, where the stages
overlap anyway and no share paid. Measured on 2x RTX 3090 (UD-Q4_K_XL, no P2P), prompt tok/s without / with: 1.1K 496 /
723, 1.5K 674 / 833, 2K 878 / 1,128, 2.5K 1,017 / 1,296, 3K 1,260 / 1,440; 4K and 8K unchanged, decode unchanged. It
costs no VRAM (the idle card's own prompt buffers) and ~110 KB of mapped host memory per token of the largest chunk it
helped (~360 MB at 3.3K tokens). The rows it computes round like a different MMQ grouping, so the output is not
bit-identical to the default's (it is repeatable: same prompt, same output), which is why it is **opt-in**:
`STRATA_PREFILL_HELP=1` turns it on. Native packs on the MMQ prompt path only (not with the fused prompt kernels,
`STRATA_PF_FUSED=1`, nor with `--peer-device`). With it on, `STRATA_PREFILL_HELP_FRAC=f` fixes the share.
The engine flags behind it: `--layer-split K1[,K2..]|auto` and `--split-device D1[,D2..]` (the later stages'
devices; default the next visible ones). `--layer-split K --split-device 0` runs both stages on one card sharing
everything - the bit-exact check of the hand-off, not a speed mode.
**auto** tries every placement (all of them for two or three cards; proportional to the free VRAM beyond that) and
**Each card loads only its own layers' dense weights** (0.1.39, PR #639) with explicit split points (`--layer-split
27`, not `auto`): every card used to keep a full copy (~3.4 GB for the Coder) though its stage reads only its own
layers, and the VRAM it frees goes to that card's expert cache (2x MI50 16 GB, Coder: 8,819 -> 10,626 experts in
VRAM, decode 39.2 -> 41.7 tok/s). Opt-in for now, on AMD and NVIDIA alike: `STRATA_STAGE_TRIM=1` (please report how
it goes).
A card holding more experts can change which experts run on the GPU, so the output can differ slightly from a run
without it.
**The resident RAM mode works on a split** (`--resident-experts`, the low-RAM mode). The RAM copy of the experts
leaves out the ones every card's cache holds, not only the first card's, and when the rest does not fit whole it keeps
the hottest by the expert profile over all the layers. An adaptive swap copies the evicted expert back into RAM from
the card that owns its layer. Before, `--resident-experts` with a split ran as `--mmap-experts`, and setup recommended
one card in the low-RAM mode; with an engine that has it (`RESIDENT_SPLIT_ENGINE` in setup.py), setup keeps the cards
together and the experts no card holds in RAM. Swift 1.5 IQ3_XXS at
160K (q4_0 KV, `--prefill 4096`, `--spec 4` with the stock draft layer), RTX 4060 Ti (layers 0-19) + RTX 5080 (20-47),
i9-14900KF, 32 GB of RAM, Windows 11, four greedy prompts at a time, decode tok/s:
| | first four prompts | after three more rounds |
|---|---|---|
| 5080 alone, `--resident-experts` | 25 | 29 |
| split, `--mmap-experts` (what `--resident-experts` became on a split) | 32 | 64 |
| split, `--resident-experts` (22 GiB of experts locked in RAM) | 71 | 69 |
The split with `--mmap-experts` catches up once the OS file cache holds the experts, on a PC with nothing else
running; the resident copy is there from the first request and stays locked when other programs need the RAM. With
`--pcie-frac 0 --adapt-every 0` the split's greedy output is the same with either mode.
**A separate VRAM reserve for the later cards:** `--vram-reserve-later-mib N` (default: `--vram-reserve-mib`'s value).
The card that drives the monitors needs more headroom than one that drives none; with the display on the last card,
`--vram-reserve-mib 300 --vram-reserve-later-mib 1800` gives the first card's cache that VRAM.
**auto** tries every placement (all of them for two, three or, since 0.1.40, four cards; proportional to the free VRAM beyond that) and
keeps the one whose caches would hold the most of the expert profile, hottest pairs weighted most; ties go to the
placement that leaves the fullest card the most room. The startup log prints the choice:
@@ -140,3 +190,51 @@ The Coder on an RTX 5080 + RTX 3090 (Ryzen 9 9950X3D), 32K context; details in
- Leave out a much slower card when two already hold the model. An RTX 2080 Ti as a third card made the 5080 +
3090 pair slower (68 / 90 tok/s decode): every extra card costs its own round per window.
- More cards pay off when the model's routed experts do not fit the faster ones.
## One conversation with both cards busy (`--pipeline-windows`, opt-in)
With a split the cards take turns on a verify window: the first card runs its layers and hands off, then waits while
the last card runs the rest, the head and the draft. `--pipeline-windows 2` lets the first card start the **next**
window while the last card still verifies this one. The next window is a guess: that this window is accepted whole
and that its bonus token is the one the draft layer predicts (the draft layer is run on through the drafts of the
window in flight). When the guess holds, half of the next window is already done; when it does not, the first card
puts its state back (a copy of its recurrent state taken while the window ran) and the next window is built from the
real tokens. A guessed window is only started when the draft layer's estimate says it is likely to be kept. Windows
copied from earlier context (`--suffix-draft`) are guessed past as well, which is where edits that copy text gain
most. `--pipeline-windows 1` overlaps only the short prompt reads that go through the verify windows
(`--short-read`).
Two cards, exactly two stages, `--serve`. In the config:
```
"args": [ ..., "--pipeline-windows", "2" ],
"layer_split": "20"
```
- **Cost**: a second verify window on each card, 160 MiB more kept out of each card's expert cache, plus two copies
of the first card's recurrent state (about 3 MiB per GDN layer it runs) on the first card with `2`.
- **Same text**: the last card only ever runs windows that are verified, and every window row computes what it
would in any other window, so the tokens are the serial loop's. With `STRATA_IQ_MT_MIN=1 --pcie-frac 0
--adapt-every 0` the greedy output is identical bit for bit to the serial loop's with the same expert caches. The pipeline keeps
its VRAM out of the caches, so against a run without the flag a few experts move from a card to the CPU, which
rounds them differently, and a near-tie can flip (a serial run given the same caches through `--vram-reserve-mib`
matches it exactly).
- **Off, with one line in the log saying why**, with `--batch` slots, `--peer-device`, the helper caches
(`--expert-cache-device1..3`, `--remote-expert-opt`), a split into three or more stages or onto one GPU
(`--split-device 0`), or no draft layer. A request with repetition penalties (`penalty_last_n`) or coupled
draft sampling decodes serially.
- **Measured** (Swift 1.5 IQ3_XXS, 160K context, q4_0 KV, the stock draft layer, RTX 4060 Ti (layers 0-19) +
RTX 5080 (20-47), i9-14900KF, 32 GB of RAM with the resident RAM mode on the split (#848); greedy, 500 tokens, two
interleaved pairs of three rounds, decode tok/s): Python code 90.4 -> 103.1, C code 76.6 -> 81.6, English prose
63.0 -> 71.1, Italian prose 41.6 -> 46.3, a copy-heavy edit (a 5 KB file back with a rename) 90.5 -> 117.9; mean
72.4 -> 84.0 (+16%). It pays when the windows are GPU-bound: with the experts read through the OS file cache
(`--mmap-experts` on that 32 GB PC) the file reads dominate and it measured no faster.
The `STRATA_PIPELINE_*` tuning and test variables (THETA, FORCE_MISS, SWITCH, LOG, TRACE and the like) are read only with
`STRATA_PIPELINE_DEBUG=1`. `--pipeline-windows` and `--adapt-async 1` exclude each other (the engine says so and keeps the
pipeline).
## Several conversations at once
With a layer split, `--batch N --batch-groups G --trim-stage-weights` decodes several conversations together and
pipelines them through the cards: see [BATCHING.md](BATCHING.md).
+71
View File
@@ -0,0 +1,71 @@
# NVIDIA Tesla V100 / Titan V (Volta, sm_70): the community build
Strata's ready-made engine and the support matrix start at compute capability 7.5 (RTX 20). A V100 (7.0) runs the same
engine compiled for it, through the experimental build that issue #236 added. It is **community-tested, not part of the
supported cards**: no ready-made engine is published for it and the numbers below come from one machine.
## Build
CUDA 13 dropped Volta, so the build needs a **CUDA 12.x** toolkit (12.8 was used) and a host GCC that toolkit accepts.
```sh
cmake -S . -B build -DSTRATA_ENABLE_CUDA=ON -DSTRATA_BUILD_TESTS=OFF \
-DCMAKE_CUDA_ARCHITECTURES=70 -DSTRATA_EXPERIMENTAL_SM60=ON
cmake --build build --target strata -j
```
Setup does the same by itself for a model on a V100 (the experimental CUDA 12 engine, in `engine-cuda12/`: ready-made
on Windows, compiled with a CUDA 12.x toolkit on Linux; [OLDER_GPUS.md](OLDER_GPUS.md)). For a PC with a V100 and a newer card, a build for both architectures (`-DCMAKE_CUDA_ARCHITECTURES="70;86"`) is the
natural choice (a community report in issue #509 ran an RTX 3080 beside a V100 that way; it was not repeated here). On the
measuring PC a Quadro RTX 4000 (sm_75) sits beside the V100s and was hidden with `CUDA_VISIBLE_DEVICES` because the sm_70-only
build has no code for it.
## What runs on Volta
The flag only lowers the floor (CMake and the runtime device check) and replaces two intrinsics Volta lacks. It does not
select "old" kernels everywhere: which kernel runs is decided per architecture when the engine is built.
| Part of the prompt path | On sm_70 |
| --- | --- |
| MoE experts (ggml MMQ) | `dp4a` kernels (Volta has no int8 tensor cores) |
| Dense projections (dequantized weights) | FP16 tensor-core GEMMs (cuBLAS / CUTLASS `s884`) |
| BF16 projections (hyper-connection, router, indexer, ...) | converted to FP16 and run on the FP16 tensor cores (#655, #540; `STRATA_BF16_TC=0`: cuBLAS BF16, an FP32 SIMT kernel on Volta) |
| QSA attention for decode and verify windows (and prompts with `STRATA_PROMPT_ATTN_OLD=1`) | #540's kernel (fewer shuffles, bit-exact; `STRATA_ATTN_PRE75=0`: the one other cards run) |
| QSA prompt attention, int8 / FP16 / K8V4 KV | `prompt_attn_v70_kernel` on `mma.m8n8k4` (`STRATA_PROMPT_ATTN_OLD=1`: the decode kernel, one query at a time) |
| QSA prompt attention, Q4_0 KV | the decode kernel |
| QSA block scores | the warp kernel (the tensor-core scorer needs sm_80) |
## Measured
One V100-PCIE-32GB, i9-7960X, Unsloth `UD-IQ4_XS` (not a setup model), int8 KV, MTP `--spec 2`; prompt read speed of random-word prompts, tokens/s,
with and without the Volta attention kernel (`STRATA_PROMPT_ATTN_OLD=1`):
| Prompt tokens | old attention path | `prompt_attn_v70_kernel` |
| --- | ---: | ---: |
| 7,194 | 1,010 | 1,164 |
| 28,650 | 1,063 | 1,251 |
| 114,338 | 973 | 1,123 |
Decode was 38-51 tok/s in every run and is not affected. The profile, the parity numbers, the needle test and the limits are in
[bench/results/2026-10-03-v100-prompt-attn](../bench/results/2026-10-03-v100-prompt-attn/README.md).
What the kernel does and does not guarantee: against an FP64 reference its error is about 5e-6 (output scale 3.6; the FP32 kernel it replaces: 2e-6); 15 of 15
needles were found; greedy output was identical on the 28,650- and 114,338-token prompts and differed late in the answer on the 7,194-token one (another
summation order).
## Checking and benchmarking the Volta kernels
```sh
cmake -S . -B build -DSTRATA_PARITY_PROMPT_ATTN=ON ... && cmake --build build --target qsa_prompt_attn_parity
CUDA_VISIBLE_DEVICES=0 ./build/qsa_prompt_attn_parity 32768 2048 5 # synthetic, no model; exit 0 = PASS
```
It compares the tensor-core attention with the FP32 kernel and an FP64 host reference and times both. The Q4_0 cases are skipped below
sm_80.
## Limits
- A layer split over two V100 was not measured with this kernel. The cards work in turn on a single request, so a split adds expert-cache room, not prompt speed.
- sm_60 (Pascal) is covered by the same flag but was not measured here.
- Volta's tensor cores take FP16 only: a model's BF16 weights are converted, and a weight outside FP16's range would saturate (none did in
the check above).
+111
View File
@@ -0,0 +1,111 @@
# Older GPUs (experimental)
Strata's supported cards are NVIDIA RTX 20 / 30 / 40 / 50 and the AMD cards in [AMD_HIP.md](AMD_HIP.md). The cards
below run through **opt-in, experimental** paths that community members wrote and measured on their own machines. The
maintainers have none of these cards: each path is compile-checked and unit-tested here, and the ready-made engines
and their output stay exactly as they were. Numbers are the reporters' own, on one machine each.
## Support matrix
### NVIDIA
| Cards | Compute capability | How it runs | What is different on it | Reported |
| --- | --- | --- | --- | --- |
| Tesla P100 | 6.0 | the CUDA 12 engine | `__dp4a` emulated (bit-exact); BF16 projections through fp32 | not measured |
| Tesla P40 / P4, GTX 10 series | 6.1 | the CUDA 12 engine | BF16 projections through fp32 (cuBLAS has no BF16 GEMM there, #395) | P40, IQ3_S, engine 0.1.30: prompt 217-374 tok/s, decode 30-33 tok/s (#395) |
| Tesla V100, Titan V | 7.0 | the CUDA 12 engine | BF16 projections on the FP16 tensor cores (#655, #540); the prompt attention on `mma.m8n8k4` (#600); a leaner attention kernel (#540) | V100-PCIE-32GB, UD-IQ4_XS: prompt 1,123-1,251 tok/s (#600); V100 32GB, IQ2_XS: prompt +22% from #540 |
| RTX 20 (Turing) | 7.5 | **supported**, the ready-made engine | opt-in: `STRATA_BF16_TC=1` runs the BF16 projections on the FP16 tensor cores | RTX 2080 Ti, Q2_0: prompt +15-18% (#655) |
A card needs enough VRAM to be useful: 12 GB or more is recommended, as for every card (a 2 GB GT 1030 is compute
capability 6.1 too, and cannot hold any of the model).
### AMD
| Cards | Architecture | How it runs | Reported |
| --- | --- | --- | --- |
| RX 6800 / 6900 series | gfx1030 | setup (`--backend hip`), unvalidated | [AMD_HIP.md](AMD_HIP.md#rdna2-gfx1030) |
| RX 6700 XT | gfx1031 | setup (`--backend hip`), unvalidated (#524) | used daily by its reporter, one card |
| RX 5500 XT (RDNA1) | gfx1012 | built by hand: `-DCMAKE_HIP_ARCHITECTURES=gfx1012` (HIP 5.7 or 7) | 8 GB card, IQ3_S, 8K prompt: 15.3 tok/s decode (#442) |
| Instinct MI50 / MI60, Radeon VII | gfx906 (wave64) | built by hand: `-DSTRATA_HIP_GFX906=ON` | 2x MI50, Coder IQ1_M, 128K context: decode 50.1 / 47.8 / 45.7 tok/s at 4K / 32K / 128K prompt tokens, prompt ~520 tok/s (#677) |
## NVIDIA: the CUDA 12 engine
CUDA 13 dropped Pascal and Volta: it cannot compile for them. Setup therefore keeps a **second engine**, built with
CUDA 12.9 and `-DSTRATA_EXPERIMENTAL_SM60=ON`, in its own folder (`engine-cuda12\`, beside `engine\`). One engine runs
per model, so the choice is made per model, by the oldest card that model runs on:
- **Every card the model uses is RTX 20 or newer:** the ready-made CUDA 13 engine, as always.
- **A card is Pascal or Volta:** the CUDA 12 engine. Setup says so (`CUDA 12: sm_70 is older than CUDA 13 supports
...`). On Windows it downloads `strata-windows-x64-cuda12.zip` with NVIDIA's CUDA 12 libraries (from pip, like the
CUDA 13 ones); on Linux, or with `--build`, it compiles the engine with a CUDA 12.x toolkit.
### Opting in
An older card is used only when you choose it; a PC with a newer card keeps recommending the newer one.
| You | Setup |
| --- | --- |
| have only Pascal / Volta NVIDIA cards (and no AMD card it can use) | uses them, with the CUDA 12 engine |
| name the card: `START-HERE.bat --setup --gpu 1`, or `--gpus 0,1` with a newer card | uses it; the model gets the CUDA 12 engine |
| `--cuda 12` (or `STRATA_CUDA=12`) | the CUDA 12 engine for this model, on any card |
| `--cuda 13` | the CUDA 13 engine even with an older card (a warning: it has no code for that card) |
| `STRATA_EXPERIMENTAL_SM60=1` | the older cards are listed as usable (the setting from #295 still works) |
The choice is kept in the model's config (`"cuda": 12`): its starts and `UPDATE.bat` keep it, and other models keep
their own engine. Setting the model up again chooses again (by its cards; add `--cuda 12` to keep a forced choice). A Pascal / Volta card added to a model at a start (`--gpus`) moves that model to the
CUDA 12 engine.
`--cuda 12` is also the way to run Strata with an NVIDIA driver older than 580: CUDA 12 needs 528 or newer on Windows
(527.41, NVIDIA's minor-version compatibility) and 525 on Linux; setup's "driver too old" stop says so. Such old
drivers were not tested here.
### Mixed cards
A model that shares a V100 with an RTX 30 / 40 card runs both on the CUDA 12 engine (it has code for sm_60 to sm_89,
plus PTX). An RTX 50 card (sm_120) in a CUDA 12 engine is a warning, not a stop: CUDA 12.8 and newer compile for it,
but engines built with 12.8 crashed on long prompts there (#220, #224). Keep the RTX 50 card on its own model (`--gpu
N`), where it runs the CUDA 13 engine.
### Building it yourself
```sh
# Linux (Windows: the same with the toolkit's nvcc.exe)
cmake -S . -B build-cuda12 -DSTRATA_ENABLE_CUDA=ON -DSTRATA_BUILD_TESTS=OFF -DSTRATA_EXPERIMENTAL_SM60=ON \
-DCMAKE_CUDA_ARCHITECTURES="61;70" -DCMAKE_CUDA_COMPILER=/usr/local/cuda-12.9/bin/nvcc
cmake --build build-cuda12 --target strata -j
```
Setup does the same when it compiles: it looks for the newest CUDA 12.x toolkit (`STRATA_NVCC=<path to nvcc>` picks
one, #601; on glibc 2.43 use 12.8, see [TROUBLESHOOTING.md](TROUBLESHOOTING.md)).
### What the flag changes, and what it does not
`-DSTRATA_EXPERIMENTAL_SM60=ON` lowers the runtime floor to compute capability 6.0 and compiles the older cards' code
paths into that build only: the Volta prompt attention (#600), #540's attention kernel (used below sm_75), and the
Pascal BF16 path. The ready-made CUDA 13 engine has none of them, so its kernels and its output are unchanged. The
FP16 path for BF16 projections is in every build but runs by default only below sm_75; RTX 20 owners can try it with
`STRATA_BF16_TC=1` (`=0` turns it off on a V100). It is not bitwise the same as cuBLAS's BF16 kernel (FP16 tensor-core
sums round differently): #540 measured a mean KL of 8.4e-3 on the next-token distribution of 24 code prompts on a V100
(the same top-1 in 23), the size of other summation-order changes; #655 a worst relative difference of 3.5e-5 per
product on an RTX 2080 Ti.
A/B switches: `STRATA_BF16_TC=0|1`, `STRATA_PROMPT_ATTN_OLD=1` (the decode kernel for prompts), `STRATA_ATTN_PRE75=0`
(#540's kernel off). [NVIDIA_V100.md](NVIDIA_V100.md) has the V100 build, its measurements and the parity test.
## AMD: building gfx906 and gfx1012
Setup does not build these; build by hand and run `serve/server.py` with a config, as on any other card.
- **gfx906** (MI50 / MI60 / Radeon VII, wave64): a separate opt-in build, `-DSTRATA_HIP_GFX906=ON
-DCMAKE_HIP_ARCHITECTURES=gfx906` (not `STRATA_ENABLE_HIP`). Current ROCm no longer ships gfx906 libraries; the
reporter used a community ROCm 7.14 image. Recipe, kernels and measurements: [AMD_HIP.md](AMD_HIP.md#gfx906-instinct-mi50--mi60-radeon-vii-wave64-built-from-source).
- **gfx1012** (RX 5500 XT): the wave32 backend, `-DSTRATA_ENABLE_HIP=ON -DCMAKE_HIP_ARCHITECTURES=gfx1012`. HIP 5.7
(Ubuntu's packages) works: older hipBLAS (0.x) is used through rocBLAS, the legacy HIP names and the missing
`__syncwarp` are version-gated, and RDNA1's missing signed dot4 uses llama.cpp's SDWA sequence
(`-DSTRATA_GFX1012_PORTABLE_DOT=ON`: the portable one).
## Reports welcome
These paths stay experimental until more people run them. A report with the card, the driver / ROCm version, the
model and the engine log (`strata-<model>.log`) in an issue helps; [COMMUNITY_BENCHMARKS.md](COMMUNITY_BENCHMARKS.md)
has the format for measurements.
+2 -1
View File
@@ -30,7 +30,8 @@ from that dependency (`STRATA_GGUF_PY` can point to its `gguf-py` directory).
Use this model's own tokenizer exported into the pack. Do not share another model's `dense.bin`,
and do not rename the Orca files to impersonate one of setup's GSQ-RCO models.
Strata's persistent server also requires the MTP runtime, prepared by the existing tools:
Strata's persistent server also uses the MTP runtime (optional: without `--mtp` it drafts by lookup only), prepared by
the existing tools:
```sh
.venv/bin/python tools/mtp_fetch.py fetch --out mtp
+33
View File
@@ -0,0 +1,33 @@
# Optional checkpoint near the prompt tail
`--prompt-cache-tail` saves at most one extra mid-prompt checkpoint near the end
of a request, at a boundary the batched prefill already reaches. It is off by
default and applies to single-GPU serve with positive `--prompt-cache` and
`--prompt-cache-every` values.
A branch can share most of a prompt but diverge before its final cached state.
If the periodic checkpoint before that branch is far away, the engine has to
read the gap again. The extra checkpoint can reduce that gap. It uses the
existing bounded checkpoint cache; it does not add another unbounded history.
The tail is the first chunk boundary within one chunk of the prompt's end, where the chunk is the size that chunk
really had (`--prefill auto` chunks differ from `--prefill N`). The tail checkpoint is evicted first (only one stays
alive, the root is never dropped), so it never pushes out a periodic checkpoint when `--prompt-cache` is full.
The option does not split prefill chunks or alter the periodic checkpoint
schedule. It selects the first completed batched chunk within one configured
chunk of the prompt's end, excluding the short final suffix controlled by
`--short-read`. If a periodic checkpoint already falls there, the same state is
saved once. Reread diagnostics and layer splits keep their existing behavior.
For example, with a 6144-token chunk and a 16384-token checkpoint interval,
the first periodic checkpoint is at 18432 and the next would be at 36864.
A branch sharing 32769 tokens cannot use the latter. A near-tail checkpoint
at 30720 can reuse more of that prefix,
without changing how the original prompt was computed. The benefit depends
on the request and the branch position; it is not a universal speed increase.
This is a separate conservative alternative to the Strata-2080Ti Daily's
checkpoint-grid patch. The grid changes chunk boundaries to reach exact
checkpoint intervals and failed its local paired numerical gate. This option
does not claim that the original grid patch has been fixed or validated.
+44
View File
@@ -84,6 +84,50 @@ The existing warning about the CUDA0 expert-cache GPU hit path still applies:
its outputs diverge from cache-off runs. Treat performance as experimental
until the generated tokens have been validated.
## Optional helper decode optimization
`--remote-expert-opt` (`--serve` only) optimizes the CUDA1-3 helper caches
above. The engine's default is off; since 0.1.39b setup adds it to a config on
two or more GPUs (`--gpus`, or "use both" at start). It acts only when a helper
cache is configured; a layer split runs exactly as before. To leave it out:
setup's `--no-remote-expert-opt`, or `"remote_expert_opt": false` in the
model's `strata-*.json` (kept when setup runs again). Measured by the PR's
author: dual RTX 4090 +63% mixed / +132% code decode over the plain helper
path; RTX 5090 + 4090 +28% / +63%. The primary cache avoids admitting experts already held
by a helper, and helpers replace cold experts with frequently routed CPU
misses using their existing same-layer slots. Each helper reduces its expert
outputs to a weighted partial sum on its GPU before returning one vector per
token. Tokens with no CPU expert work skip CPU activation quantization.
For an existing server configuration with CUDA0 as the primary and CUDA1 as a
helper, use these engine arguments alongside the model and profile arguments:
```text
--expert-cache auto --expert-cache-device1 auto --remote-expert-opt
```
`--expert-cache-device1`, `--expert-cache-device2` and
`--expert-cache-device3` now also accept `auto`: fill each helper from its
assigned ranking using actual aligned expert bytes and free VRAM, retaining
the existing 512 MiB allowance. Explicit numeric budgets still work. This is
startup capacity sizing, not throughput balancing; with several helpers a
card's truncated candidate tail is not redistributed to another card.
The optimization uses the existing host scheduling and pinned-host transport,
not P2P or tensor parallelism. Each layer still waits for its participating
helpers. It changes floating-point summation order, so enabled output is not
claimed to be bitwise identical. Only two-card CUDA operation has been measured
by its author; a community machine measured two cards on HIP (2x RX 6900 XT,
PCIe 4.0 x8 each, IQ3_S): there the expert plan's PCIe share had been taking
experts the helper already held, which made the primary card wait 43 ms per
verify window instead of 19 (decode 40 instead of 66 tok/s). #854 keeps a
helper's experts out of that share; before it, `--pcie-frac 0` avoided the
cost ([bench/results/2026-10-04-rdna2-helper-pcie-share](../bench/results/2026-10-04-rdna2-helper-pcie-share/README.md)).
Three/four cards have not been validated. Without the switch, the
existing decode path remains in use. This does not optimize the separate
`--peer-device` path below or change its existing incompatibility with helper
caches.
## Peer tier (`--peer-device`)
`--peer-device N` puts a second adaptive expert cache on CUDA device N. It
+164
View File
@@ -0,0 +1,164 @@
# Strix Halo (Ryzen AI Max, gfx1151): build from source on Linux
Strata runs on the Ryzen AI Max "Strix Halo" APUs (Radeon 8060S / 8050S, RDNA3.5, `gfx1151`, one pool of LPDDR5X shared by the
CPU and the GPU) through the same HIP backend as the Radeon cards in [AMD_HIP.md](AMD_HIP.md). **Status: experimental.**
Everything here was built and measured on one machine, the maintainers' Strix Halo box (Ryzen AI Max+ 395, 128 GB,
Ubuntu 26.04, kernel 7.0, ROCm 7.14.1). Setup (`setup.sh`, `START-HERE.bat`) recognizes it from 0.1.40, by its PCI id (`1002:1586`, one id for the 8060S, 8050S and 8040S) and by
`gfx1151` exactly: a Strix Point (890M / 880M, gfx1150), Krackan (860M / 840M, gfx1152) or Phoenix / Hawk Point (780M / 760M, gfx1103) Radeon is
named as what it is and not supported. On Linux setup compiles the engine for gfx1151 itself (the steps below are the same, by hand). On
Windows the ready-made `strata-windows-x64-hip.zip` carries gfx1151 code from 0.1.40 (untested on a Windows Strix Halo so far); a zip
without it is refused with a message, never used. Setup counts the GPU's memory as the BIOS carve-out plus the shared memory
(the GTT pool; on Windows half the RAM) and recommends UD-IQ4_XS from about 80 GB of memory; it changes no BIOS or kernel setting.
What the engine does differently on this chip:
- **Unified memory.** The GPU's "VRAM" is the GTT pool of the same RAM. `device_free_bytes()` (the figure `--expert-cache auto` and
the memory plan use) counts what Linux can give back (`MemAvailable` less 6 GiB for the system, `STRATA_UMA_HEADROOM_GIB`
changes the 6) instead of only `MemFree`, per device, for any integrated GPU on Linux (this applies to a DGX Spark too).
- **gfx11 matrix cores.** The prompt attention, the block scorer, the prompt GEMMs and the prompt experts have WMMA kernels for
gfx1100 / 1101 / 1102 / 1150 / 1151. A gfx11 part outside that list (gfx1103, gfx1152) is not given them: it takes the portable path.
- **The hipBLASLt table** `tools/hip/gfx1151-hipblaslt-100401.txt` (ROCm 7.14.1's hipBLASLt 1.x `100401`), including the prompt shapes
at `--prefill 16384` (T 16383 / 16384).
## 1. The toolchain (no root needed)
ROCm 7.14.1 for gfx1151 from AMD's TheRock builds, extracted into a folder of your own:
```sh
mkdir -p ~/rocm && cd ~/rocm
curl -LO https://repo.amd.com/rocm/tarball-multi-arch/therock-dist-linux-gfx1151-7.14.1.tar.gz # 1.7 GB
sha256sum therock-dist-linux-gfx1151-7.14.1.tar.gz # c40e8f2bd6630a7d11557c762b99c6fa8afb04c9bd0e51ed1675ee1ca24afb00
mkdir install && tar -xzf therock-dist-linux-gfx1151-7.14.1.tar.gz -C install
export SDK=$HOME/rocm/install
export ROCM_PATH=$SDK HIP_PATH=$SDK HIP_PLATFORM=amd
export PATH=$SDK/bin:$SDK/lib/llvm/bin:$PATH
export LD_LIBRARY_PATH=$SDK/lib:$SDK/lib/rocm_sysdeps/lib:$SDK/lib/llvm/lib:${LD_LIBRARY_PATH:-}
```
You also need CMake 3.24+, Ninja, git and a C++ host toolchain with libstdc++ headers (`sudo apt install build-essential cmake ninja-build git`).
The maintainers' box has no system compiler: they point ROCm's clang at a user-space copy of gcc 15 with
`--gcc-toolchain=<dir>/usr` (added to `CMAKE_HIP_FLAGS` below; leave it out when the system has one).
The kernel's amdgpu driver and `/dev/kfd` access (your user in the `render` and `video` groups) are all the GPU side needs.
To let the GPU use most of a 128 GB machine the maintainers' box boots with `ttm.pages_limit=29360128 ttm.page_pool_size=29360128`
(a 112 GiB GTT; the default is about half of the RAM) and `amd_iommu=off`; both are host settings the engine does not need to run,
only to have the room.
## 2. The engine
llama.cpp's `ggml` sources are pinned by this repository: CMake fetches the revision it needs. For an offline build, point
`STRATA_GGML_DIR` at a checkout of exactly that revision.
```sh
git clone https://github.com/Niko1221/Strata.git && cd Strata
cmake -S . -B build-halo -G Ninja -DCMAKE_BUILD_TYPE=Release \
-DSTRATA_ENABLE_HIP=ON -DSTRATA_ENABLE_CUDA=OFF -DSTRATA_PREFILL_MMQ=ON \
-DCMAKE_HIP_ARCHITECTURES=gfx1151 \
-DCMAKE_HIP_COMPILER=$SDK/lib/llvm/bin/clang++ -DCMAKE_HIP_COMPILER_ROCM_ROOT=$SDK \
"-DCMAKE_PREFIX_PATH=$SDK;$SDK/lib/rocm_sysdeps;$SDK/lib/llvm" \
"-DCMAKE_HIP_FLAGS=--rocm-path=$SDK --rocm-device-lib-path=$SDK/lib/llvm/amdgcn/bitcode"
cmake --build build-halo --target strata -j 8 # about 25 minutes the first time
```
`gfx1151` is "unvalidated" in the build's own wording (it warns). `STRATA_PREFILL_MMQ=ON` builds ggml's MMQ kernels the prompt
experts need. The tests: `cmake --build build-halo -j 8` builds them all, `ctest --test-dir build-halo` runs them;
`hip_prefill_hcd_exact_parity` and `hip_prefill_hipblaslt_gemm` need the table (next section) and skip without it.
## 3. The prompt table, and running
Hand the engine the table (`setup` does it for the supported cards; for gfx1151 you do it): without it the prompt's dense matrix
products use plain hipBLAS and are slower.
```sh
export STRATA_HIPBLASLT_TUNING=$PWD/tools/hip/gfx1151-hipblaslt-100401.txt
```
The engine accepts it only for the architecture and the hipBLASLt version named on the table's second line
(`STRATA_HIPBLASLT_TUNING_V1 gfx1151 100401`: ROCm 7.14.1) and says so at start-up (`prefill gemm: hipBLASLt tuning enabled (90 rows,
gfx1151, version 100401)`). Then run `strata` with the same arguments as on any other card (the model packs, `--serve`, the
settings of [DETAILS.md](DETAILS.md)); a pack of the Unsloth UD-IQ4_XS or UD-Q4_K_XL GGUFs is what the numbers below used.
## 4. What is on by default on gfx1151
At start the engine looks at GPU 0; on a gfx1151 it sets the switches below unless you set them yourself
(`STRATA_GFX1151_DEFAULTS=0` turns the whole table off, and the start-up line names what was set). **Every one of them gave
byte-identical output on the maintainers' box** (the same token ids, 4K / 32K / 64K and four models, and a bitwise harness per kernel):
they change speed, not answers. Nothing here applies to CUDA or to another AMD architecture.
| Switch | What it does |
|---|---|
| `STRATA_GDN_HEAD`, `STRATA_GDN_PP=2`, `STRATA_GDN_CONVL2`, `STRATA_GDN_NOY` | the prompt's gated-delta-net recurrence: four lanes per column, two columns per lane, the convolution and the norms fused |
| `STRATA_HCD_EXACT` | the hyper-connection down projection (N 320, K 10240) in a WMMA kernel that reproduces hipBLASLt solution 1176 / 1177's summation order (`hip_prefill_hcd_exact_parity` checks it bit for bit); it runs only where the table makes hipBLASLt pick one of those two, and says once if it cannot |
| `STRATA_CVEC_FUSE`, `STRATA_PF_PAD` | a steered layer's write, control vector and next norm in one pass; padded GEMM row strides |
| `STRATA_Q8_PACKED`, `STRATA_Q6_PACKED`, `STRATA_MMVF_ROWS`, `STRATA_ATTN_LANECELL` | decode layouts and kernels (packed Q8_0 / Q6_K weights, 4-row BF16 GEMV, one-cell-per-thread attention scores) |
| `STRATA_EXPERT_V2`, `STRATA_TSUM`, `STRATA_LFUSE` | the grouped decode experts (IQ3_S / IQ4_NL), single-butterfly warp sums, fewer launches around the shared expert and the KV append |
| `STRATA_GDN_SPLIT`, `STRATA_QFUSE`, `STRATA_PLE_BATCH` | the GDN step over four blocks per head, activation images written by their producers, the PLE key / value projections of a verify window at once |
| `STRATA_SH_STREAM=1` | the shared expert on a second stream beside the routed experts. It is off by default on other AMD cards (#816), but it pays on gfx1151: decode +1.8% (UD-IQ4_XS) and +6.7% (UD-Q4_K_XL) at 8K |
## 5. What is not on by default (it changes bits)
These are faster on gfx1151 and **round differently** from the default path (a different summation order or a half-precision
intermediate), so they are opt-in and the maintainers gate them with KL against the default (`STRATA_PF_SWITCH_MIN_T=4096` keeps
the short prompts on the default numerics, since these pay on long ones). The maintainers' fast configuration sets all of
them:
```sh
export STRATA_PF_FUSED=1 # the prompt experts on the matrix cores (IQ2 / IQ3 / IQ4_XS gate/up, Q2_0 / IQ4_NL / Q8_0 down)
export STRATA_PF_GEMM=1 # the prompt's FP16 projections on the 128 x 256 WMMA GEMM
export STRATA_HC_UPMIX=1 # the hyper-connection up projection with its epilogue
export STRATA_PA_FAST=1 # the prompt attention with single FP16 q and p
export STRATA_HIP_WMMA=1 # the prompt attention on matrix cores
export STRATA_SELECT_WMMA=1 # the block scorer on matrix cores
export STRATA_HC_Q8=1 # the hyper-connection read from the GGUF's own Q8_0 projections
export STRATA_PF_SWITCH_MIN_T=4096
```
Two engine flags help on long contexts and are not Strix-specific: `--mtp-window 8192` (the draft layer attends to the last 8,192
cells: 64K output +3.5%, 32K -0.4%) and `--spec`, `--lookup-chain`, `--mtp-q4` (see [DETAILS.md](DETAILS.md)).
## 6. Measured
Ryzen AI Max+ 395 (128 GB, gfx1151), the maintainers' fast configuration of section 5 on top of the defaults of section 4, `--spec 4`,
`--mtp`, `--lookup-chain 3`, `--mtp-q4 all`, `--prefill 16384`, int8 KV, all experts in the unified memory. Prompt tok/s is
(prompt tokens - 1) / prompt time, output tok/s is the tokens / decode time; medians of 3 runs, one fresh process per run, the two
engines interleaved. "Before" is the maintainers' earlier engine branch (the same switches set by hand), "now" is this release.
Every pair printed the same token ids.
| Model | Context | Prompt before -> now | Output before -> now |
|---|---|---|---|
| UD-IQ4_XS | 8K | 1,212 -> 1,293 (+6.7%) | 51.7 -> 53.8 (+4.0%) |
| UD-IQ4_XS | 128K | 1,241 -> 1,320 (+6.4%) | 49.3 -> 51.4 (+4.2%) |
| UD-Q4_K_XL | 8K | 1,136 -> 1,169 (+2.9%) | 50.6 -> 52.6 (+3.9%) |
| UD-Q4_K_XL | 128K | 1,220 -> 1,248 (+2.4%) | 41.6 -> 43.3 (+3.9%) |
| IQ3_S | 8K | 1,202 -> 1,242 (+3.4%) | 56.2 -> 59.7 (+6.3%) |
| IQ3_S | 128K | 1,254 -> 1,312 (+4.6%) | 40.6 -> 42.8 (+5.5%) |
| IQ3_XXS | 8K | 1,237 -> 1,238 (+0.1%) | 48.8 -> 51.5 (+5.6%) |
| IQ3_XXS | 128K | 1,259 -> 1,252 (-0.6%) | 48.4 -> 50.9 (+5.1%) |
UD-IQ4_XS at 4K / 32K / 64K (5-6 interleaved pairs): prompt +9.2% / +6.0% / +5.3%, output +5.0% / +3.9% / +4.6%.
IQ3_XXS at 8K was measured again with 6 interleaved pairs (an earlier 3-run set had read 5% low while other jobs shared the GPU): prompt equal, output +5.6%. With the defaults off the same build's prompt is 8% lower (1,137), so the table helps there too.
### Re-measured on the merged 0.1.40 code
The same box and method, no environment switches at all (the arch defaults only), on the final merged tree. 4 interleaved pairs per cell,
medians; "j" is the build of the table above (the Aurora port before the other 0.1.40 work was merged), "merged" is the release
code. Every run printed the same token ids, and the ids also match `strata-int29` (the v4.2 config) on UD-IQ4_XS and UD-Q4_K_XL at
a8 / 4K / 64K.
| Model | Context | Prompt j -> merged | Output j -> merged |
|---|---|---|---|
| UD-IQ4_XS | 8K | 1,260 -> 1,159 (-8.0%, spread 1,070-1,320) | 53.94 -> 53.14 (-1.5%) |
| UD-IQ4_XS | 128K | 1,317 -> 1,299 (-1.4%) | 51.38 -> 50.72 (-1.3%) |
| UD-Q4_K_XL | 8K | 1,163 -> 1,104 (-5.0%, spread 890-1,240) | 52.66 -> 50.47 (-4.2%) |
| UD-Q4_K_XL | 128K | 1,252 -> 1,211 (-3.3%) | 43.28 -> 42.64 (-1.5%) |
| IQ3_S | 8K | 1,283 -> 1,265 (-1.4%) | 59.81 -> 59.10 (-1.2%) |
| IQ3_S | 128K | 1,311 -> 1,310 (-0.1%) | 42.74 -> 42.09 (-1.5%) |
| IQ3_XXS | 8K | 1,127 -> 1,212 (+7.6%) | 51.64 -> 50.95 (-1.3%) |
| IQ3_XXS | 128K | 1,252 -> 1,250 (-0.1%) | 50.88 -> 50.22 (-1.3%) |
The prompt numbers at 8K swing by more than the differences (other jobs share this box), so read them as unchanged. The output
column moved by a steady -1.2% to -1.5% (-4% on UD-Q4_K_XL at 8K, where the default arm also had slow runs): since 0.1.39 the
shared-expert stream fork is off by default on HIP (#816). It overlaps the shared expert with the routed experts on a second
stream and does help on gfx1151, so 0.1.40 turns it on there by default (section 4). It changes no bits (same ids). 4 interleaved pairs at 8K, merged
code with the fork off -> on: UD-IQ4_XS output 53.1 -> 54.05 (+1.8%), UD-Q4_K_XL 49.5 -> 52.8 (+6.7%, the default arm ranged 46.0-52.1),
prompt unchanged. With the fork on, the merged code's output is at or above the j build's (IQ4_XS 54.05 vs 53.94).
+32 -1
View File
@@ -21,13 +21,37 @@ Update it (NVIDIA App or [nvidia.com/drivers](https://www.nvidia.com/drivers)),
**It says port 8080 is already in use.**
Strata is already running. Look for its window. Or another program uses the port: `START-HERE.bat --port 8081`.
Only an address that is really taken says "already in use". Any other reason (Windows keeps the port reserved, or
the config's `"host"` is not an address of this PC) is printed as what the OS said, with what to change (#769).
**"Windows blocked the Strata engine (or the image encoder) ... Smart App Control".**
On a clean Windows 11, Smart App Control refuses programs that are not signed, and Strata's `strata.exe` and
`strata-vision.exe` are not (#735). Turn it off (Windows Security > App & browser control > Smart App Control
settings; it cannot be turned back on without a reset of Windows), or, if only the image encoder is blocked, run
`START-HERE.bat --setup --vision no`.
**A new engine misbehaves after an update.**
An update keeps the engine it replaced in `engine\.previous` (one generation, about 210 MiB).
`python setup.py --rollback-engine` puts it back (and keeps the newer one there: run it again to go forward) (#670).
**Python or the build tools could not be installed.**
Install what it names (links are printed), then run it again. Everything already done is kept.
**Linux: the engine does not compile (`unsupported GNU version`, or `exception specification is incompatible` for
`cospi`/`sinpi`/`rsqrt`).** Two known mismatches between the CUDA toolkit and a new Linux (#601):
- gcc newer than 14 (Ubuntu 26.04's default 15): CUDA 12.x and 13.0 refuse it. Install g++-14 and run
`CXX=g++-14 CUDAHOSTCXX=g++-14 ./setup.sh`.
- glibc 2.43 with CUDA 12.9: the toolkit's math headers clash with glibc's, already in CMake's first test compile.
Use CUDA 12.8 or 13.x instead. Setup takes the newest toolkit it finds; `STRATA_NVCC=/usr/local/cuda-12.8/bin/nvcc
./setup.sh` makes it use that one (only that one).
**The first start takes minutes.**
It is reading 34-55 GB into RAM; the second start is faster while the files are in the OS cache. Started from Task
Scheduler, it can be 24x slower: see [Running it at startup](DETAILS.md#running-it-at-startup-task-scheduler).
On Linux the engine now asks the kernel to read the model files ahead (a cold start went from ~920 s to 70 s on one
PC); `STRATA_READ_AHEAD=0` turns that off. On Linux with transparent huge pages on `always`, the arena no longer asks
for `MADV_HUGEPAGE` on top (a fragmented machine spent minutes compacting memory, #771); `STRATA_NO_ARENA_THP=1` skips
that request on any setting.
## While it answers
@@ -46,9 +70,16 @@ context.
The monitor plugged into the graphics card and other GPU programs take VRAM from the expert cache; RAM running below
its rated speed (enable EXPO/XMP in the BIOS) slows the CPU half.
**An earlier reply that came back empty is gone from the chat history (0.1.40, #843).**
The server leaves out finished assistant turns that have no text, so the next reply does not copy the empty ones.
`STRATA_KEEP_EMPTY_TURNS=1` in the server's environment renders them as before.
**A Turing card (RTX 20) reads prompts above ~90K tokens differently (0.1.40, #743).**
The prompt's top-k selection takes a wider kernel there, with the same ids. `STRATA_TOPK_STREAM=0` restores the old one.
**Pictures are refused, or slow.**
"this server was started without the vision encoder": the model was set up for text only - run setup again with
`--vision gpu` (or `--vision cpu`). Pictures that take 10-30 s are read by the encoder on the CPU; `--vision gpu`
`--vision gpu` (or `--vision cpu`). Pictures that take several seconds (about 3 s at 300 image tokens on 8 cores, more with more tokens) are read by the encoder on the CPU; `--vision gpu`
(NVIDIA, ~1.4 GB of VRAM) makes it 0.1-0.5 s.
## AMD cards
+70 -6
View File
@@ -140,11 +140,17 @@ model's (`data/expert-profile.bin`); it ranks the same 48 x 512 experts and deci
budget take first.
**Choosing the RAM budget N:** your RAM minus 20-24 GB (the OS, the engine itself, the 126 MB router copy, and room for
the OS file cache that serves the rest). On 64 GB, 40. The engine clamps a budget larger than the RAM it finds free
(minus 4 GB and a 256 MiB margin) and says so; before #403's fix such a clamped budget could then fail the start. Everything above N comes from the SSD for every token, so N is the setting that matters
the OS file cache that serves the rest). On 64 GB, 40. The engine checks available RAM and, on Windows, available
commit capacity. If N exceeds the smaller value minus 4 GiB headroom, it reduces N to that amount minus a 256 MiB
margin and reports which limit applies. Before #403's fix a clamped budget could then fail the safety check.
Everything above N comes from the SSD for every token, so N is the setting that matters
most; a bigger budget was faster in every measurement (24 / 32 / 40 GiB). When the driver page-locks the whole budget
(24 GiB did on this PC, 32 and 40 did not), the GPU also computes a share of the misses over PCIe, as in the resident
low-RAM mode; `STRATA_RESIDENT_PIN=0` keeps the budget locked only (the CPU then computes every miss).
low-RAM mode; `STRATA_RESIDENT_PIN=0` uses ordinary pageable memory (the CPU then computes every miss).
On Windows, the allocation log reports available physical RAM and commit capacity before allocation and after a
failure. Free RAM alone does not guarantee that Windows can commit the requested memory. If the pageable fallback
also fails, the warning preserves both the GPU allocation or mapping error and the `malloc` error.
**Context:** measured at 4K. The KV cache takes VRAM from the expert cache, so a longer context makes decoding slower;
8K is a reasonable start on 12 GB. `--kv int8` halves the KV cache's VRAM.
@@ -221,13 +227,71 @@ engine with `--short-read` covering the positions to compare; llama.cpp's side w
| `STRATA_PARTIAL_PIN=1` | Registers the hottest part of the RAM budget (up to `STRATA_PARTIAL_PIN_GIB`, default 24) with the GPU driver, so the GPU computes a share of the misses over PCIe (`--pcie-frac`). Measured no faster on this PC, and it changes the numerics of those experts (GPU kernels instead of the CPU's), so off. |
| `STRATA_FETCH_THREADS=N` | Threads that read the experts from the GGUF while it answers (default 8; 16 was no faster). The prompt path has its own: `STRATA_STAGER_THREADS` (default 32 here) and `STRATA_STAGER_RING` (128). |
## UD-IQ4_XS (setup from 0.1.39, #621)
Unsloth's `UD-IQ4_XS` at the same revision is in setup too, as a regular choice (not experimental; the Unsloth
family's first size and its default): `START-HERE.bat --setup --family unsloth --model UD-IQ4_XS` (engine 0.1.38 or
newer). It sits between IQ3_S and UD-Q4_K_XL: its routed experts are IQ3_S gate/up (IQ4_XS
in one layer) with IQ4_NL downs (Q8_0 in five layers), 59.5 GB of them; the dense side (Q8_0 projections, the IQ4_NL
PLE table, a Q6_K head) is UD-Q4_K_XL's. Three shards, 93.7 GB:
| File | Bytes | SHA-256 |
| --- | ---: | --- |
| `Qwen3.8-Flash-Next-UD-IQ4_XS-00001-of-00003.gguf` | 10,946,624 | `5ce89370720f8bf90890f439361282104c1aa1482d4013bb9a50923e758e71a4` |
| `Qwen3.8-Flash-Next-UD-IQ4_XS-00002-of-00003.gguf` | 49,835,229,856 | `577a38a2392b40ca2193cea502e1d92f60b8cd370675d308e0ec21885d9daaa7` |
| `Qwen3.8-Flash-Next-UD-IQ4_XS-00003-of-00003.gguf` | 43,836,407,744 | `d4634e6d84f0ebb0940be15c90d3790bf6464e3dea3a1cddc567dc0e83ad8833` |
Setup treats it like UD-Q4_K_XL (the list above): the same RAM budget (your RAM less 24 GB; at most all 55 GiB of its
experts, so a PC with ~80 GB of RAM or more holds all of them), the pack with `--compat-bf16`, no `experts.bin`, one
GPU by default. It does not ask on AMD: its experts' formats have prompt kernels there too. Images are an option (asked,
off by default; NVIDIA): the image path has no restriction for this pack, which uses the original model's image
encoder; not yet run with images.
What is known so far: it packs and runs on a Strix Halo (AMD gfx1151, 128 GB unified memory), where prompts read at
~820 tokens/s at 8K and 64K and the answers passed the retrieval checks at 8K and 64K. It has **not** been measured on
NVIDIA yet; please report what you see. One fidelity note that applies to every `--compat-bf16` pack (UD-Q4_K_XL too):
the pack rounds the file's Q8_0 hyper-connection projections to BF16, and on the Strix Halo that rounding put
perplexity 6-9% above llama.cpp's on the same file (teacher-forced, short context). Reading the Q8_0 values instead
closes most of that gap; it is measured on AMD only and not in this release.
## Q4_0 files
A plain `llama-quantize` Q4_0 file of this model runs the same way: pack shard 1 with `--compat-bf16` as above and
point `--native` at it. Tested:
[bartowski/Qwen3.8-Flash-Next-GGUF, `Qwen3.8-Flash-Next-Q4_0`, revision `928589f`](https://huggingface.co/bartowski/Qwen3.8-Flash-Next-GGUF/tree/928589fdb66c6ff07f22ac561e3fbce76553548f/Qwen3.8-Flash-Next-Q4_0)
(3 shards, 100.6 GB). Its routed experts are Q4_0 for gate/up and for down, except down in layers 0-5, which is Q4_1;
the PLE table and the token embedding are Q4_0. The pack serves 302 tensors from the GGUF as stored and converts 362
(168 exact, 194 rounded to BF16, largest absolute error 0.0156; 1.23 GiB).
The Q4_0 expert kernels centre the codes (q - 8) before the integer dot product, as ggml-cpu's Q4_0 x Q8_0 dot does,
instead of llama.cpp's CUDA form (uncentred codes, minus 8 x the Q8_1 block sum in float); Q4_1 follows the Q5_1
kernel without the fifth bit. `native_expert_parity` on this file (layers 0 and 5: Q4_0/Q4_1, layers 20 and 47:
Q4_0/Q4_0): relative error against the float product 1.1-1.3e-2 on the CPU and the GPU alike, CPU vs GPU 1.1e-7 for
Q4_0/Q4_0 and 5e-4 to 1.1e-3 with Q4_1 down.
Measured on one RTX 3060 12 GB (PCIe 3.0 x16), Xeon E5-2696 v4, 121 GiB DDR4-2400, Linux, CUDA 12.4; every expert in
RAM, `--max-context 131072 --kv int8 --kv-resident 20480`, expert cache `auto` (1,960 slots, 5.06 GiB), prompt chunk
`auto` (6,144 tokens); greedy, `reasoning_effort` medium, one request at a time:
| Prompt | Prompt read | Output |
| --- | --- | --- |
| 63 tokens (prose) | - | 25.4 tok/s |
| 111 tokens (code) | - | 28.0 tok/s |
| 24,461 tokens | 787 tok/s | 24.0 tok/s |
| 67,934 tokens (a fact hidden in the middle: found) | 737 tok/s | 26.4 tok/s |
A coding prompt was answered correctly. Not measured: agreement with llama.cpp on this file (as
[above](#quality-against-llamacpp-on-the-same-file) for UD-Q4_K_XL), AMD, Windows, and bartowski's Swift-1.5 Q4_0,
whose header shows the same formats.
## Scope and validation
- Only UD-Q4_K_XL at revision `38bb39e` is targeted. Other Unsloth quantizations use formats this engine may not
have kernels for; the engine checks every layer's formats at start and refuses an unsupported one by name.
- UD-Q4_K_XL and UD-IQ4_XS at revision `38bb39e` are targeted, and plain Q4_0 files ([above](#q4_0-files)). Other
quantizations use formats this engine may not have kernels for; the engine checks every layer's formats at start
and refuses an unsupported one by name.
- Tests: the packer's synthetic 4-shard and conversion tests (`.venv/bin/python -m unittest discover -s tools -p
test_iq_pack.py`); CTests `gguf_split_test`, `expert_layout_test`, `native_expert_parity_*` (the three real expert
format pairs against ggml-cpu, the Q5_1 min term, Q8_0 rows), `prefill_mmq_kquant_test` (with `-DSTRATA_MMQ_KQUANTS=ON`: the
format pairs against ggml-cpu, the Q5_1 min term, Q8_0 rows, Q4_0/Q4_0 and Q4_0/Q4_1), `prefill_mmq_kquant_test` (with `-DSTRATA_MMQ_KQUANTS=ON`: the
prompt path's MMQ products for Q4_K / Q5_K / Q5_1 / Q8_0 against ggml's dequantized weights); the in-place mode against `experts.bin` on the Coder
(identical tokens and logits).
- Real runs: greedy answers to a coding prompt (correct) at every budget and setting above, identical across them;
+142
View File
@@ -0,0 +1,142 @@
# Unsloth UD-Q6_K_XL (experimental)
**Experimental; manual workflow only — setup does not offer this model yet.** This is Unsloth's 6-bit file of the
same model the other packs use:
[unsloth/Qwen3.8-Flash-Next-GGUF, `UD-Q6_K_XL`, revision `38bb39e`](https://huggingface.co/unsloth/Qwen3.8-Flash-Next-GGUF/tree/38bb39ee97821de2c9009abb7e93950eec396e66/UD-Q6_K_XL).
Build it for engine 0.1.38 or newer (earlier engines read `per_layer_token_embd.weight` in IQ4_NL, Q5_0 or FP8 only,
below). UD-Q4_K_XL's manual workflow asks nothing of setup, and neither does this one.
> **0.1.40 status.** The Q6_K gate/up kernels are an opt-in build (`-DSTRATA_Q6K_EXPERTS=ON`, plus `-DSTRATA_MMQ_KQUANTS=ON`
> for the prompt kernels): every grouped-kernel instance is loaded at start and would cost other models expert slots. The
> Q8_0 PLE table reader is not in this release (it comes with the PLE format series, PR #651); until then this file does
> not load. The parity programs below have not been run on a GPU yet.
## What is in the file
Six shards, 169,165,382,688 bytes (169.2 GB) together ([sizes and SHA-256 from the HF tree API](https://huggingface.co/api/models/unsloth/Qwen3.8-Flash-Next-GGUF/tree/38bb39ee97821de2c9009abb7e93950eec396e66/UD-Q6_K_XL);
shards 1, 2 and 6 were also checked bit for bit against their Hugging Face LFS hashes after download). Shard 1 holds
only metadata; shard 2 only the output head (`output.weight`, `output_hc_*`, all Q8_0 but `output_hc_norm`, F32);
shard 3 is exactly the PLE table, nothing else.
| File | Bytes | SHA-256 |
| --- | --- | --- |
| `Qwen3.8-Flash-Next-UD-Q6_K_XL-00001-of-00006.gguf` | 10,946,624 | `8bdc6bad55b6699f46f60708829932ff4018d7ac9bd309c388534fa31753bdb7` |
| `Qwen3.8-Flash-Next-UD-Q6_K_XL-00002-of-00006.gguf` | 682,434,912 | `494ca4ed3dbf97bc28da88af3890b8877b9032f909812d00c0526a9ca5e91d2e` |
| `Qwen3.8-Flash-Next-UD-Q6_K_XL-00003-of-00006.gguf` | 54,400,261,312 | `34efd79a80a1ce540a517a5d56171924b66ce1c38b04c904f17ad6d8ef17cf20` |
| `Qwen3.8-Flash-Next-UD-Q6_K_XL-00004-of-00006.gguf` | 49,814,108,512 | `0ea4b599880a5a52fcd9c188ba1443f646c69c96571904ddbd6a79246a01a997` |
| `Qwen3.8-Flash-Next-UD-Q6_K_XL-00005-of-00006.gguf` | 49,419,317,344 | `9948e81ae8368144b135a7403d8c0f2cf9a6c2f9f692313865855b2832e696c9` |
| `Qwen3.8-Flash-Next-UD-Q6_K_XL-00006-of-00006.gguf` | 14,838,313,984 | `e4ae255234f42b18012f6d6eec5bf615b57479b7e0a0233c47436a321ffeec3f` |
Formats, read from the shard headers with `tools/gguf_reader.py` (all six shards verified: shard 4 = blocks 0-20,
520 tensors, shard 5 = blocks 20-41, 530 tensors, and per layer the types above):
- **Routed experts:** `ffn_gate_exps` and `ffn_up_exps` are Q6_K (seen in every layer of shard 6: blocks 41-47);
`ffn_down_exps` is Q8_0 (per layer 891.3 MB at 640×2560×512, against Q6_K's 688.1 MB at 2560×640×512 for gate or
up). Gate and up are Q6_K in 47 of 48 layers and Q8_0 in layer 2; down is Q8_0 in every layer (the pack's
`native_experts.txt`: 47 rows of ggml types 14/8, and layer 2 is 8/8). Q6_K gate/up is native: the
engine multiplies them directly on the GPU in every path check below, and the CPU has the ggml block layout (210
bytes per 256 elements); the per-role layout in `native_experts.txt` v4 handles shards whose boundaries cut
through a layer like UD-Q4_K_XL's layer 11 case does.
- **Everything else:** attention and shared-expert projections, SSM, hyper-connections, the PLE key/value, the token
embedding and the output head are Q8_0, or F32/BF16 where the engine reads floats — the same pattern
[UD-Q4_K_XL](UNSLOTH_Q4.md) handles with `tools/iq_pack.py --compat-bf16`.
- **The PLE table** (`per_layer_token_embd.weight`, [160, 320001536]): **Q8_0**, its own shard 3. A row is
5 blocks of 32 elements at 34 bytes = 170 bytes, and the whole table is 54.4 GB, against the 28.8 GB IQ4_NL of
UD-Q4_K_XL. Engine 0.1.38 reads Q8_0 rows natively: `PleTable` accepts the type, the row reader is already
parameterized by bytes-per-row (its selftest runs 90, 110 and 170 byte rows), and no GPU kernel changes —
`gather` outputs floats. (UD-Q4_K_XL's table is IQ4_NL; the FP8 table of [tools/ple_fp8_pack.py](../tools/ple_fp8_pack.py)
needs the original checkpoint, which this file is not a substitute for. Converting this Q8_0 table to FP8 would
trade table precision away for 6% smaller rows; Q8_0 read directly has none that conversion's error.)
Q6_K gate/up costs 210 bytes per 256 elements, against Q4_K's 144, so the routed experts' share of the file grows
about 45% per element read; expected decode speed accordingly lower than UD-Q4_K_XL's on the same hardware. The
table row gather grows 170/90 = 1.89x over IQ4_NL.
## The pack (by hand)
Same workflow as [UD-Q4_K_XL](UNSLOTH_Q4.md#the-pack-by-hand), from the repository root:
```sh
.venv/bin/python tools/iq_pack.py \
--gguf /path/to/Qwen3.8-Flash-Next-UD-Q6_K_XL-00001-of-00006.gguf \
--out packs/ud-q6_k_xl --compat-bf16
```
**`--compat-bf16` is required** for the same reason as there (the 195 small Q8_0 projections the engine reads as
BF16). Do not add `--experts-bin`; the experts stay in the GGUF files. Built here end to end on this file
(`packs/ud-q6_k_xl`, the packer's own tests cover the shard and conversion logic via
`.venv/bin/python -m unittest discover -s tools -p test_iq_pack.py`): 1079 tensors indexed, 303 served natively, 460
tensors converted (264 exact, 196 rounded, max abs error 0.0144 - the same profile as UD-Q4_K_XL's), 195
compat-BF16 tensors (1.20 GiB), arena 1.38 GiB, and `native_experts.txt` v4 with the per-role shard column on
layers 20 and 41.
## The server
Like UD-Q4_K_XL's [config](UNSLOTH_Q4.md#the-server), with the Q6 paths and names. `--ple-gguf` is not needed: the
engine finds shard 3 by the table's name. On a machine whose RAM holds the ~110 GB of routed experts plus the rest
(499 GB total, ~325 GB free, measured on the machine this was written on), there is no RAM budget and no
`--expert-cache` gymnastics: drop `--resident-budget-gib` and `--expert-cache-device1` from the Q4 config. The
54.4 GB table is mmap'd (the default) — do not switch it to `--ple-io ram` unless the RAM really is there to lock
it. `mtp/rt` is the base model's draft layer as always.
```json
{
"exe": "build/strata",
"args": [
"--pack", "packs/ud-q6_k_xl",
"--native", "/path/to/Qwen3.8-Flash-Next-UD-Q6_K_XL-00001-of-00006.gguf",
"--prefill", "auto", "--spec", "4", "--spec-min-p", "0.5",
"--mtp", "mtp/rt", "--max-context", "8192"
],
"cwd": ".",
"tokenizer": "packs/ud-q6_k_xl/tokenizer",
"model_name": "qwen3.8-flash-next-ud-q6_k_xl",
"log": "strata-ud-q6_k_xl.log",
"host": "127.0.0.1",
"port": 8080
}
```
On 64 GB machines treat the Q4 numbers below as the starting point: the RAM budget matters the same way, and
everything it leaves out comes from the SSD, faster now per expert (Q6_K) and per table row (170 bytes).
## What the engine did not have, and has now
UD-Q4_K_XL's expert formats (gate/up Q4_K, down Q5_1) were in every path; Q6_K gate/up was not. This branch adds
it (the PLE Q8_0 reader is the other half):
- **Decode windows:** the grouped expert kernels (`native_expert_grouped`) take gate/up Q6_K — a pinned copy of
llama.cpp's `vec_dot_q6_K_q8_1` behind `Fmt<14>`, and the dequantizer (`dequantize_q6_K` folded onto the kernel's
32 threads). Per-token MMVQ (`native_mmvq`) already had Q6_K.
- **The prompt path:** llama.cpp's MMQ prompt kernels now instantiate Q6_K (`STRATA_MMQ_KQUANTS=ON` builds
`template-instances/mmq-instance-q6_k.cu` alongside q4_k/q5_k/q5_1; `moe_mmq` accepts and dispatches it).
- **The gates:** `native_expert_supported` took Q6_K/Q8_0 pairs (gate/up Q8_0 as in layer 2 was already in the
lists) — the engine previously refused the whole pack with "no GPU kernels for".
Tests: `native_grouped_parity` loops Q6_K/Q8_0 pairs; `prefill_mmq_kquant_test` runs Q6_K gate/up products at
1280×2560 against a double-precision reference; `native_expert_parity --synthetic q6_K/q8_0` checks GPU and CPU
against ggml's dequantized weights. All three are built (CUDA, `86;120`). They have not been run on a GPU: the
card was serving the file below.
## Status: what is verified and what is not
Verified here (test clone, Linux, NVIDIA GPU, source build):
- The six files' sizes and SHA-256 (HF tree API; shards 1, 2, 6 additionally against the downloaded blobs).
- The tensor inventory of shards 1, 2 and 6, read from the headers (`tools/gguf_reader.py`); shards 4 and 5's
tensor counts and per-layer expert types, from the pack's `conversions.json` and `native_experts.txt` v4.
- Q8_0 PLE rows: `ple_reader_test --selftest` runs 90, 110 and 170-byte rows (all OK); `ple_q8_parity` on a
synthetic Q8_0 table (`tools/ple_q8_fixture.py`) matches ggml's reference dequantizer bit for bit through both
Mmap and Direct (`max_abs 0.000e+00`). Both still pass on this branch with the Q6_K expert work in.
- The pack on the finished file (numbers above), and the engine binary builds (CUDA `86;120`, MMQ K-quants on).
- Q6_K expert source support: decode-side MMVQ was already there; the grouped verify-window kernels, the MMQ
prompt kernels and the `native_expert_supported` gate were not, and are in this branch (section above).
The server has run this file. On 2026-10-04, Linux, an RTX PRO 4500 Blackwell (compute capability 12.0) plus a
second GPU, a Threadripper PRO 5975WX (AVX2, no AVX-512), 30 expert-pool workers and 499 GB of RAM, engine 0.1.39
loaded 101.75 GiB of experts (8.43 GiB/s) and served one review. The cold read was 14,915 tokens at 3,176 tok/s.
Fourteen turns generated 5,799 tokens in 74.1 s (78.3 tok/s) and finished at 44,160 tokens of context; drafts were
accepted 3,485 of 4,406 (79.1%). The same machine on engine 0.1.38, before this branch was rebased onto 0.1.39,
read 249,193 tokens cold at 2,802 tok/s and then generated 1,079 tokens at 69.0 tok/s. Quality against llama.cpp
on this file has not been measured, so the page stays experimental. The three Q6_K parity programs above are the
remaining GPU check.
+16
View File
@@ -109,6 +109,22 @@ inline void dequantize_q5_0(const uint8_t* block, float* out) {
out[j + 16] = (float)(x1 - 16) * d;
}
}
// ---- Q5_1: 32 elements from a 24-byte block: fp16 d, fp16 m, then Q5_0's qh[4] and qs[16]. The 5-bit codes are
// unsigned and the block's minimum m is added instead of Q5_0's fixed -16 offset (dequantize_row_q5_1).
inline void dequantize_q5_1(const uint8_t* block, float* out) {
const float d = fp16_to_fp32(read_u16(block));
const float m = fp16_to_fp32(read_u16(block + 2));
const uint8_t* qh = block + 4;
const uint8_t* qs = block + 8;
const uint32_t h =
(uint32_t)qh[0] | ((uint32_t)qh[1] << 8) | ((uint32_t)qh[2] << 16) | ((uint32_t)qh[3] << 24);
for (int j = 0; j < 16; ++j) {
const int x0 = (int)(qs[j] & 0x0F) | (int)(((h >> j) & 1u) << 4);
const int x1 = (int)(qs[j] >> 4) | (int)(((h >> (j + 16)) & 1u) << 4);
out[j] = (float)x0 * d + m;
out[j + 16] = (float)x1 * d + m;
}
}
// ---- IQ4_NL: 32 elements from an 18-byte block. Non-linear 4-bit codebook, NOT a linear grid, so
// the table must be exact. Copied verbatim from ggml-common.h `kvalues_iq4nl` at 3cf03257:
// -127, -104, -83, -65, -49, -35, -22, -10, 1, 13, 25, 38, 53, 69, 89, 113
+28
View File
@@ -0,0 +1,28 @@
// include/strata/core/arch_defaults.hpp - the per-architecture defaults of switches that are exact everywhere.
//
// Every gfx1151 (Strix Halo, RDNA3.5, unified memory) switch in the table was measured on Aurora (experiments/s23 .. s26,
// the c-best3-v4.2 config) with BYTE-IDENTICAL output against the same engine with the switch off: the same token ids at
// 4K / 32K / 64K and on four models, and a bitwise harness per kernel. They are therefore on by default on that
// architecture and nowhere else: a CUDA build and every other HIP architecture run the code they always ran.
// Switches that change bits (STRATA_PF_GEMM, STRATA_PF_FUSED, STRATA_HC_UPMIX, STRATA_PA_FAST, STRATA_HIP_WMMA,
// STRATA_SELECT_WMMA, STRATA_HC_Q8: rounding-level, KL-gated) stay opt-in; docs/STRIX_HALO.md lists them.
//
// A switch the user set (to anything) is never overridden; STRATA_GFX1151_DEFAULTS=0 turns the whole table off.
#pragma once
#include <string>
#include <utility>
#include <vector>
namespace strata::core {
/// The environment settings that are the defaults of `gcn_arch` (hipDeviceProp_t::gcnArchName; "gfx1151" or
/// "gfx1151:sramecc-:xnack-"): empty for every other architecture.
std::vector<std::pair<std::string, std::string>> arch_default_env(const char* gcn_arch);
/// Sets `arch_default_env(gcn_arch)` in this process's environment where the user has not set the variable, prints one
/// line to stderr when it set anything, and returns the names it set. Call once, before any engine code reads its
/// switches (they are read lazily, at first use).
std::vector<std::string> apply_arch_defaults(const char* gcn_arch);
} // namespace strata::core
+91 -3
View File
@@ -53,13 +53,94 @@ struct ConversationKvReuse {
std::vector<ConversationKv> kv;
// Original image extent for validation, and the earliest subsequent rewrite.
int64_t captured_tokens = 0, unchanged_tokens = 0;
// With a layer split: the later stages' own retained K/V, one per stage, same extents (empty: none)
std::vector<ConversationKvReuse> stages;
size_t bytes() const {
size_t n = kv.capacity() * sizeof(ConversationKv);
size_t n = kv.capacity() * sizeof(ConversationKv) + stages.capacity() * sizeof(ConversationKvReuse);
for (const auto& layer : kv) n += layer.bytes();
for (const auto& s : stages) n += s.bytes();
return n;
}
};
/// Layer-split parking: each running checkpoint holds the first stage's state and, in `stage_parts`, one part per
/// later stage; a parked conversation keeps them in one image per stage. `split` MOVES the checkpoints apart (no
/// running state is copied): the complete ones (a part for each of `stages` stages) into `stage0` and `parts[k]`,
/// every part carrying the checkpoint's ids / images / LRU stamp, the others aside; `merge` puts them back in their
/// original order. merge(split(x)) == x.
struct ConversationCheckpointSplit {
std::vector<ConversationCheckpoint> stage0;
std::vector<std::vector<ConversationCheckpoint>> parts; ///< [stage][checkpoint]
std::vector<ConversationCheckpoint> rest; ///< not split (no complete set of stage parts)
std::vector<bool> complete; ///< per original position: in stage0/parts or in rest
};
inline ConversationCheckpointSplit conversation_checkpoints_split(std::vector<ConversationCheckpoint>&& checks,
size_t stages) {
ConversationCheckpointSplit out;
out.parts.resize(stages);
out.complete.reserve(checks.size());
size_t whole = 0;
for (const auto& c : checks) {
const bool complete = c.stage_parts.size() == stages;
out.complete.push_back(complete);
whole += complete ? 1 : 0;
}
out.stage0.reserve(whole);
out.rest.reserve(checks.size() - whole);
for (auto& parts : out.parts) parts.reserve(whole);
// Allocate and copy identity metadata before moving any running state.
// If allocation fails, every input checkpoint remains usable by the caller.
for (const auto& c : checks) {
if (c.stage_parts.size() != stages) continue;
for (size_t k = 0; k < stages; ++k) {
ConversationCheckpoint part;
part.ids = c.ids; part.imgs = c.imgs; part.used = c.used;
out.parts[k].push_back(std::move(part));
}
}
size_t w = 0;
for (auto& c : checks) {
if (c.stage_parts.size() != stages) { out.rest.push_back(std::move(c)); continue; }
for (size_t k = 0; k < stages; ++k) {
auto& dst = out.parts[k][w];
auto& src = c.stage_parts[k];
dst.gdn = std::move(src.gdn); dst.ple = std::move(src.ple);
dst.tails = std::move(src.tails); dst.dead = std::move(src.dead);
dst.block_pos = std::move(src.block_pos);
}
c.stage_parts.clear();
out.stage0.push_back(std::move(c));
++w;
}
checks.clear();
return out;
}
/// false (and `out` empty) when the parts do not line up: a stage with another number of checkpoints, or a part
/// whose ids differ from its checkpoint's.
inline bool conversation_checkpoints_merge(ConversationCheckpointSplit&& split, std::vector<ConversationCheckpoint>& out) {
out.clear();
for (const auto& p : split.parts)
if (p.size() != split.stage0.size()) return false;
size_t whole = 0, other = 0;
for (bool c : split.complete) (c ? whole : other) += 1;
if (!split.complete.empty() && (whole != split.stage0.size() || other != split.rest.size())) return false;
if (split.complete.empty()) split.complete.assign(split.stage0.size(), true); // a restored image: all whole
for (size_t j = 0; j < split.stage0.size(); ++j)
for (const auto& p : split.parts)
if (p[j].ids != split.stage0[j].ids) return false;
size_t w = 0, r = 0;
for (bool c : split.complete) {
if (!c) { out.push_back(std::move(split.rest[r++])); continue; }
ConversationCheckpoint cp = std::move(split.stage0[w]);
for (auto& p : split.parts) cp.stage_parts.push_back(std::move(p[w]));
++w;
out.push_back(std::move(cp));
}
return true;
}
struct SavedConversation {
// Runtime compatibility only; NOT a model/weights identity or disk schema.
std::array<int64_t, 18> geometry{};
@@ -69,10 +150,13 @@ struct SavedConversation {
std::vector<ConversationCheckpoint> checkpoints;
std::vector<ConversationKv> kv; // main layers followed by the draft layer
bool cvec = true;
// with a layer split, the later stages' own images, one per stage, in stage order
std::vector<SavedConversation> stage_images;
size_t bytes() const {
size_t n = live.bytes() + checkpoints.capacity() * sizeof(ConversationCheckpoint) +
kv.capacity() * sizeof(ConversationKv);
for (const auto& s : stage_images) n += s.bytes();
for (const auto& c : checkpoints) n += c.bytes();
for (const auto& k : kv) n += k.bytes();
return n;
@@ -110,13 +194,17 @@ public:
// Retain only the restored K/V buffers, not duplicate running checkpoints.
// This optimization never evicts a parked conversation to make itself fit.
void retain(std::vector<ConversationKv>&& kv, int64_t tokens) {
// `stage_kv`: with a layer split, the later stages' restored K/V (one per stage), retained with the first's.
void retain(std::vector<ConversationKv>&& kv, int64_t tokens,
std::vector<std::vector<ConversationKv>>&& stage_kv = {}) {
reuse_ = {};
ConversationKvReuse candidate{std::move(kv), tokens, tokens};
ConversationKvReuse candidate{std::move(kv), tokens, tokens, {}};
for (auto& k : stage_kv) candidate.stages.push_back(ConversationKvReuse{std::move(k), tokens, tokens, {}});
if (enabled() && candidate.bytes() <= budget_ - bytes_) reuse_ = std::move(candidate);
}
void limit_reuse(int64_t first_dirty) {
reuse_.unchanged_tokens = std::min(reuse_.unchanged_tokens, first_dirty);
for (auto& s : reuse_.stages) s.unchanged_tokens = std::min(s.unchanged_tokens, first_dirty);
if (reuse_.unchanged_tokens <= 0) reuse_ = {};
}
ConversationKvReuse take_reuse() { return std::exchange(reuse_, {}); }
+243
View File
@@ -0,0 +1,243 @@
// On-disk persistence of a conversation's state (SavedConversation): one file holding the running state, the
// checkpoints the caller passes (the engine passes only the deepest one), every QSA layer's authoritative K/V and
// the draft layer's K/V. Pure host code: no CUDA. Format v1 and the fail-closed read order are in
// docs/DETAILS.md (Session files).
//
// Format v1 is little-endian, with fixed-width integers and IEEE-754 floats; a big-endian build is refused at
// compile time. The hashes detect accidental corruption; they do not authenticate a file: restore trusted files only.
#pragma once
#include "strata/core/conversation_cache.hpp"
#include <array>
#include <cstddef>
#include <cstdint>
#include <functional>
#include <optional>
#include <string>
#include <utility>
#include <vector>
namespace strata::core {
// What the file is bound to: the model files it was computed with, and the engine settings that change what the
// saved bytes mean. A read with any other identity is refused before the payload is parsed.
struct SessionFileIdentity {
uint64_t model = 0, config = 0;
};
// Non-cryptographic 64-bit hash with xxHash64-style rounds (NOT the standard XXH64 stream): corruption and identity
// checks, not adversarial input. Streaming and one-shot agree.
class SessionHasher {
public:
explicit SessionHasher(uint64_t seed = 0);
void update(const void* data, size_t n);
uint64_t digest() const;
private:
void block(const uint8_t* p);
uint64_t lane_[4];
uint8_t pending_[32];
size_t npending_ = 0;
uint64_t total_ = 0;
};
uint64_t session_hash64(const void* data, size_t n, uint64_t seed);
// Typed, length-delimited fields for an identity hash: each field is (tag, type, length, bytes), so two different
// field lists never give the same byte stream, and a double is hashed by its exact bits (no decimal rounding).
class SessionIdentityBuilder {
public:
explicit SessionIdentityBuilder(uint64_t seed) : hash_(seed) {}
void str(const std::string& tag, const std::string& value);
void i64(const std::string& tag, int64_t value);
void u64(const std::string& tag, uint64_t value);
void f64(const std::string& tag, double value);
void bytes(const std::string& tag, const void* data, size_t n);
uint64_t digest() const { return hash_.digest(); }
private:
void field(const std::string& tag, uint8_t type, const void* data, uint64_t n);
SessionHasher hash_;
};
// The RESOLVED rope configuration (rope_scaling.hpp, after the CLI flags and the model file's keys met). The cached
// K is post-RoPE, so a different rotation makes the saved K mean something else.
struct SessionRope {
int64_t type = 0;
double freq_base = 0, factor = 0, freq_scale = 0, orig_ctx = 0, ext_factor = 0, attn_factor = 0, beta_fast = 0,
beta_slow = 0;
};
// Every engine setting that changes what the saved state means. Sampling, seeds, draft tuning, the expert tier and
// the prompt-reading chunk are not here: they change what is computed next, not what the saved cells hold.
struct SessionConfig {
std::string engine_version; // a file is bound to one engine version
std::string backend; // "cuda" or "hip": no cross-backend restore
std::string kv; // --kv
int64_t max_context = 0, kv_resident = 0, mtp_window = 0;
bool kv_rot = false; // STRATA_KV_ROT
SessionRope rope;
// the control vectors as loaded: a digest of the tables the engine uploaded (every file's content x its exact
// scale, summed), the mode, the layer range and the direction; 0 when none is loaded
uint64_t cvec = 0;
// the arithmetic switches that change the computed state (name, value), in a fixed order
std::vector<std::pair<std::string, int64_t>> switches;
};
uint64_t session_config_fingerprint(const SessionConfig& config);
// One model input the engine loaded and the role it plays ("native shard 1", "expert blk.3.ffn_up", ...). The role,
// not the path, enters the fingerprint, so a moved model folder still matches. Every (role, file) pair counts: one
// file in two roles is two entries (its bytes are read once). `optional`: a missing file is recorded as absent
// instead of failing (a pack without experts.bin).
struct SessionModelFile {
std::string role, path;
bool optional = false;
};
// Sampled fingerprint of the model inputs: role, size, first and last MiB of each entry, in the given order (a file
// listed under several roles is read once). Paths are UTF-8 (wide APIs on Windows; invalid UTF-8 is refused). An
// edit in the middle of a file that keeps its size is NOT detected: model files must not change while sessions saved
// with them are kept. `beat` (optional) is called after each distinct file is read.
bool session_model_fingerprint(const std::vector<SessionModelFile>& files, uint64_t& fingerprint, std::string& error,
const std::function<void()>& beat = {});
// What the engine loaded, as its loaders resolved it (generate.cpp fills it). `experts`: (role, file) of the expert
// source - the pack's experts.bin, or every expert tensor read from a GGUF in place, per layer and role, as
// native_experts.txt resolved it (ExpertSource::model_inputs).
struct SessionInputs {
std::vector<std::string> native_shards, native_dense, native_head;
std::string embedding, ple, pack, mtp;
std::vector<std::pair<std::string, std::string>> experts;
};
// The fingerprint's list: every role with its file, none merged (a path in two roles is two entries).
std::vector<SessionModelFile> session_model_inputs(const SessionInputs& inputs);
// Why a session file operation failed, for the caller's answer (the server maps it to an HTTP status):
// invalid refused: not a session file, corrupt, another model or configuration, over this engine's limits, a
// link, nothing to save (400; nothing changed)
// storage no room on the disk: the free-space preflight, or ENOSPC / EDQUOT / EFBIG (507)
// memory the RAM preflight refused it, or an allocation failed (503)
// io any other failure: permissions, read/write/flush/rename errors, a missing folder (500)
enum class SessionError { none, invalid, storage, memory, io };
const char* session_error_name(SessionError e);
struct SessionStatus {
SessionError error = SessionError::none;
// write: the new file has replaced `path` (the rename happened). Together with a failure: the file's bytes are
// complete and flushed, but the folder flush after the rename failed, so the new name may not survive a power loss
bool published = false;
// write (POSIX): this filesystem cannot flush a folder (fsync EINVAL); the save succeeded without that flush
bool dir_flush_unsupported = false;
};
// Progress of a file transfer: (bytes done, bytes in the file). A write reports once when its temporary file is
// created (0) and after every block it hands to the OS (16 MiB, or less for the last one: a small file reports too);
// a read reports after every block it reads (16 MiB, or what is left; the first one, which holds the header, too).
// Each report follows a completed transfer of at most 16 MiB: no step between two reports moves more.
using SessionProgress = std::function<void(uint64_t done, uint64_t total)>;
// A step that does not stream - one call that blocks until it is done, over far more than 16 MiB or on the OS's
// schedule (flushing the file, the rename and folder flush) - is announced before it starts, with the bytes it
// concerns. The engine turns this into an explicit, bounded allowance (session_phase_limit_s) for its watchdog and
// for the server; when the allowance runs out the step is stuck.
using SessionPhase = std::function<void(const char* phase, uint64_t bytes)>;
// The longest a non-streaming step over `bytes` may take before both sides call it stuck: 60 s plus one second per
// 4 MiB (a slow disk or network folder at 4 MiB/s), at most an hour. 1.2 GB: 345 s.
int64_t session_phase_limit_s(uint64_t bytes);
// Optional knobs of a write.
struct SessionWriteOptions {
// the free-space PREFLIGHT: refused when the disk has less than the new file plus this many bytes free before the
// write starts. Not a reservation: other writers can take the space afterwards.
uint64_t min_free_bytes = 0;
bool durable = true; // flush the file before the rename and (POSIX) the folder after it
SessionProgress progress;
// "flush" (the file's flush, `bytes` = the file) and "publish" (rename and folder flush), before each starts
SessionPhase phase;
// tests: return an errno value to make a step fail ("write", "file_flush", "rename", "dir_flush"), 0 to go on
std::function<int(const char* step)> fault;
};
// Writes a temporary file beside `path` (a unique hidden name, created exclusively: never an existing file or link;
// mode 0600 on POSIX), flushes it, renames it over `path` and, on POSIX, flushes the folder (Windows has no folder
// flush: the rename is MoveFileExW with MOVEFILE_WRITE_THROUGH). A failure BEFORE the rename keeps an existing file
// at `path` and removes only this call's own temporary file. A failure of the folder flush AFTER the rename returns
// false with `status->published`: the new file is at `path` (the old one is gone), its directory entry may not
// survive a power loss. A filesystem that cannot flush a folder (EINVAL) is not a failure; `status` says so.
bool session_file_write(const std::string& path, const SavedConversation& image, const SessionFileIdentity& id,
size_t& bytes, std::string& error, const SessionWriteOptions& options = {},
SessionStatus* status = nullptr);
// One K/V layer read straight from where it lives (device or pinned host pool) into the file's staging buffer,
// without a full host copy first. `read(part, offset, dst, n)` copies bytes [offset, offset+n) of part
// 0..4 = k, v, k_scale, v_scale, pooled; after a failed read `error()` (optional) says why. The file bytes are
// identical to writing a captured ConversationKv.
struct SessionKvSource {
int format = 0;
int64_t cells = 0, heads = 0, head_dim = 0, page_size = 0, pooled_rows = 0, idx_dim = 0;
std::array<size_t, 5> sizes{};
std::function<bool(size_t part, size_t offset, void* dst, size_t n)> read;
std::function<std::string()> error;
};
// `meta` carries everything but the K/V (meta.kv must be empty); `kv` supplies the layers in order.
bool session_file_write(const std::string& path, const SavedConversation& meta, const std::vector<SessionKvSource>& kv,
const SessionFileIdentity& id, size_t& bytes, std::string& error,
const SessionWriteOptions& options = {}, SessionStatus* status = nullptr);
// The checkpoints worth a disk write: the deepest one (the next turn's resume point when the live tail was
// rewritten). Earlier checkpoints only serve edits further back and cost ~118 MB each at this geometry.
std::vector<ConversationCheckpoint> session_checkpoints_to_save(const std::vector<ConversationCheckpoint>& chain);
// The same choice without a copy (nullptr for an empty chain).
const ConversationCheckpoint* session_deepest_checkpoint(const std::vector<ConversationCheckpoint>& chain);
// What a SAVE copies into RAM besides what the engine already holds, for its RAM preflight.
struct SessionSaveLive {
uint64_t state_bytes = 0; // the live running state (gdn + ple + every owned QSA layer's tail, dead, block_pos)
uint64_t tokens = 0, images = 0;
uint64_t kv_layers = 0; // K/V sources (owned QSA layers + the draft)
};
// The peak, with saturating arithmetic: two copies of the deepest checkpoint (the list handed to the capture and the
// one inside the captured image), the copied live state with its token and image lists, the K/V source directory,
// the 16 MiB write buffer and 1 MiB for the small vectors.
uint64_t session_save_peak_bytes(const ConversationCheckpoint* deepest, const SessionSaveLive& live);
// The SAVE's RAM PREFLIGHT, BEFORE anything is copied: `admit` is asked for session_save_peak_bytes; only when it
// agrees is the deepest checkpoint copied into `out`. On a refusal nothing was allocated, `out` is empty, `chain`
// and the live state are untouched and no file was opened. Not a reservation.
bool session_save_checkpoints(const std::vector<ConversationCheckpoint>& chain, const SessionSaveLive& live,
const std::function<bool(uint64_t need_bytes, std::string& why)>& admit,
std::vector<ConversationCheckpoint>& out, std::string& why);
// What a read checks before it allocates. Every count is checked against the bytes left in the payload and against
// these limits BEFORE its array is allocated; the defaults accept whatever the payload can hold. The engine sets all
// of them from its runtime (conversation_session_read_limits), and then the file size is bounded too.
struct SessionReadLimits {
uint64_t max_file_bytes = UINT64_MAX;
uint64_t max_tokens = UINT64_MAX; // token and image counts of the live state and of each checkpoint
uint64_t max_checkpoints = UINT64_MAX;
uint64_t max_kv_layers = UINT64_MAX;
// the runtime's geometry and layer range: a file with others is refused before its state arrays are read
std::optional<std::array<int64_t, 18>> geometry;
std::optional<std::pair<int64_t, int64_t>> layer_range;
// the most bytes of each running-state array (gdn, ple, tails, dead, block_pos), live and per checkpoint
std::array<uint64_t, 5> max_state_bytes{UINT64_MAX, UINT64_MAX, UINT64_MAX, UINT64_MAX, UINT64_MAX};
// per K/V layer (the QSA layers, then the draft): the most bytes of each part (k, v, k_scale, v_scale, pooled).
// A layer past the end of the list is bounded by the payload only.
std::vector<std::array<uint64_t, 5>> max_kv_bytes;
// the RAM PREFLIGHT: asked once the header and identity match and before any payload allocation, with what the
// parse holds at its peak (session_read_peak_bytes); false refuses the file. Not a reservation.
std::function<bool(uint64_t need_bytes, std::string& why)> admit;
SessionProgress progress;
};
// The largest file these limits admit (UINT64_MAX when one of them is open); session_file_read applies it.
uint64_t session_read_max_file_bytes(const SessionReadLimits& limits);
// What the RAM preflight is asked for a file of `file_bytes`: the parsed image (about the payload), the 16 MiB read
// buffer, and the per-allocation overhead of its arrays (a page per 16 MiB segment plus the small vectors).
uint64_t session_read_peak_bytes(uint64_t file_bytes);
// Full validation (a regular file with one link, size, header, identity, bounded parse, payload hash) before `image`
// is replaced. The file is opened without following a symbolic link (or a reparse point) at its name, and the checks
// are made on the opened handle. On failure `image` is unchanged and `status->error` says what kind of failure.
bool session_file_read(const std::string& path, const SessionFileIdentity& id, SavedConversation& image,
size_t& bytes, std::string& error, const SessionReadLimits& limits = {},
SessionStatus* status = nullptr);
// The folder the free-space preflight asks about for `path`, by the rules of Windows or of POSIX (exposed so both can
// be tested anywhere). Windows: backslashes and a trailing backslash, as GetDiskFreeSpaceExW requires for a UNC
// folder (\\server\share\dir\) and gives a drive root (C:\); empty for a bare file name (the current folder).
std::string session_free_space_dir(const std::string& path, bool windows_rules);
} // namespace strata::core
@@ -1,6 +1,7 @@
#pragma once
#include "strata/core/conversation_cache.hpp"
#include "strata/core/conversation_file.hpp"
#include "strata/core/layer.hpp"
#include "strata/core/session.hpp"
@@ -29,6 +30,19 @@ bool conversation_kv_restore(const ConversationKv& image, const QsaState& state,
// fingerprints the authoritative payload only. Never changes model state.
bool conversation_kv_verify(const ConversationKv& image, const QsaState& state, const ModelGeometry& g,
int64_t upto, bool include_index, uint64_t& fingerprint, std::string& error);
// Disk save without a host copy: the layer's authoritative K/V as a streamed source (same bytes and metadata as
// conversation_kv_save). Caller synchronizes the device first and keeps the state untouched while it is read.
bool conversation_kv_source(SessionKvSource& source, const QsaState& state, const ModelGeometry& g,
int64_t upto, bool include_index, std::string& error);
// The bytes of each K/V part (k, v, k_scale, v_scale, pooled) a snapshot of `upto` tokens holds: no state read.
bool conversation_kv_part_sizes(const QsaState& state, const ModelGeometry& g, int64_t upto, bool include_index,
std::array<uint64_t, 5>& sizes, std::string& error);
// Disk sessions: the read limits this session can ever restore - its geometry and layer range, at most
// min(max_tokens, its cells) tokens, `max_checkpoints` checkpoints, the exact running-state sizes and the K/V part
// sizes at that many tokens - so session_file_read refuses an oversized or foreign file before it allocates.
bool conversation_session_read_limits(SessionReadLimits& limits, const SessionState& session, const ModelGeometry& g,
const QsaState& draft, uint64_t max_tokens, uint64_t max_checkpoints,
std::string& error);
struct ConversationStateSizes {
size_t gdn = 0, ple = 0, tail = 0, dead = 0, block_pos = 0;
@@ -67,6 +81,12 @@ bool conversation_snapshot_save(SavedConversation& image, const ConversationView
const SessionState& session, const ModelGeometry& g,
const QsaState& draft, std::string& error,
ConversationKvReuse reuse = {}, size_t* reused_bytes = nullptr);
// Disk save without capturing the K/V on the host: `meta` gets everything but the K/V (running state copied,
// checkpoints as given by the view), `sources` one streamed source per QSA layer then the draft. Caller has
// synchronized and must not run the session until the file is written.
bool conversation_snapshot_sources(SavedConversation& meta, std::vector<SessionKvSource>& sources,
const ConversationView& view, const SessionState& session,
const ModelGeometry& g, const QsaState& draft, std::string& error);
bool conversation_snapshot_validate(const SavedConversation& image, const SessionState& session,
const ModelGeometry& g, const QsaState& draft, std::string& error);
enum class ConversationRestore { restored, invalid, transfer_failed };
@@ -76,4 +96,21 @@ ConversationRestore conversation_snapshot_restore(const SavedConversation& image
const ModelGeometry& g, const QsaState& draft,
std::string& error);
// The same with `draft == nullptr`: an image WITHOUT the draft layer's K/V (kv holds the session's own QSA layers
// only). A layer split's later stages park this way; the draft ring is saved once, with the first stage's image.
// An image is validated and restored with the same kind of call it was saved with (a K/V layer count mismatch is
// rejected as invalid).
bool conversation_snapshot_bytes(const ConversationView& view, const SessionState& session, const ModelGeometry& g,
const QsaState* draft, size_t& bytes, std::string& error);
bool conversation_snapshot_capture_bytes(const ConversationKvReuse& reuse, const ConversationView& view,
const SessionState& session, const ModelGeometry& g, const QsaState* draft,
size_t& bytes, std::string& error);
bool conversation_snapshot_save(SavedConversation& image, const ConversationView& view, const SessionState& session,
const ModelGeometry& g, const QsaState* draft, std::string& error,
ConversationKvReuse reuse = {}, size_t* reused_bytes = nullptr);
bool conversation_snapshot_validate(const SavedConversation& image, const SessionState& session,
const ModelGeometry& g, const QsaState* draft, std::string& error);
ConversationRestore conversation_snapshot_restore(const SavedConversation& image, SessionState& session,
const ModelGeometry& g, const QsaState* draft, std::string& error);
} // namespace strata::core
+125
View File
@@ -0,0 +1,125 @@
#pragma once
// dma_batch.hpp - independent host-to-device blob copies as one submission: copy_blobs.
//
// The batched path is midhatn's (PR #807, MIT-compatible contribution): cudaMemcpyBatchAsync for independent
// pinned expert uploads on CUDA 13+, with the Windows alias lookup it found necessary. eddoursul's fork (F12) uses
// the same call for the adaptive tier's and the prefetch's copies: under WDDM a copy submitted alone raises an
// interrupt when it lands, on the processor its GPU's interrupts go to; a batch raises one.
//
// Modes (STRATA_DMA_BATCH, read once; `dma_batch_mode()`):
// 0 one cudaMemcpyAsync per blob: exactly the loop the engine always ran
// 1 cudaMemcpyBatchAsync (CUDA 13+, groups of 2..128), the copies in any order, the batch in the stream's
// 2 as 1, plus CUDA 13.4's cudaMemcpyFlagPreferOverlapWithCompute (a hint)
// Every other build (HIP, CUDA before 13) and every single copy or group over 128 takes the loop. A batch the
// runtime refuses is redone as the loop (with one warning), never dropped: readiness is signalled only for
// uploads that were submitted.
#include <cuda_runtime.h>
#include <array>
#include <cstddef>
#include <cstdint>
#include <cstdio>
#include <cstdlib>
#include <mutex>
#include <unordered_map>
namespace strata::core {
/// The mode: STRATA_DMA_BATCH (0, 1, 2); unset = the engine's default (`default_mode`, set at the call site's first
/// use; 0 until a measured win makes it 1).
inline int dma_batch_mode(int default_mode = 0) {
static const int mode = [default_mode] {
const char* e = std::getenv("STRATA_DMA_BATCH");
return e != nullptr && e[0] != '\0' ? std::atoi(e) : default_mode;
}();
return mode;
}
namespace detail {
// A registered Windows host pointer may differ from its device-visible alias; the batch API needs the alias
// (cudaMemcpyAsync(kind=HostToDevice) resolves it itself). The experts' blobs are fixed, so each is looked up once.
inline cudaError_t host_alias(const void* p, const void** out) {
static std::mutex mu;
static std::unordered_map<const void*, const void*> cache;
{
std::lock_guard<std::mutex> lk(mu);
const auto it = cache.find(p);
if (it != cache.end()) { *out = it->second; return cudaSuccess; }
}
void* alias = nullptr;
const cudaError_t e = cudaHostGetDevicePointer(&alias, const_cast<void*>(p), 0);
if (e != cudaSuccess) return e;
std::lock_guard<std::mutex> lk(mu);
cache.emplace(p, alias);
*out = alias;
return cudaSuccess;
}
} // namespace detail
/// `n` independent copies, dst[i] <- src[i] (bytes[i] each), on `stream`. The sources are immutable pinned host
/// blobs and the destinations disjoint; the caller keeps them alive until the stream completes. Ordering with
/// earlier and later work on the stream is that of the copies one by one. Returns the first error (the loop
/// continues past none: the first failing copy ends it).
inline cudaError_t copy_blobs(void* const* dst, const void* const* src, const size_t* bytes, size_t n,
cudaStream_t stream, int batch_mode) {
if (n == 0) return cudaSuccess;
#if !defined(STRATA_USE_HIP) && !defined(STRATA_HIP_GFX906) && defined(CUDART_VERSION) && CUDART_VERSION >= 13000
constexpr size_t cap = 128;
if (batch_mode != 0 && n > 1 && n <= cap) {
std::array<const void*, cap> srcs{};
bool aliased = true;
for (size_t i = 0; i < n; ++i)
if (detail::host_alias(src[i], &srcs[i]) != cudaSuccess) { aliased = false; break; }
if (aliased) {
cudaMemcpyAttributes attr{};
attr.srcAccessOrder = cudaMemcpySrcAccessOrderStream;
#if CUDART_VERSION >= 13040
if (batch_mode == 2) attr.flags = cudaMemcpyFlagPreferOverlapWithCompute;
#endif
size_t first = 0;
std::array<void*, cap> dsts{};
std::array<size_t, cap> sizes{};
for (size_t i = 0; i < n; ++i) { dsts[i] = dst[i]; sizes[i] = bytes[i]; }
const cudaError_t e = cudaMemcpyBatchAsync(dsts.data(), srcs.data(), sizes.data(), n, &attr, &first, 1, stream);
if (e == cudaSuccess) return cudaSuccess;
static bool warned = false;
if (!warned) {
warned = true;
std::fprintf(stderr, "strata: cudaMemcpyBatchAsync failed (%s); the uploads go one copy at a time\n",
cudaGetErrorString(e));
}
(void) cudaGetLastError();
}
}
#else
(void) batch_mode;
#endif
for (size_t i = 0; i < n; ++i) {
const cudaError_t e = cudaMemcpyAsync(dst[i], src[i], bytes[i], cudaMemcpyHostToDevice, stream);
if (e != cudaSuccess) return e;
}
return cudaSuccess;
}
/// The verifier's form: `n` blobs of `bytes` each into consecutive slots of `dst`.
inline cudaError_t copy_expert_blobs(uint8_t* dst, const uint8_t* const* src, int n, size_t bytes,
cudaStream_t stream, int batch_mode) {
if (n <= 0) return cudaSuccess;
constexpr size_t cap = 128;
if (batch_mode == 0 || (size_t) n > cap) { // the old loop, with no staging of its own
for (int i = 0; i < n; ++i) {
const cudaError_t e = cudaMemcpyAsync(dst + (size_t) i * bytes, src[i], bytes, cudaMemcpyHostToDevice, stream);
if (e != cudaSuccess) return e;
}
return cudaSuccess;
}
void* d[cap] = {}; // plain arrays: nvcc on Linux (13.0, g++) rejects std::array<T, cap> here ("template argument 2 is invalid")
const void* s[cap] = {};
size_t b[cap] = {};
for (int i = 0; i < n; ++i) { d[i] = dst + (size_t) i * bytes; s[i] = src[i]; b[i] = bytes; }
return copy_blobs(d, s, b, (size_t) n, stream, batch_mode);
}
} // namespace strata::core
+114
View File
@@ -0,0 +1,114 @@
#pragma once
#include <atomic>
#include <cstddef>
#include <memory>
#include <utility>
#include <cstdint>
#include <limits>
#include <string>
#include <vector>
namespace strata::core::detail {
// Non-owning views: the complement and exchange allocations retain their original
// owners until FileExpertSource::close(). Ownership changes via slot IDs; the
// physical buffer addresses, bytes and CUDA aliases stay in place. This first path requires uniform, fully mapped/pinned expert slots.
class ExchangeStorage {
public:
struct View {
uint8_t* host = nullptr;
const uint8_t* device = nullptr;
};
bool initialize(const std::vector<uint64_t>& offsets, uint8_t* resident_host,
const uint8_t* resident_device, uint64_t resident_bytes,
uint8_t* exchange_host, const uint8_t* exchange_device,
size_t count, size_t blob_bytes, std::string& error) {
error.clear();
if (active()) { error = "exchange storage already initialized"; return false; }
if (!resident_host || !resident_device || !exchange_host || !exchange_device ||
!blob_bytes || !count || !resident_bytes || resident_bytes % blob_bytes ||
count > std::numeric_limits<size_t>::max() / blob_bytes) {
error = "invalid uniform mapped exchange geometry"; return false;
}
const size_t resident_slots = (size_t)(resident_bytes / blob_bytes);
if (resident_slots > std::numeric_limits<size_t>::max() - count) {
error = "exchange slot count overflow"; return false;
}
auto experts = std::make_unique<std::atomic<size_t>[]>(offsets.size());
std::vector<size_t> spares(count);
std::vector<View> buffers(resident_slots + count);
for (size_t slot = 0; slot < resident_slots; ++slot)
buffers[slot] = {resident_host + slot * blob_bytes, resident_device + slot * blob_bytes};
std::vector<bool> used(resident_slots, false);
for (size_t i = 0; i < offsets.size(); ++i) {
const uint64_t at = offsets[i];
experts[i].store(kAbsent, std::memory_order_relaxed);
if (at == ~uint64_t{0}) continue;
if (at >= resident_bytes || at % blob_bytes || used[(size_t)(at / blob_bytes)]) {
error = "invalid or duplicate resident slot"; return false;
}
used[(size_t)(at / blob_bytes)] = true;
experts[i].store((size_t)(at / blob_bytes), std::memory_order_relaxed);
}
for (bool present : used) if (!present) {
error = "unassigned resident slot"; return false;
}
for (size_t q = 0; q < count; ++q) {
spares[q] = resident_slots + q;
buffers[spares[q]] = {exchange_host + q * blob_bytes, exchange_device + q * blob_bytes};
}
experts_ = std::move(experts);
expert_count_ = offsets.size();
spares_.swap(spares);
buffers_.swap(buffers);
blob_bytes_ = blob_bytes;
return true;
}
bool active() const { return blob_bytes_ != 0; }
View resident(size_t expert) const {
if (expert >= expert_count_) return {};
const size_t slot = experts_[expert].load(std::memory_order_acquire);
return slot != kAbsent ? buffers_[slot] : View{};
}
View spare(size_t q) const { return q < spares_.size() ? buffers_[spares_[q]] : View{}; }
// Only after every CPU reader and H2D copy of `in` has completed, and `out`
// has landed in spare(q). No allocation, memcpy, or CUDA operation here.
bool commit(size_t in, size_t out, size_t q, const uint8_t* staged, size_t bytes) {
if (in >= expert_count_ || out >= expert_count_ || q >= spares_.size() ||
!staged || bytes != blob_bytes_) return false;
const size_t incoming_slot = experts_[in].load(std::memory_order_acquire);
if (incoming_slot == kAbsent || experts_[out].load(std::memory_order_acquire) != kAbsent ||
buffers_[spares_[q]].host != staged) return false;
// A background router lookahead can query residency to skip redundant
// file prefetches. Publish one atomic slot ID; the pointer/alias pair is
// immutable. Actual compute readers still obey the completion contract.
experts_[out].store(spares_[q], std::memory_order_release);
spares_[q] = incoming_slot;
experts_[in].store(kAbsent, std::memory_order_release);
++exchanges_;
avoided_bytes_ += bytes;
return true;
}
uint64_t exchanges() const { return exchanges_; }
uint64_t avoided_bytes() const { return avoided_bytes_; } // memcpy payload, not read+write total
void clear() {
experts_.reset(); expert_count_ = 0; buffers_.clear(); spares_.clear();
blob_bytes_ = 0; exchanges_ = avoided_bytes_ = 0;
}
private:
static constexpr size_t kAbsent = std::numeric_limits<size_t>::max();
std::unique_ptr<std::atomic<size_t>[]> experts_;
size_t expert_count_ = 0;
std::vector<View> buffers_; // immutable physical addresses after initialize
std::vector<size_t> spares_; // touched only by the serialized cache manager
size_t blob_bytes_ = 0;
uint64_t exchanges_ = 0, avoided_bytes_ = 0;
};
} // namespace strata::core::detail
+72 -10
View File
@@ -16,21 +16,20 @@
// * `h_layer` held-out is **0.0456** - only 4.6% of (layer, token) pairs have all ten experts resident - so
// this cannot be a per-layer grouped kernel and the three-way split is not an optimisation.
//
// **WHAT THIS FILE IS AND IS NOT, TODAY.** It is the SLOT STORAGE and the RESIDENCY TABLE: it allocates the
// VRAM, fills it from the host arena, and answers `(layer, expert) -> slot or -1`. It does **not** yet compute
// anything - `moe_hit_grouped_s2` does not exist and the hit/miss split is not wired into the graph - so with
// the cache on and nothing consuming it, **the engine is slower by the fill cost and faster by nothing.**
// That is stated here rather than discovered from a benchmark, and it is why `--expert-cache` defaults to 0.
//
// The order of work is `Memory/R4-design-note.md` §7: slots and residency first, then the split, then the
// kernel. This is that first step, and the step it unblocks is the one that can be measured.
// **WHAT THIS FILE IS.** The SLOT STORAGE and the RESIDENCY TABLE: it allocates the VRAM, fills it from the host
// arena, and answers `(layer, expert) -> slot or -1`. It computes nothing itself: the GPU computes the experts it
// holds in the verify window's grouped kernels and the prompt path (the hit/miss split), and the CPU pool the rest.
// (Written when nothing consumed the slots yet and `--expert-cache` defaulted to 0; setup's configs use `auto`.)
#pragma once
#include <cstddef>
#include <cstdint>
#include <memory>
#include <string>
#include <vector>
#include "strata/core/vmm.hpp"
namespace strata::core {
/// Where one expert's VRAM lives, or -1. `int32_t` and not a bool because the slot index is what the kernel
@@ -73,6 +72,12 @@ std::vector<std::pair<int32_t, int32_t>> rank_learned_profile(int64_t n_layers,
bool write_expert_profile(const std::string& path, int64_t n_layers, int64_t n_expert,
const std::vector<std::pair<int32_t, int32_t>>& ranked, std::string& err);
/// Free device memory for the expert cache. On a unified memory system - one memory for the CPU and the GPU, such as
/// the DGX Spark (GB10); CUDA reports it as cudaDevAttrIntegrated - cudaMemGetInfo counts only MemFree, not the page cache - which, with the experts read from the GGUF in place, is mostly those
/// files' clean pages and gives way to a device allocation. There the figure is MemAvailable less 6 GiB for the OS
/// and the engine's host side (STRATA_UMA_HEADROOM_GIB). Elsewhere it is cudaMemGetInfo's.
size_t device_free_bytes();
class ExpertCache {
public:
ExpertCache() = default;
@@ -93,13 +98,44 @@ public:
void close();
bool valid() const { return base_ != nullptr; }
int64_t slots() const { return slots_; }
/// The slots usable now: all of them, unless `shrink` gave the tail's VRAM back (#533) - then the ones wholly
/// inside the VRAM still backed, always a prefix. The prompt path's loan (the last slots) and every report
/// follow this; `full_slots()` is what `open` allocated.
int64_t slots() const { return live_slots_; }
int64_t full_slots() const { return slots_; }
/// Slots actually claimed. Not the same as `slots()` - the cache does not evict, so a run that routes
/// fewer distinct experts than there are slots leaves the rest empty.
int64_t resident() const { return per_layer_ ? admitted_ : next_free_; }
int64_t bytes() const { return off_.empty() ? slots_ * blob_ : (int64_t) off_.back(); }
/// The bytes of `slots()` (all of the arena unless shrunk); `full_bytes()`: of every slot.
int64_t bytes() const { return slot_end(live_slots_); }
int64_t full_bytes() const { return slot_end(slots_); }
double gib() const { return (double) bytes() / 1073741824.0; }
/// **#533: HOT VRAM RESIZE, OPT-IN (--vram-elastic).** With a segment size set before `open`/`open_sized`, the
/// arena is one reserved address range backed by physical segments of that size (CUDA virtual memory
/// management) instead of one cudaMalloc, so `shrink` can give the tail's VRAM back to the driver - for another
/// program - and `grow` can take it again, while every slot keeps its address (the captured graphs, the verify
/// plan's pointers and the prompt path's loan all hold addresses into it). 0 (the default): one cudaMalloc, as
/// always. CUDA only: a HIP build refuses it at open. The slots past `slots()` after a shrink must hold no
/// resident expert and no loan: the CALLER evicts them first (they become CPU misses).
void set_segment_bytes(int64_t seg_bytes) { seg_req_ = seg_bytes > 0 ? seg_bytes : 0; }
bool segmented() const { return !segs_.empty(); }
int64_t segment_bytes() const { return seg_; }
/// Bytes of the arena backed by VRAM now (a whole number of segments; the arena when not segmented).
int64_t mapped_bytes() const;
/// Unmaps every segment past the first `keep_bytes` (rounded UP to a segment boundary): `slots()` becomes the
/// slots wholly inside what stays. Waits for the device first. False with `err` when not segmented or a driver
/// call failed.
bool shrink(int64_t keep_bytes, std::string& err);
/// Maps segments again up to `want_bytes` (rounded DOWN to a segment, at most the arena; the last, shorter
/// segment only when `want_bytes` covers the arena). Stops at the first segment the driver cannot back (false,
/// `err`: what was mapped by then stays). The new slots are empty until the caller fills them.
bool grow(int64_t want_bytes, std::string& err);
/// The number of leading slots that fit wholly inside the first `bytes` bytes.
int64_t slots_within(int64_t bytes) const;
/// The bytes the first `n` slots span.
int64_t bytes_of(int64_t n) const { return slot_end(n < 0 ? 0 : n > slots_ ? slots_ : n); }
/// `(layer, expert)` -> slot index, or `kNotResident`. Bounds-checked: a bad layer or expert returns
/// `kNotResident` rather than reading whatever is adjacent in the table.
int32_t slot_of(int64_t layer, int64_t expert) const;
@@ -108,6 +144,13 @@ public:
/// sweep) and a placeholder policy would set the hit rate that everything downstream is then sized against.
int32_t admit(int64_t layer, int64_t expert);
/// Publish a same-layer replacement after its slot copy has completed.
void replace(int64_t layer, int32_t old_expert, int32_t new_expert) {
auto& old = residency_[(size_t) layer * n_expert_ + old_expert];
residency_[(size_t) layer * n_expert_ + new_expert] = old;
old = kNotResident;
}
/// **R4.2g: GIVE EACH LAYER ITS OWN SLOTS. ROUND 328 MEASURED WHY THE GLOBAL FORM CANNOT WORK.**
///
/// `admit`'s original policy hands out slots in ARRIVAL ORDER from one counter shared by every layer
@@ -158,13 +201,32 @@ public:
/// Slots filled so far, for the startup report.
int64_t fills() const { return fills_; }
/// The elastic K/V (--kv-grow): the arena in a VMM range (vmm.hpp) instead of one cudaMalloc, so the K/V can take
/// single chunks of it and give them back. Applies to the next `open`; ignored where VMM is not available.
static void set_vmm(bool enabled);
/// The arena's range (null: one cudaMalloc).
VmmRange* vmm_range() { return vmm_.get(); }
/// Byte offset of slot `s` in the arena (s == slots(): the end).
uint64_t slot_offset(int64_t s) const { return off_.empty() ? (uint64_t) s * (uint64_t) blob_ : off_[(size_t) s]; }
private:
#if defined(STRATA_USE_HIP)
bool ensure_blocking_staging(std::size_t bytes, std::string& err);
uint8_t* blocking_staging_ = nullptr;
std::size_t blocking_staging_bytes_ = 0;
#endif
int64_t slot_end(int64_t n) const { return off_.empty() ? n * blob_ : (int64_t) off_[(size_t) n]; }
bool open_segmented(uint64_t want, std::string& err);
void release_segmented();
uint8_t* base_ = nullptr;
int64_t live_slots_ = 0; ///< #533: slots() - all of them unless shrunk
int64_t seg_req_ = 0; ///< #533: the segment size asked for (0: one cudaMalloc)
int64_t seg_ = 0; ///< #533: the segment size used (a multiple of the driver's granularity)
uint64_t reserved_ = 0; ///< #533: the reserved address range's size
std::vector<unsigned long long> segs_; ///< #533: each segment's physical handle (0: unmapped)
std::vector<int64_t> seg_size_; ///< #533: each segment's size (the last one may be shorter)
int64_t mapped_segs_ = 0; ///< #533: segments [0, mapped_segs_) are backed
std::unique_ptr<VmmRange> vmm_; ///< the arena's range when it is in VMM (set_vmm)
std::vector<int32_t> residency_; ///< [n_layers * n_expert] -> slot or kNotResident
int64_t slots_ = 0;
int64_t n_layers_ = 0;
+97 -9
View File
@@ -22,6 +22,7 @@
#pragma once
#include "strata/core/expert_cache.hpp"
#include "strata/core/exchange_storage.hpp"
#include "strata/core/hit_hook.hpp"
#include "strata/kernels/cpu/pool.hpp"
@@ -65,6 +66,27 @@ struct CgroupMemoryStat {
/// Returns false when the required memory.stat counters were unavailable.
bool cgroup_available_bytes(uint64_t limit, const CgroupMemoryStat& stat, uint64_t& bytes);
/// #633: the RAM this process can get. `available`: MemAvailable (Windows: the available physical memory), lowered
/// to the room under the tightest cgroup limit; `cgroup_limit`: that tightest limit itself (v2 memory.max of the group
/// and its ancestors, or v1 memory.limit_in_bytes), ~0 when there is none - what a container can never exceed.
struct HostMemory {
uint64_t available = 0;
uint64_t cgroup_limit = ~uint64_t{0};
uint64_t commit = ~uint64_t{0}; ///< available Windows commit capacity; ~0 when not reported
};
/// Linux reads `meminfo`, `self_cgroup` and the cgroup tree under `cgroup_root` (the parameters are for tests; the
/// defaults are the real files). cgroup v2 as before (an unreadable limit of a group that has one fails); cgroup v1's
/// memory controller (`<root>/memory/<path>`: memory.limit_in_bytes - memory.usage_in_bytes); no cgroup line at all is
/// MemAvailable alone. False when the RAM cannot be determined.
bool host_available_memory(HostMemory& m, const std::string& meminfo = "/proc/meminfo",
const std::string& self_cgroup = "/proc/self/cgroup",
const std::string& cgroup_root = "/sys/fs/cgroup");
/// Bound a resident budget by RAM and commit capacity after headroom; leave 256 MiB more when clamping.
/// Pass UINT64_MAX for commit when the platform does not report it.
uint64_t clamp_resident_budget(uint64_t requested, uint64_t physical, uint64_t commit, uint64_t headroom);
/// Build compact offsets for experts absent from both the primary GPU cache and an optional second GPU tier.
/// Kept CPU-only so selection and byte accounting can be tested without initializing a GPU.
bool make_cache_complement_plan(
@@ -122,6 +144,11 @@ public:
virtual void begin_layer(int64_t layer, const int32_t* ids, int64_t k) { (void) layer; (void) ids; (void) k; }
/// Plan v0.3 P6: the DEVICE address of a pinned, mapped blob (the GPU can read it over PCIe), or null.
virtual const uint8_t* device_alias(int64_t layer, int64_t expert) const { (void) layer; (void) expert; return nullptr; }
/// A file-backed source: start reading this expert's pages now (it will be needed by the CPU); no-op elsewhere.
virtual void prefetch(int64_t layer, int64_t expert) { (void) layer; (void) expert; }
/// A file-backed source: this expert lives in VRAM, so its pages need not stay in RAM - hand them back to the
/// kernel (a later read re-reads the file; no result depends on it). Returns the bytes released; 0 elsewhere.
virtual uint64_t release(int64_t layer, int64_t expert) { (void) layer; (void) expert; return 0; }
/// Whether the verify window may give the GPU a PCIe share of this layer's misses at all (each expert is still
/// checked with `pinned`). The arena answers per layer through its expert 0; the resident RAM mode's compact
/// copy has no expert 0 when the GPU cache holds it, so it answers for the whole copy.
@@ -132,9 +159,17 @@ public:
/// stays valid for the layer it was asked in and the next one or two; a consumer that keeps a blob longer (the
/// prompt path's stager queues a whole chunk) copies it with `copy_blob` instead.
virtual bool transient(int64_t layer, int64_t expert) const { (void) layer; (void) expert; return false; }
/// Disk sessions: the files this source read its experts from, as (role, path), resolved by the loader itself
/// - the pack's experts.bin, or every GGUF tensor native_experts.txt named, per layer and role
/// ("expert blk.L.ffn_up"). Filled by open(); empty before.
const std::vector<std::pair<std::string, std::string>>& model_inputs() const { return inputs_; }
/// The blob's bytes into `dst` (blob_bytes(layer) of them). Safe from several threads for a source whose
/// `transient` can be true.
virtual bool copy_blob(int64_t layer, int64_t expert, uint8_t* dst);
/// Asks the OS for these pairs' bytes ahead of the `blob` calls that read them. False (nothing asked) by default.
virtual bool advise_pairs(const std::pair<int32_t, int32_t>* pairs, int64_t n) const {
(void) pairs; (void) n; return false;
}
/// The `n` experts of `layer` the CPU is about to ask `blob` for, all at once: a source that reads a file may
/// fetch them in parallel. The bytes `blob` then returns are the same. Default: nothing.
virtual void prefetch(int64_t layer, const int64_t* experts, int64_t n) { (void) layer; (void) experts; (void) n; }
@@ -143,6 +178,11 @@ public:
virtual void warm(int64_t layer, const int64_t* experts, int64_t n) { (void) layer; (void) experts; (void) n; }
/// Whether `warm` does anything (the predictor is not run otherwise).
virtual bool warms() const { return false; }
protected:
/// What model_inputs() returns; set by a source's open().
void record_inputs(const std::string& pack_experts, const std::string& gguf,
const strata::kernels::cpu::ExpertLayout& lay, bool from_gguf);
std::vector<std::pair<std::string, std::string>> inputs_;
};
/// CS-T, routing-aware prefetch of the file tier: when the CPU pool starts layer `l`, a worker thread applies layer
@@ -331,6 +371,9 @@ struct ExpertDispatch {
GpuPlanSink* plan = nullptr;
int pcie_num = 0;
int64_t pcie_experts = 0; ///< distinct experts the GPU read over PCIe in verify windows
/// #588: routed (token, expert) entries the GPU computed from outside its cache in verify windows: read over PCIe
/// (--pcie-frac, kind 1) or on another GPU (kind 2). In neither cache_hits nor cache_refused.
int64_t offload_entries = 0;
double ms_plan = 0, ms_actq = 0, ms_jobs = 0, ms_run = 0; ///< verify-window dispatch sections
/// Plan v0.3 P6: decayed routing counts per (layer, expert) during decode (sized by the caller; empty = off),
/// which the driver uses to swap the most-routed missing experts into the VRAM tier between rounds.
@@ -418,15 +461,16 @@ public:
/// working set instead. `pin = false` is ordinary pageable memory (the ROCm arm: large pinned allocations
/// can fail there, and it is what the HIP measurements used).
/// - `lend_from_slot` >= 0: the GPU-cache slots from there to the end are the prompt path's lend region; their
/// experts are kept in RAM too, from the last slot down, as far as `available RAM - headroom_bytes` allows
/// experts are kept in RAM too, from the last slot down, as far as available memory minus headroom allows
/// (a lent slot's expert is streamed during the prompt and copied back after it).
/// - the rest (the experts no slot holds) must fit that budget, or nothing is allocated and this returns false.
///
/// CS-T, `budget_bytes` > 0 (`--resident-budget-gib`): only as many of those experts as fit `budget_bytes`, taken
/// in `rank` order (the expert profile: the hottest after the GPU cache's), are copied; the rest stay on the
/// mapped files (the SSD tier). No lend region then (a lent slot's expert is read from the files).
/// #467: `budget_bytes` = `kResidentWhatFits` is that path sized by the RAM alone (available minus the headroom
/// and the #403 margin) - the soft --resident-experts mode's second try when the whole complement does not fit;
/// Available memory is limited by both RAM and commit capacity on Windows.
/// #467: `budget_bytes` = `kResidentWhatFits` sizes that path from available memory minus headroom and the #403
/// margin - the soft --resident-experts mode's second try when the whole complement does not fit;
/// false when not even one expert fits. On Windows the mapped experts leave the working set before any reading.
static constexpr uint64_t kResidentWhatFits = ~0ull;
bool pin_cache_complement(
@@ -451,17 +495,34 @@ public:
// buffer and calls `stage_exchange`: `out` is then read from that buffer, and `in` still from here (the CPU
// computes both until the swap lands). Once the slot copy has landed, `commit_exchanges` moves `out` into
// `in`'s place, so the copy keeps holding exactly the experts the GPU does not - with no read of the file.
// STRATA_EXCHANGE_ROTATE=1 can instead transfer buffer ownership (uniform, fully pinned/mapped slots).
/// Whether the compact copy holds `(layer, expert)`.
bool has_resident(int64_t layer, int64_t expert) const;
/// Host room for `n` evicted blobs (page-locked when possible). Idempotent for the same or a smaller `n`.
bool reserve_exchanges(int64_t n, std::string& err);
int64_t exchange_capacity() const { return xstage_cap_; }
/// The exchange buffers are page-locked (cudaHostAlloc): copies to and from them are asynchronous.
bool exchange_pinned() const { return xstage_pinned_; }
uint8_t* exchange_buffer(int64_t q) const;
/// Requires `has_resident(layer, in)`, `!has_resident(layer, out)` and `exchange_buffer(q)` holding out's blob.
bool stage_exchange(int64_t layer, int64_t in, int64_t out, int64_t q);
/// --pipeline-windows: size the exchange table now, so a `stage_exchange` on the adaptive tier's thread never
/// reallocates it under a concurrent `blob` (the pool reads it while windows are in flight).
void prepare_overrides() { if (override_.empty()) override_.assign((size_t) blobs_, nullptr); }
/// After the GPU copies of every staged swap have landed. Returns how many exchanges were applied.
int64_t commit_exchanges();
/// `commit_exchanges` in two halves, for the asynchronous adaptive tier (--adapt-async): `commit_copies` moves
/// every staged evicted blob into its `in`'s place in the copy (on another thread: safe once nothing computes `in`
/// from RAM - it is resident on the GPU - while `out` is still read from its exchange buffer), then `commit_flip`
/// (the caller's thread, between windows) points `out` there and drops the staging. Returns how many were applied.
void commit_copies();
int64_t commit_flip();
/// The compact copy's blob of `(layer, expert)`, or null; not counted as a read (any thread).
const uint8_t* resident_blob(int64_t layer, int64_t expert) const;
int64_t exchanges() const { return exchanges_; }
bool exchange_rotation() const { return exchange_storage_.active(); }
uint64_t rotated_exchanges() const { return exchange_storage_.exchanges(); }
uint64_t avoided_exchange_copy_bytes() const { return exchange_storage_.avoided_bytes(); }
/// With the compact copy ready: blobs read from the mapped file since (what the plain mmap mode may read from
/// the SSD). 0 in a steady resident mode; lend-region experts that did not fit the RAM count here.
int64_t file_reads() const { return file_reads_.load(std::memory_order_relaxed); }
@@ -475,18 +536,31 @@ public:
double file_ms() const { return (double) file_us_.load(std::memory_order_relaxed) / 1000.0; }
/// Threads `prefetch` reads the GGUF with (STRATA_FETCH_THREADS, default 8).
void set_fetch_threads(int n) { fetch_threads_ = n < 1 ? 1 : n; }
/// #286 (Windows): read the experts straight from the drive (FILE_FLAG_NO_BUFFERING, overlapped) instead of
/// #286: read the experts straight from the drive (Windows: FILE_FLAG_NO_BUFFERING, overlapped; Linux: O_DIRECT and
/// the kernel's asynchronous reads) instead of
/// through the mapped files - the GGUF in place, or a pack's experts.bin - when the file cache could not keep them
/// beside `ram_bytes` (the RAM budget), cached now or not (their mapped pages would land in the working set); see
/// experts_unbuffered. The mapped reads' page faults are one small request each, and the pages they bring in
/// take the RAM the budget was sized for. `why` says what decided.
/// #577: `ram_bytes` counts up to every expert, and only the experts outside it are what the cache must keep.
bool set_unbuffered(uint64_t ram_bytes, std::string& why);
/// #577: the same decision once the RAM copy is built (pin_cache_complement), from the RAM it really holds and the
/// expert bytes outside it; switches either way (startup only, nothing reading). Returns whether unbuffered.
bool recheck_unbuffered(std::string& why);
bool unbuffered() const { return !direct_.empty(); }
/// Every expert's bytes (n_layers x n_expert blobs).
uint64_t expert_bytes() const;
/// #286, unbuffered: assembles the blobs of these pairs ahead of the `blob` calls that will ask for them (the
/// GPU cache's fill from the profile) - one batch of reads instead of one blob at a time. At most 64 pairs.
void prefetch_pairs(const std::pair<int32_t, int32_t>* pairs, int64_t n);
/// Mapped reads: asks the OS for these pairs' blobs ahead (platform::advise_willneed). False when unbuffered,
/// when read-ahead is off, or before `open`.
bool advise_pairs(const std::pair<int32_t, int32_t>* pairs, int64_t n) const override;
const uint8_t* blob(int64_t layer, int64_t expert) override;
/// Windows, opt-in with STRATA_FILE_RELEASE=1: trim the file mapping's full pages after their expert reaches
/// VRAM. Never touches the resident complement or staging buffers. Disabled by default.
uint64_t release(int64_t layer, int64_t expert) override;
bool pinned(int64_t layer, int64_t expert) const override;
const uint8_t* device_alias(int64_t layer, int64_t expert) const override;
bool pcie_layer(int64_t layer) const override;
@@ -504,12 +578,15 @@ public:
/// Of the blobs the file tier read for the decode, how many had been warmed for their layer beforehand.
int64_t warmed_hits() const { return warm_hits_.load(std::memory_order_relaxed); }
int64_t warmed() const { return warm_count_.load(std::memory_order_relaxed); }
/// #286: blobs an unbuffered read could not deliver, read through the mapping instead (0 when all went direct).
int64_t direct_fallbacks() const { return direct_fallbacks_.load(std::memory_order_relaxed); }
/// Blobs touched, for the driver to report. With `h = 0` this is `48 * k` per token and the number is only
/// interesting once Phase 3 makes it not so.
int64_t reads() const override { return reads_; }
private:
const uint8_t* resident_blob(size_t index) const;
const uint8_t* mapped_blob(int64_t layer, int64_t expert) const;
/// The blob's bytes from the mapped file(s) - experts.bin, or the three GGUF role slices - into `dst`.
bool copy_from_files(int64_t layer, int64_t expert, uint8_t* dst) const;
@@ -521,11 +598,12 @@ private:
struct Fill { size_t v; int64_t layer, e; uint8_t* dst; };
/// The claimed buffers' blobs: one overlapped batch when unbuffered, else the fetch threads' mapped copies.
void fill_many(const std::vector<Fill>& todo);
/// #286: the blobs from the drive, unbuffered: every role's 4 KiB-aligned window is read at once (overlapped)
/// into this thread's aligned buffer, then copied into place. False when a read fails.
/// #286: the blobs from the drive, unbuffered: every role's 4 KiB-aligned window is read at once (overlapped on
/// Windows, io_submit on Linux) into this thread's aligned buffer, then copied into place. False when a read fails.
bool read_direct(const Fill* fills, size_t n) const;
bool open_direct(std::string& why);
std::vector<std::string> paths_; ///< the mapped files, as maps_
std::vector<void*> direct_; ///< #286: per file, an unbuffered overlapped handle (Windows)
std::vector<void*> direct_; ///< #286: per file, an unbuffered handle (Windows) or O_DIRECT fd (Linux)
std::vector<int> role_file_; ///< 3 x n_layers: index into maps_ / direct_
/// blobs assembled in the stage buffers (`blob` hands those out): the GGUF in place, or any unbuffered source
bool staged() const { return !role_ptr_.empty() || !direct_.empty(); }
@@ -559,6 +637,7 @@ private:
std::atomic<uint64_t> file_blob_bytes_{0}, file_us_{0};
std::unique_ptr<std::atomic<uint32_t>[]> warm_stamp_; ///< per (layer, expert): epoch_ + 1 when warmed
std::atomic<int64_t> warm_hits_{0}, warm_count_{0};
mutable std::atomic<int64_t> direct_fallbacks_{0};
std::unordered_map<int64_t, size_t> stage_of_;
uint64_t stage_blob_ = 0;
uint64_t stage_seq_ = 0;
@@ -578,6 +657,7 @@ private:
const uint8_t* complement_device_ = nullptr;
uint64_t complement_bytes_ = 0;
std::vector<uint64_t> complement_offsets_;
detail::ExchangeStorage exchange_storage_; // authoritative when active; original arenas still own memory
bool complement_pinned_ = false;
bool complement_partial_ = false; ///< CS-T: only the first complement_pin_limit_ bytes are registered
uint64_t complement_pin_limit_ = 0;
@@ -641,10 +721,14 @@ public:
int64_t reads() const { return reads_; }
bool pinned(int64_t layer, int64_t expert) const override;
const uint8_t* device_alias(int64_t layer, int64_t expert) const override;
void prefetch(int64_t layer, int64_t expert) override;
uint64_t release(int64_t layer, int64_t expert) override;
/// What backing was obtained and why, for the startup print. "The engine adapts to the machine it is on" is
/// only true if the engine says what it got.
const std::string& note() const { return note_; }
/// #633: set by `open` when the RAM available is less than the arena needs (a recommendation; it still loads)
const std::string& ram_warning() const { return ram_warning_; }
double load_gib_per_second() const { return gib_per_s_; }
// Loader fix: the load, split. `load_seconds()` is the wall clock of the load loop; the other two are
// sums over the reader threads (see LoadStats), so on their own they say how much of that wall was spent
@@ -655,6 +739,8 @@ public:
private:
void* arena_ = nullptr; ///< the PinnedArena, owned
void* map_ = nullptr; ///< STRATA_ARENA_MMAP: the arena file, mapped read-only (not the PinnedArena)
uint64_t map_bytes_ = 0;
std::vector<const uint8_t*> dev_slice_; ///< device alias of each registered slice (or of the whole range)
uint64_t slice_bytes_ = 0;
const uint8_t* base_ = nullptr;
@@ -662,6 +748,7 @@ private:
int64_t n_expert_ = 0;
int64_t reads_ = 0;
std::string note_;
std::string ram_warning_;
double gib_per_s_ = 0.0;
double load_seconds_ = 0.0;
double load_read_s_ = 0.0;
@@ -675,8 +762,9 @@ private:
/// layout's type and dimensions, and inside the file. `native` is the --native shard (see set_gguf).
bool check_experts_gguf(const std::string& native, const strata::kernels::cpu::ExpertLayout& lay, std::string& err);
/// Fills `dst` (lay.total bytes, the experts.bin layout) from the GGUF files, one role at a time.
/// `unbuffered`: each chunk read past the file cache (Windows).
/// `unbuffered`: each chunk read past the file cache (Windows); `ready`: layer l is written only once
/// *ready > l + 1 (an arena that is still being registered).
LoadStats load_experts_gguf(const std::string& native, uint8_t* dst, const strata::kernels::cpu::ExpertLayout& lay,
int threads, bool unbuffered = false);
int threads, bool unbuffered = false, const std::atomic<int>* ready = nullptr);
} // namespace strata::core
+30 -8
View File
@@ -41,6 +41,9 @@
#include "strata/kernels/ple.hpp"
#include <cstdio>
#include <functional>
#include "strata/core/vmm.hpp"
#include <cstdlib>
#include <cstdint>
#include <string>
@@ -168,6 +171,9 @@ bool moe_layer(const WeightTable& tables, const ModelGeometry& g, int64_t layer,
/// rung, so a host that sees it knows `h_x_f`/`h_ids`/`h_weights` are in place.
bool moe_route(const WeightTable& tables, const ModelGeometry& g, int64_t layer, int64_t k, const MoEBuffers& b,
const float* x, void* stream, std::string& err, const Doorbell* db = nullptr);
/// The verify window's n tokens routed at once (see layer.cpp); bitwise per token what `moe_route` gives.
bool moe_route_window(const WeightTable& tables, const ModelGeometry& g, int64_t layer, int64_t k, const MoEBuffers& b,
const float* x, float* logits, int32_t* ids, float* weights, int n, void* stream, std::string& err);
/// The finishing half: the shared expert and the combination.
///
@@ -230,6 +236,9 @@ struct QsaState {
/// pages at `block % n_slots`, with the host copy for a resume. The pool pointers above point at the slots.
int kv_mode = 0;
int64_t n_slots = 0;
/// The elastic K/V (qsa_set_kv_elastic): this state's pools are in a VMM range mapped only as far as the
/// context needs - its index in layer.cpp's registry; -1: carved from the arena as before.
int32_t kv_elastic = -1;
strata::kernels::KvHostPools host;
strata::kernels::KvStreamMap map;
int64_t idx_pooled_rows = 0; ///< rows of `idx_pooled` (a ring, which has no indexer, keeps 2)
@@ -273,6 +282,24 @@ uint64_t qsa_state_bytes(const ModelGeometry& g, int64_t max_cells, bool with_ro
/// Also puts the MTP drafter's K/V in a ring of its window (`ring_cells` of qsa_state_bytes/init; -1 forces a fully
/// resident state).
void qsa_set_kv_resident(int64_t cells);
/// THE ELASTIC K/V (--kv-grow; vmm.hpp). A mode-0 state's K/V pools get addresses for every cell of the context but
/// physical memory only for the first `init_cells`; `qsa_kv_elastic_grow` maps more as the context grows (from
/// chunks the expert cache gives up) and `qsa_kv_elastic_shrink` hands them back. The addresses never move, so the
/// captured graphs stay valid. Set before sizing and initializing the session and the drafter.
void qsa_set_kv_elastic(bool enabled, int64_t init_cells);
bool qsa_kv_elastic();
/// Cells every elastic state can hold now (INT64_MAX when none is elastic).
int64_t qsa_kv_elastic_cells();
/// Chunks still to map for every elastic state to hold `cells` cells.
int64_t qsa_kv_elastic_need(int64_t cells);
/// Maps them, each from `take()` (0: a new chunk), the new memory zeroed. Synchronous; nothing may be running on
/// the device. false: out of memory.
bool qsa_kv_elastic_grow(int64_t cells, const std::function<VmmChunk()>& take);
/// Unmaps the chunks past `cells` cells, each handed to `give`. Returns how many.
int64_t qsa_kv_elastic_shrink(int64_t cells, const std::function<void(VmmChunk)>& give);
/// Physical bytes the elastic pools hold, and what all of them would at the full context.
uint64_t qsa_kv_elastic_mapped_bytes();
uint64_t qsa_kv_elastic_full_bytes();
int64_t qsa_kv_resident();
/// The fewest resident cells a streamed layer may have: one verify window's selections (8 queries x 2,051 cells
/// in whole blocks) must fit at once, with room to spare.
@@ -288,17 +315,12 @@ bool qsa_kv_int8();
/// PR #21: store K/V as Q4_0 after a Hadamard rotation (`--kv q4_0`): 576 B per cell, vs 1,056 in INT8.
void qsa_set_kv_q4(bool enabled);
bool qsa_kv_q4();
/// Hybrid K8V4 (`--kv k8v4`): K in INT8, V in rotated Q4_0 - 816 B per cell. Not with --kv-resident.
/// Hybrid K8V4 (`--kv k8v4`): K in INT8, V in rotated Q4_0 - 816 B per cell; streams with --kv-resident too.
void qsa_set_kv_hybrid(bool enabled);
bool qsa_kv_hybrid();
/// The state's KV format for the block-moving functions of kv_stream.hpp (kKvF16 / kKvInt8 / kKvQ4).
/// The state's KV format for the block-moving functions of kv_stream.hpp (kKvF16 / kKvInt8 / kKvQ4 / kKvHybrid).
inline int qsa_kv_format(const QsaState& st) {
// A hybrid K8V4 state is mode 0 only and never reaches the block movers; refuse rather than let it
// fall through to kKvF16 - a wrong layout silently applied is worse than a hard stop (PR review).
if (st.kv_hybrid) {
std::fprintf(stderr, "strata: qsa_kv_format: a hybrid K8V4 state must never reach the block movers\n");
std::exit(1); // the kernels' own "unsupported geometry" convention (kv_q8.cu, qsa_decode_attn.cu)
}
if (st.kv_hybrid) return strata::kernels::kKvHybrid; // K8V4: its own three runs (kv_stream.cu)
return st.kv_q4 ? strata::kernels::kKvQ4 : st.kv_int8 ? strata::kernels::kKvInt8 : strata::kernels::kKvF16;
}
uint64_t qsa_state_init(const ModelGeometry& g, int64_t max_cells, void* base, QsaState& st,
+91 -4
View File
@@ -43,11 +43,26 @@ public:
/// Loads `rt_dir` (from tools/mtp_rt.py) and allocates the layer's K/V and buffers for up to `max_t` rows.
/// Call before the VRAM expert tier is sized: this takes ~0.9 GB.
bool load(const std::string& rt_dir, const ModelGeometry& g, SessionState& ss, int max_t, std::string& err,
int64_t window = 32768);
int64_t window = 32768, const MtpDrafter* shared = nullptr);
/// The prompt's length: prefill() skips the cells the attention window can never reach again.
void set_prompt_len(int64_t n) { prompt_len_ = n; }
/// At most this many drafts per round (below max_t - 1): a window longer than the MTP's comes from elsewhere.
void set_max_drafts(int k) { max_drafts_ = k; }
/// --mtp-hnorm stream (opt-in; before the first draft or prefill): pre_fc_norm_hidden normalizes each
/// hyper-connection stream on its own, as llama.cpp's qwen4exp MTP graph does, instead of one RMS over all four.
void set_hnorm_per_stream(bool on) { hnorm_stream_ = on; }
bool hnorm_per_stream() const { return hnorm_stream_; }
/// --mtp-q4 (opt-in; before load): the draft layer's Q8_0 projections and its draft head run from 4-bit (Q4_0)
/// copies made at load/bind - fewer bytes per draft step; drafts only, the verify window decides every token.
/// (The prompt path's batched K/V pass keeps the Q8_0 originals.)
void set_q4(bool proj, bool head) { q4_ = proj; q4_head_ = head; }
/// --mtp-draft-vocab FILE (opt-in; before bind): the draft head's token subset from FILE instead of
/// rt/draft_vocab.bin (e.g. data/draft_vocab_en.bin, 40K tokens)
void set_draft_vocab(const std::string& path) { dvocab_path_ = path; }
/// STRATA_MTP_TOP2=1 (diagnostic): draft j's runner-up token in the last draft() (-1 = unknown)
static bool top2_env();
int32_t top2(int j) const { return j >= 0 && j < (int) top2_.size() ? top2_[(size_t) j] : -1; }
int max_t() const { return max_t_; }
uint64_t vram_bytes() const { return vram_; }
/// The draft layer's K/V state (read-only: --serve's STRATA_STATE_HASH check hashes it)
const QsaState& kv_state() const { return st_; }
@@ -58,11 +73,18 @@ public:
/// the draft head over rt/draft_vocab.bin's subset. The expert cache is sized before bind(), so it reserves this.
uint64_t bind_bytes(uint64_t head_row_bytes, int64_t n_vocab) const;
/// The main model's embedding and head, and the verify window's final residuals (T rows, hc*n_embd each).
bool bind(const WeightTable& wt, const NativeHead* head, const float* window_R, std::string& err);
bool bind(const WeightTable& wt, const NativeHead* head, const float* window_R, std::string& err,
const MtpDrafter* shared = nullptr);
/// Prompt cells [cell0, cell0 + n): residual rows `R_rows` (device, hc*n_embd each) and `next_tokens` (host,
/// the token at position cell+1). Runs in batches of up to max_t rows.
bool prefill(const float* R_rows, const int32_t* next_tokens, int64_t n, int64_t cell0, std::string& err);
/// the token at position cell+1). Runs in batches of up to max_t rows. `sync` false: returns without waiting
/// for the drafter's stream (the caller orders on it: `stream()`).
bool prefill(const float* R_rows, const int32_t* next_tokens, int64_t n, int64_t cell0, std::string& err,
bool sync = true);
/// --pipeline-windows: capture the prefill graphs and size its input records for windows of up to max_t rows now,
/// so a later prefill(sync = false) of a window neither captures nor allocates (both wait for the device).
bool prepare_prefill(std::string& err);
cudaStream_t stream() const { return cs_; }
/// One round: catch-up over T cells from `p` (rows = the window's final residuals, `tokens` = the window's
/// argmaxes: row t pairs R_{p+t} with the token at p+t+1), then the draft chain from row `a` (the last
@@ -82,8 +104,34 @@ public:
/// Coupled mode with penalties: the history the next draft() chain starts from - what the session holds after the
/// window's commit (`tail`) and the window's pick at its last accepted row (`next`, the next window's row 0).
void set_draft_history(const int32_t* tail, int64_t n_tail, int32_t next);
void set_ple_session(SessionState* s) { ple_ss_ = s; }
bool coupled() const { return coupled_active_; }
// ---- --pipeline-windows 2: the chain as one asynchronous launch (the round and its steps back to back, no host
// wait), its first `n_force` steps fed the given tokens instead of their own picks (teacher forcing: the drafts of
// a window already in flight), each output read once the event after its step has completed.
/// Before `load`: the round/step graphs get the forcing kernel (a no-op while its token is -1), and the drafter's
/// stream the highest priority (STRATA_MTP_PRIORITY=0: the default priority).
void set_force_capture(bool on) { force_on_ = on; }
/// The rows the next round reads (null: the bound `window_R`), copied into the bound buffer before the round.
void set_source_R(const float* rows) { src_R_ = rows; }
/// Capture the round graphs for T = 1..max_t and every step graph now (a capture syncs the drafter's stream).
bool prepare_chain(std::string& err);
/// The catch-up over T cells from `p` (as draft()), then `n_out` outputs from row `a`: output j is the chain's
/// own pick, and step j (1 <= j <= n_force) takes force[j - 1] as its input token instead of output j - 1.
/// `n_early`: an event after each of the first `n_early` outputs (chain_outputs_ready).
bool chain_launch(int T, const int32_t* tokens, int64_t p, int a, const int32_t* force, int n_force, int n_out,
int n_early, std::string& err);
/// 1: every output is in chain_tok()/chain_prob(); 0: still running; -1: an error.
int chain_poll(std::string& err);
/// How many of the chain's first outputs (up to `n_early`; all of them once the chain is done) have landed in
/// chain_tok()/chain_prob(); -1: an error. A window's size is often decided by its first outputs (a draft below
/// spec_min_p ends it), so the next launch need not wait for all of them.
int chain_outputs_ready(std::string& err);
bool chain_live() const { return chain_live_; }
const int32_t* chain_tok() const { return chain_tok_; }
const float* chain_prob() const { return chain_prob_; }
double ms_draft = 0, ms_prefill = 0;
int64_t rounds = 0;
@@ -102,6 +150,13 @@ public:
private:
bool record_forward(int T, int step_row0, cudaStream_t cs, std::string& err);
/// The layer's front for T rows at step rows [row0, +T): the embedding, the fc projections, the attention
/// hyper-connection read (R_, inj_, mixed_) and the K/V appended.
bool record_front(int T, int row0, cudaStream_t cs, std::string& err);
/// The rest for row 0 at step row `step_row`: the attention from the q projection, the MLP, the final mixer,
/// the head, the draft and its probability. Needs row 0's R_, inj_ and mixed_ (its q8_1 rows in xq_).
bool record_rest(int step_row, cudaStream_t cs, std::string& err);
void norm_rope(float* data, const float* gamma, int rows, int cols, const int32_t* p, cudaStream_t cs);
bool capture_prefill(int T, std::string& err);
bool capture_prefill_dev(int T, std::string& err); ///< E-4: without the mapped staging (inputs copied on device)
bool capture_round(int T, bool coupled, std::string& err);
@@ -124,12 +179,37 @@ private:
const ModelGeometry* g_ = nullptr;
SessionState* ss_ = nullptr;
SessionState* ple_ss_ = nullptr;
const WeightTable* wt_ = nullptr;
const NativeHead* head_ = nullptr;
const float* window_R_ = nullptr;
int max_t_ = 0;
int device_ = -1; ///< the device `load` ran on: the public calls switch to it (layer split)
int max_drafts_ = 1 << 30;
bool hnorm_stream_ = false;
bool q4_ = false, q4_head_ = false;
uint8_t* dense4_ = nullptr; ///< --mtp-q4: Q4_0 copies of the Q8_0 tensors
std::vector<std::pair<std::string, uint64_t>> q4_off_;
int dhead_type_ = -1; ///< the draft head subset's ggml type (the main head's, or Q4_0)
std::string dvocab_path_;
std::string vocab_file() const { return dvocab_path_.empty() ? rt_dir_ + "/draft_vocab.bin" : dvocab_path_; }
/// a projection's weights for the draft layer's own pass: the Q4_0 copy under --mtp-q4, else the Q8_0 original
const void* wq(const char* name, int& type) const;
bool make_q4_dense(const std::vector<uint8_t>& blob, std::string& err);
bool make_q4_head(std::string& err);
void record_top2(int j);
std::vector<int32_t> top2_, dvocab_host_;
std::vector<float> lg_host_;
// --pipeline-windows 2 (chain_launch)
bool stage_source_R(int T, std::string& err); ///< set_source_R's rows into the bound buffer
bool force_on_ = false, chain_live_ = false;
const float* src_R_ = nullptr;
int32_t *h_force_ = nullptr, *m_force_ = nullptr; ///< the forced tokens (mapped), -1 = the step's own pick
cudaEvent_t ev_chain_ = nullptr;
cudaEvent_t ev_step_[8] = {}; ///< after each of the first n_early outputs
int steps_seen_ = 0, chain_n_ = 0, chain_early_ = 0;
int32_t chain_tok_[8] = {};
float chain_prob_[8] = {};
int64_t n_vocab_ = 0;
uint64_t vram_ = 0;
cudaStream_t cs_ = nullptr;
@@ -137,12 +217,17 @@ private:
cudaGraphExec_t prefill_dev_exec_[9] = {};
int32_t* pf_dev_ = nullptr; ///< E-4: a prompt's rows' token / step / position records, uploaded at once
int64_t pf_cap_ = 0; ///< its capacity in ints
cudaStream_t side_ = nullptr; // the shared expert's branch of the draft graphs (CUDA)
cudaEvent_t sh_fork_ = nullptr, sh_join_ = nullptr;
cudaGraphExec_t round_exec_[9] = {};
struct Tensor { std::string name, kind; int64_t rows = 0, cols = 0; uint64_t off = 0, bytes = 0; };
std::vector<Tensor> tensors_;
uint8_t* dense_ = nullptr;
uint8_t* experts_ = nullptr;
// Slot drafters borrow immutable weights and head; each still owns its state and scratch.
bool owns_weights_ = true;
bool owns_draft_head_ = true;
void* state_arena_ = nullptr;
QsaState st_;
void* arena_ = nullptr;
@@ -161,6 +246,8 @@ private:
int64_t window_ = 0; // attention over the last window_ cells (0 = every cell)
int64_t prompt_len_ = 0;
float* probs_ = nullptr;
uint8_t* arg_scratch_ = nullptr; ///< argmax_rows' and row_top_prob_split's partials and counters
uint8_t* top_scratch_ = nullptr;
// device
int32_t *tok_ = nullptr, *step_ = nullptr, *pos_ = nullptr, *row_ = nullptr, *ident_ = nullptr;
float *Rin_ = nullptr, *R_ = nullptr, *emb_ = nullptr, *en_ = nullptr, *e2_ = nullptr, *hn_ = nullptr, *h2_ = nullptr;
+8 -2
View File
@@ -19,21 +19,27 @@ public:
~NativeDense();
NativeDense(const NativeDense&) = delete;
NativeDense& operator=(const NativeDense&) = delete;
/// With layer_hi >= 0, only the `blk.<l>.` matrices with layer_lo <= l < layer_hi are uploaded (a layer
/// split's stage holds its own layers' projections, not the whole model's); the others keep data == nullptr.
bool load(const std::vector<std::string>& shards, WeightTable& table, std::string& err,
bool include_ple_key = false);
bool include_ple_key = false, int64_t layer_lo = 0, int64_t layer_hi = -1);
/// Plan v0.3 P1: the canonical tensor names `load` would serve natively from these shards (eligible name,
/// supported type, 2-D), read from the GGUF headers only - so the canonical arena can skip them.
static bool served_names(const std::vector<std::string>& shards, bool include_ple_key,
std::set<std::string>& out, std::string& err);
/// Layer split: load only blocks [lb, le) (every other `blk.N.` projection belongs to another GPU's stage; the
/// PLE tensors are loaded everywhere). Process-wide, read by the next `load`; (-1, -1) = all layers.
static void set_layer_range(int lb, int le);
/// #326: a native pack whose `blk.1.ple_key.weight` row is unquantized (iq_pack --compat-bf16 of a GGUF key
/// the native kernel also reads, e.g. OrcaRouter's IQ3_XXS) serves the PLE from that row, so it is taken out
/// of `skip` and `load` does not upload the GGUF key over it. A quantized row leaves `skip` unchanged.
static bool keep_unquantized_ple_key(const std::string& pack_dir, std::set<std::string>& skip, std::string& err);
uint64_t weight_bytes() const { return bytes_; }
size_t tensor_count() const { return weights_.size(); }
size_t tensor_count() const { return weights_.size() - packed_keys_.size(); }
private:
std::vector<void*> weights_;
std::vector<const void*> packed_keys_; // GGUF-layout pointers registered with STRATA_Q8_PACKED=1
void* scratch_ = nullptr;
uint64_t bytes_ = 0;
};
+20 -3
View File
@@ -14,6 +14,7 @@
// fallback is the common case and not an error path.
#pragma once
#include <atomic>
#include <cstdint>
#include <string>
#include <vector>
@@ -51,6 +52,14 @@ struct PinnedArena {
/// existing allocation path. Population/coordination and backing-file lifetime remain the caller's job.
PinnedArena(uint64_t bytes, const std::vector<uint64_t>& bounds, uint64_t max_pinned_bytes = 0,
const std::string& shared_file = {}, uint64_t shared_pack_hash = 0);
/// #285: reserve only; the caller registers the slices with register_slices(), on a thread, while its readers
/// fill the ones already registered (one registration of the whole arena cost ~2.5 s at 33 GiB of 4 KB pages).
struct Deferred {};
PinnedArena(uint64_t bytes, const std::vector<uint64_t>& bounds, Deferred);
/// Registers the deferred slices in order, publishing how many are done in `ready`, and INT_MAX once it
/// returns (a slice CUDA refuses ends the registration, and the rest is kept resident by the working-set lock).
void register_slices(std::atomic<int>& ready);
std::vector<uint64_t> bounds_;
std::vector<uint64_t> slice_starts;
void* mapping_base = nullptr; ///< actual mapping start; differs from base when a shared-file header exists
uint64_t mapping_bytes = 0; ///< bytes to release from mapping_base
@@ -101,14 +110,19 @@ struct LoadStats {
LoadStats load_experts(const std::string& path, uint8_t* dst, uint64_t blob_bytes, uint64_t blobs_per_layer,
uint64_t layers, int threads, uint64_t chunk);
/// Plan v0.3 P6: the same with one byte range per layer (`layer_off[L]`, `layer_bytes[L]`).
/// `ready` (optional): layer L is written only once *ready > L + 1 - its slice and the next one registered (a
/// PinnedArena registering its slices meanwhile: writing a page while cudaHostRegister runs on it corrupted the
/// arena under WDDM, even on large pages; a layer boundary inside a page puts that page in both registrations).
LoadStats load_experts_ranges(const std::string& path, uint8_t* dst, const std::vector<uint64_t>& layer_off,
const std::vector<uint64_t>& layer_bytes, int threads, uint64_t chunk);
const std::vector<uint64_t>& layer_bytes, int threads, uint64_t chunk,
const std::atomic<int>* ready = nullptr);
/// The same ranges read UNBUFFERED straight into `dst` (Windows): no staging buffer and no file-cache copy - the
/// drive's DMA lands where the experts live. Every range, `dst` and `chunk` must be 4 KiB aligned; returns ok =
/// false with an empty `error` when they are not (or off Windows), and the caller falls back to load_experts_ranges.
LoadStats load_experts_direct(const std::string& path, uint8_t* dst, const std::vector<uint64_t>& layer_off,
const std::vector<uint64_t>& layer_bytes, int threads, uint64_t chunk);
const std::vector<uint64_t>& layer_bytes, int threads, uint64_t chunk,
const std::atomic<int>* ready = nullptr);
/// Whether the expert files are better read unbuffered (Windows; false elsewhere): a timed probe of random 64 KiB
/// reads says they are not in the OS file cache (a cached read takes ~10 us, the drive ~80), and the RAM left
@@ -117,8 +131,11 @@ LoadStats load_experts_direct(const std::string& path, uint8_t* dst, const std::
/// STRATA_UNBUFFERED_LOAD=1 / 0 forces it. `why` says what decided.
/// `cache_counts` false (the file tier with a RAM budget): only whether the files could be kept decides - their mapped
/// pages land in the process's working set, so a partly cached file would still take the RAM the budget was sized for.
/// `read_bytes` (#577): the bytes the files are read for, when that is less than the files (the file tier reads only
/// the experts outside its RAM copy); `kAllFileBytes` = every byte of `files`. See platform::file_cache_keeps.
constexpr uint64_t kAllFileBytes = ~0ull;
bool experts_unbuffered(const std::vector<std::string>& files, uint64_t arena_bytes, std::string& why,
bool cache_counts = true);
bool cache_counts = true, uint64_t read_bytes = kAllFileBytes);
// FNV-1a 64. Per layer, so a corrupt or short read names WHICH layer rather than just failing a whole-file
// comparison - the same reason the Phase 1 tools report the first differing element.
+11
View File
@@ -22,6 +22,9 @@ struct Progress {
std::atomic<int64_t> chunk{-1}; ///< #251: the prompt chunk (its first position) a batched-read stage is in
std::atomic<int64_t> since_ms{0}; ///< when `where` was set (steady clock): how long a stage has lasted
std::atomic<uint64_t> ticks{0}; ///< layers served: a window that still moves, slowly, against one that stopped
/// a step that blocks in one call (a file flush, a host->device transfer of a whole session) is given until this
/// time (steady ms; 0 = none) instead of the watchdog's limit - an explicit, bounded allowance, not a beat
std::atomic<int64_t> allow_until_ms{0};
};
inline int64_t progress_now_ms() {
@@ -39,6 +42,8 @@ inline std::atomic<DiagFn>& release_gpu_fn() { static std::atomic<DiagFn> f{null
inline void release_gpu_waits(std::FILE* f) {
if (auto fn = release_gpu_fn().load()) fn(f);
}
/// --pipeline-windows: the pipelined loop's windows and the drafter's chain (set while that loop runs)
inline std::atomic<DiagFn>& diag_pipeline_fn() { static std::atomic<DiagFn> f{nullptr}; return f; }
inline Progress& progress() {
static Progress p;
@@ -55,5 +60,11 @@ inline void progress_at(const char* where, int64_t detail = -1, int64_t chunk =
inline void progress_beat() { progress().beats.fetch_add(1, std::memory_order_relaxed); }
inline void progress_tick() { progress().ticks.fetch_add(1, std::memory_order_relaxed); }
/// A beat that also sets (seconds > 0) or clears (0) the allowance of a blocking step: the watchdog does not fire
/// before the allowance ends, and fires at its end if nothing beat since.
inline void progress_allow(int64_t seconds) {
progress().allow_until_ms.store(seconds > 0 ? progress_now_ms() + seconds * 1000 : 0, std::memory_order_relaxed);
progress_beat();
}
} // namespace strata::core
+53
View File
@@ -0,0 +1,53 @@
#pragma once
#include <cstddef>
#include <cstdint>
#include <string>
#include <utility>
#include <vector>
namespace strata::core {
class RemoteExperts;
class ExpertSource;
// Remote Expert Decode Optimization v1 on existing helper caches. One instance per serial engine.
class RemoteExpertOpt {
public:
RemoteExpertOpt() = default;
~RemoteExpertOpt();
RemoteExpertOpt(const RemoteExpertOpt&) = delete;
RemoteExpertOpt& operator=(const RemoteExpertOpt&) = delete;
void attach(RemoteExperts& remote); // before RemoteExperts::open
bool init(std::string& err); // after all helper caches are open
bool owns(int64_t layer, int32_t expert) const;
bool adapt(const std::vector<float>& usage, const std::vector<int32_t>& primary,
const std::vector<std::pair<int32_t, int32_t>>& pending, int max_swaps,
ExpertSource& source);
// Bracket the existing pool callback; its signature and dispatch remain unchanged.
void begin(const float* weights, int token_begin, int tokens);
void end() { weights_ = nullptr; }
void copy_rows(float* dst, const float* src, int token_begin, int tokens,
const int32_t* hit_rows, const int32_t* count, void* stream) const;
void combine(float* dst, float* scratch, int token_begin, int tokens,
const uint32_t* skip, uint32_t ring, void* stream) const;
private:
friend class RemoteExperts;
static size_t metadata_bytes();
bool active() const { return weights_ != nullptr; }
void prepare(const RemoteExperts& remote, void* metadata) const;
bool reduce(RemoteExperts& remote, const void* metadata, std::string& err);
void accumulate(const RemoteExperts& remote);
struct Peer { RemoteExperts* remote; float* sum = nullptr; };
std::vector<Peer> peers_;
float* h_sum_ = nullptr;
float* m_sum_ = nullptr;
int32_t* h_mask_ = nullptr;
int32_t* m_mask_ = nullptr;
const float* weights_ = nullptr;
int token_begin_ = 0, tokens_ = 0;
};
} // namespace strata::core
+7 -1
View File
@@ -11,6 +11,7 @@
#include <vector>
namespace strata::core {
class RemoteExpertOpt;
/// A static, profile-filled expert tier on another CUDA device. CUDA0 keeps all
/// dense weights and state; results return through the existing pinned CPU rows.
@@ -27,7 +28,7 @@ public:
bool open(int device, int slots, int64_t layers, int64_t experts,
const std::vector<std::pair<int32_t, int32_t>>& ranked,
const ExpertCache& primary, ExpertSource& source,
std::vector<uint8_t>& claimed, std::string& err);
std::vector<uint8_t>& claimed, std::string& err, bool auto_size = false);
void close();
/// `kind` is the primary verifier's classification (-1 = CPU candidate),
@@ -36,6 +37,9 @@ public:
int64_t k, const int32_t* kind, const int32_t* primary_res,
std::string& err);
bool owns(int64_t index) const { return owned_[(size_t) index] != 0; }
/// This helper's cache holds (layer, expert): begin() will take its rows unless the plan gave them away.
bool holds(int64_t layer, int32_t expert) const { return cache_.slot_of(layer, expert) >= 0; }
bool optimized_decode() const { return remote_opt_ != nullptr; }
bool finish(float* out, std::string& err);
int64_t resident() const { return cache_.resident(); }
int64_t computed() const { return computed_; }
@@ -48,6 +52,8 @@ public:
double ms_wait() const { return ms_wait_; }
private:
friend class RemoteExpertOpt;
RemoteExpertOpt* remote_opt_ = nullptr;
int device_ = -1;
int64_t n_expert_ = 0;
int32_t groups_ = 0;
+2 -1
View File
@@ -16,6 +16,7 @@
#include "strata/core/hit_hook.hpp"
#include "strata/core/layer.hpp"
#include "strata/kernels/cpu/pool.hpp"
#include <cuda_runtime.h>
@@ -369,7 +370,7 @@ struct SessionLoopScratch {
float* y_miss = nullptr; ///< pinned host staging for the pool's answer, `parts_bytes` long
size_t parts_bytes = 0;
cudaEvent_t probe = nullptr;
long long pinned_core = -1; ///< the affinity to restore, or -1 if the host was never pinned
strata::kernels::cpu::ThreadAffinity pinned_core{}; ///< affinity to restore, or invalid if the host was not pinned
bool pinned = false;
/// Allocates the buffers and pins the host thread. Call ONCE, at session setup.
+177
View File
@@ -31,6 +31,8 @@
#include <cuda_runtime.h>
#include <atomic>
#include <chrono>
#include <map>
#include <cstdint>
#include <string>
#include <vector>
@@ -38,6 +40,7 @@
namespace strata::core {
class NativeHead;
class RemoteExpertOpt;
/// The CPU pool for a window: x_f (n_tok, n_embd), ids (n_tok, k) -> out (n_tok * k, n_embd), hit rows zeroed.
using PoolMultiFn = void (*)(void* user, const float* x_f, const int32_t* ids, int64_t n_tok, int64_t k, float* out,
@@ -45,6 +48,7 @@ using PoolMultiFn = void (*)(void* user, const float* x_f, const int32_t* ids, i
struct VerifyHits {
const int32_t* d_res = nullptr; ///< device [n_layers * n_expert] slot or -1
const int32_t* h_res = nullptr; ///< host [n_layers * n_expert] slot or -1 (when all >= 0 in stage: zero-doorbell)
const uint8_t* cache_base = nullptr; ///< slot 0 of the VRAM expert arena
const uint64_t* slot_off = nullptr; ///< E-6: host per-slot offsets when slots differ in size (null: slot * blob)
int64_t n_slots = 0; ///< E-6: how many (for the device copy)
@@ -57,9 +61,13 @@ public:
~Verifier();
Verifier(const Verifier&) = delete;
Verifier& operator=(const Verifier&) = delete;
void set_remote_expert_opt(RemoteExpertOpt* opt) { remote_opt_ = opt; } // before init/capture
/// The watchdog's view of the window in flight (issue #31): the layer, the GPU's sequence, the flags.
void diag(std::FILE* f) const;
/// #649 (STRATA_VERIFY_TRACE=1, opt-in): the last windows' host-side handshake events and the GPU's breadcrumbs
/// (in mapped memory, so they read while the GPU hangs). Nothing when the trace is off.
void trace_dump(std::FILE* f) const;
/// #267: raise every flag the window's spin kernels wait on past any ring (UINT32_MAX), so a window the GPU
/// cannot finish drains instead of staying resident, then wait up to `timeout_ms` for its streams. For the
@@ -119,6 +127,55 @@ public:
/// Keep the first `n_keep` (1..T) tokens of the last window; advances `ss.ple_prev` by them.
bool commit(int n_keep, std::string& err);
// ================================ SEVERAL SEQUENCES IN ONE WINDOW ================================
//
// A batch window holds S INDEPENDENT sequences, one token each: row s is slot s, at slot s's own position,
// reading and writing slot s's own state (GDN recurrence and conv history, QSA K/V and indexer, PLE history),
// which lives in `slots[s]` - a session carved like this verifier's own (same layer range, same max_cells).
// Everything that is per row already (hyper-connections, dense projections, router, shared expert, the
// routed experts on the GPU and the CPU, the head) runs once over the S rows, so the weights are read once
// per window for all the sequences. Row s's arithmetic is the single-token window's, so a slot's greedy
// tokens are its solo greedy tokens (modulo the multi-token CPU kernel choice: STRATA_IQ_MT_MIN=1).
//
// Greedy only, no drafts (MTP) in a batch window. `init_slots` once after `init` (S <= max_t); a layer
// split's stages each get their own sessions, and run_slots/commit_slots continue into the next stage.
bool init_slots(const std::vector<SessionState*>& slots, std::string& err);
int n_slots() const { return (int) slots_.size(); }
/// One batch window over slots [0, S): tokens[s] at positions pos[s]; out[s] = the greedy pick after it.
bool run_slots(int S, const int32_t* tokens, const int64_t* pos, PoolMultiFn pool, void* user, int32_t* out,
std::string& err);
/// The same over the S slots `rows` (row t is slot rows[t], any distinct slots in any order): the slots not
/// listed are not touched, so an idle slot keeps its state (a finished conversation it may continue later).
bool run_slot_rows(const int* rows, int S, const int32_t* tokens, const int64_t* pos, PoolMultiFn pool, void* user,
int32_t* out, std::string& err);
/// Keep every row of the last batch window.
bool commit_slots(std::string& err);
/// Commit an accepted prefix in each contiguous slot group of the last window. `keep` has
/// one entry per slot (indexed by slot ID), each in 1..that slot's group length.
bool commit_slot_prefixes(const int* keep, std::string& err);
/// --batch-mtp: slot rotation makes many row layouts, so bound the captured batch graph pairs (LRU). 0 = unbounded.
void set_batch_graph_limit(size_t n) { batch_graph_limit_ = n; }
// ---- The stages of a layer split as a PIPELINE. A batch window over the slot GROUP
// [base, base + S) is launched on ONE stage with its commit right behind it on the stage's stream (a batch window
// keeps every row, so the commit needs no host decision), and the host serves the rings of every stage that has
// a window in flight from one thread (batch_poll does not block). Stage k can then run group g while stage k+1
// runs group g-1. Rows of group `base` use hand-off rows [base, base + S), so groups never share a hand-off row.
bool batch_launch(int base, int S, const int32_t* tokens, const int64_t* pos, std::string& err);
/// 1 = this stage's window and commit are done (the last stage's picks are in batch_out), 0 = still running,
/// -1 = an error (err). Serves every layer that has rung so far.
int batch_poll(PoolMultiFn pool, void* user, std::string& err);
bool batch_busy() const { return b_running_; }
/// A slot's sampling (temperature / top_p / top_k / min_p / seed; penalties are not applied in batch windows):
/// its row is drawn again on the last stage with Philox(seed, position), as a solo window draws it. Greedy by
/// default. Set on the first stage, it reaches the last.
void set_slot_sampling(int slot, const strata::kernels::SamplerParams& sp) {
if (slot >= 0 && slot < (int) slot_sp_.size()) slot_sp_[(size_t) slot] = sp;
if (next_) next_->set_slot_sampling(slot, sp);
}
const int32_t* batch_out() const { return b_out_; }
bool last_stage() const { return g_ != nullptr && le_ == g_->n_layers; }
/// commit() returns without waiting for its graph (a single-GPU session sets it): the next window follows it on
/// the same stream and the drafter reads nothing it writes, so it overlaps the draft. Whoever reads or writes
/// the session from another stream or the host afterwards (a new request, a checkpoint, a snapshot, the prompt
@@ -128,6 +185,45 @@ public:
/// false with `err` when it failed. Free when nothing is pending.
bool wait_commit(std::string& err);
// ---- PIPELINED WINDOWS (--pipeline-windows, a layer split on two GPUs). One conversation's windows with the
// stages overlapped: stage 0 runs window K+1 while stage 1 still runs window K. The same window as `run`, driven
// without blocking the host, so one host thread keeps a window in flight on each stage (the batch pipeline's
// pattern, batch_launch / batch_poll, for one sequence with drafts). Two verifiers per stage (one per window
// parity) share the stage's stream and each has its own hand-off. `pl_launch` stages and launches (it never
// captures: `capture_all` first, with nothing in flight), `service` serves the layers whose doorbells have rung
// and returns at once, `done` polls the window's completion, `pl_finish` reads the picks, `pl_commit_async`
// queues the commit. Nothing chains to `next_`: the caller drives every stage.
/// The compute stream to use instead of a private one (the two verifiers of one stage share it). Before `init`.
void set_stream(cudaStream_t s) { ext_stream_ = s; }
/// Every layer copies the token rows to the host (doorbell_publish), not only the layers with a routed expert
/// outside the VRAM tier by the device's residency table: with windows in flight the adaptive tier marks an
/// expert evicted on the host (the pool then computes it on the CPU) before the device table follows. Also turns
/// the device-planned layers (E-6) off. Before `init`.
void set_always_publish(bool on) { always_publish_ = on; }
cudaStream_t stream() const { return cs_; }
int device() const { return device_; }
/// Capture every window size and the commit graph now (a capture syncs the stream: never with a window in flight).
bool capture_all(std::string& err);
bool pl_launch(int T, const int32_t* tokens, int64_t pos0, std::string& err);
/// Stage a window ahead of its launch (positions, and the PLE rows from `ple_prev` = the two tokens before the
/// window as they WILL be, their pages prefetched). A later `pl_launch` of the same window (T, pos0, tokens, and
/// `ss.ple_prev` equal to `ple_prev` by then) skips the staging; anything else stages again.
bool prestage(int T, const int32_t* tokens, int64_t pos0, const int32_t ple_prev[2], std::string& err);
/// 1: every layer served; 0: the GPU has not reached the next layer yet; -1: an error (`err`).
int service(PoolMultiFn pool, void* user, std::string& err);
bool in_flight() const { return fl_active_; }
/// The window's graph (and its profile copy) completed; false while it runs. An error sets `err`.
bool done(std::string& err);
/// After `done`: the profile, the last stage's host sampling and picks (`out` may be null on an earlier stage).
bool pl_finish(int32_t* out, std::string& err);
/// The commit without a host sync; `ss.ple_prev` advances now (host side). A second call for the same window
/// (after its state was restored) replays it with another count.
bool pl_commit_async(int n_keep, std::string& err);
/// Fold another verifier's counters and GPU profile into this one's (the two verifiers of one stage report once).
void absorb_stats(Verifier& o);
/// The watchdog's line for a pipelined verifier: in flight, layers served, the GPU's ring and flags, its events.
void diag_pipelined(std::FILE* f, const char* name) const;
/// Measurement hook (STRATA_LOGPOS): after run(), write one line per row t of the last window's head -
/// "pos target logprob top top_logprob hit extra_logprob target_logprob_without_extra" - where row t is the
/// distribution at pos0 + t, targets[t] is the token at pos0 + t + 1, extra_logprob is the log-probability of
@@ -161,7 +257,60 @@ public:
std::string profile_report();
private:
RemoteExpertOpt* remote_opt_ = nullptr;
bool capture(int T, std::string& err);
// batch windows (see init_slots)
std::vector<SessionState*> slots_;
bool batch_rec_ = false; ///< record_window is capturing a batch window
int row_base_ = 0; ///< ... its hand-off rows start here (a pipeline group's own rows)
int brow_[8] = {}; ///< ... and row t is slot brow_[t]
bool last_batch_ = false; ///< the last run was a batch window (set_plan_slot: one group)
std::map<std::vector<int>, cudaGraphExec_t> exec_bm_, commit_bm_; ///< full row layout avoids slot-ID collisions
std::map<std::vector<int>, uint64_t> bm_used_; ///< last use of each captured layout (LRU, only with a graph limit)
uint64_t bm_tick_ = 0;
size_t batch_graph_limit_ = 0; ///< 0: keep every captured batch graph (0.1.39); N: LRU-evict beyond N layouts
int last_rows_[8] = {}; ///< the slots of the last batch window's rows
std::vector<int> bkey(const int* rows, int S, int hbase) const { // #871: the doorbell variant has its own graphs
std::vector<int> k = batch_key(rows, S, hbase);
k.push_back(ar_off_ ? 1 : 0);
return k;
}
static std::vector<int> batch_key(const int* rows, int S, int hbase) {
std::vector<int> k;
k.reserve((size_t) S + 1);
k.push_back(hbase);
for (int t = 0; t < S; ++t) k.push_back(rows[t]);
return k;
}
// batch_launch / batch_poll
bool b_running_ = false;
int64_t b_k_ = 0, b_steps_ = 0;
std::chrono::steady_clock::time_point b_last_;
int32_t b_out_[8] = {};
std::vector<strata::kernels::SamplerParams> slot_sp_;
bool sample_rows(int S, std::string& err); ///< the sampled slots' rows of the last batch window
int32_t* h_commitb_ = nullptr; int32_t* m_commitb_ = nullptr; // per slot [1, 0, pos, -1 ..], stride 2 + max_t
int32_t* commitb_ = nullptr;
float* tail_snap_b_ = nullptr; ///< per (slot, QSA layer) indexer tail snapshot
void* arena_b_ = nullptr;
int64_t last_pos_b_[8] = {};
bool capture_batch(const int* rows, int S, int hbase, std::string& err);
void collect_profile(); ///< STRATA_VERIFY_PROFILE: add the last window's stamps to prof_sum_
void accumulate_profile(const unsigned long long* stamps); ///< one window's stamps (host copy) into prof_sum_
// pipelined windows (pl_launch ...)
cudaStream_t ext_stream_ = nullptr; ///< set_stream: the stage's shared stream (not destroyed here)
bool always_publish_ = false;
void pl_stage(int T, const int32_t* tokens, int64_t pos0, const int32_t ple_prev[2]);
cudaEvent_t ev_done_ = nullptr, ev_commit_ = nullptr;
unsigned long long* prof_pin_ = nullptr; ///< pinned host copy of the stamps (pipelined windows)
bool fl_active_ = false, fl_prof_ = false, fl_ple_ = false, commit_live_ = false, pl_prestaged_ = false;
int fl_T_ = 0;
int64_t fl_k_ = 0, fl_total_ = 0;
double fl_since_ms_ = 0, fl_flush_ms_ = 0, fl_launch_ms_ = 0;
int32_t pl_prev_[2] = {-1, -1};
std::vector<uint32_t> pl_ple_rows_; ///< the window's PLE rows (T x PLE_N_HEADS), gathered when layer 0 is served
bool capture_commit_batch(const int* rows, int S, int hbase, std::string& err);
bool stage_batch(const int* rows, int S, int hbase, const int32_t* tokens, const int64_t* pos, std::string& err);
strata::kernels::SamplerParams sampling_ = [] {
strata::kernels::SamplerParams s;
s.greedy = true;
@@ -173,8 +322,19 @@ private:
bool head_sampling_ = true; ///< set_head_sampling
int device_ = -1; ///< the device `init` ran on: run/commit switch to it (layer split)
std::atomic<bool> released_{false}; ///< #267: release_gpu_waits ran (maybe on the watchdog thread): no more windows
bool all_resident_ = false; ///< 100% of experts in [lb_, le_) resident in VRAM: zero-doorbell graph
/// #871: the zero-doorbell graph plans from the device residency table alone, so it is only right while every
/// expert of the stage is in VRAM. A prompt loan, a VRAM shrink or an adaptive swap marks some -1 for a while:
/// every window then runs the doorbell graph (exec_nr_ / the batch key's top bit), the same as a stage that never
/// was all-resident. refresh_ar() looks at the host table before each window.
bool ar_off_ = false;
bool ar_on() const { return all_resident_ && !ar_off_; }
void refresh_ar();
uint32_t* h_plan_err_ = nullptr; uint32_t* m_plan_err_ = nullptr; // set by resident_plan on a -1 (all-resident graph)
const int32_t* h_res_ = nullptr; ///< the host residency table (VerifyHits::h_res)
bool device_plan_ = false; ///< E-6: resident-only layers planned on the device (STRATA_VERIFY_DEVICE_PLAN)
uint32_t* skip_ = nullptr; ///< E-6: per group, the ring whose plan the device built (0: the host's)
unsigned* qcnt_ = nullptr; ///< S26 STRATA_QFUSE: the HC read's q8_1 group counters (n_embd / 32)
unsigned long long* slot_off_d_ = nullptr; ///< E-6: the slot offsets on the device
int64_t lb_ = 0, le_ = -1; ///< set_stage: the layers this verifier runs (-1: to the last)
const float* hand_in_ = nullptr;
@@ -182,8 +342,17 @@ private:
Verifier* next_ = nullptr;
void* next_user_ = nullptr;
bool ple_stage() const { return lb_ <= 1 && 1 < le_; } ///< holds layer 1, where the PLE block runs
void stage_inputs(int T, const int32_t* tokens, int64_t pos0);
bool staged_ = false;
bool copy_used_ = false;
bool capture_commit(std::string& err);
bool record_window(int T, cudaStream_t cs, std::string& err);
// #649: STRATA_VERIFY_TRACE=1 - a host event ring (trace_ev) and GPU breadcrumbs: the profiler's stamp points,
// per layer and token group, written to mapped memory (null when the trace is off)
void trace_ev(const char* what, int64_t step, int64_t layer, int64_t aux) const;
unsigned long long* trace_h_ = nullptr;
unsigned long long* trace_m_ = nullptr;
size_t trace_n_ = 0;
static constexpr int kProfPer = 33; // stamps per layer (32 left the hc-read second
// half's up-stamp at slot 32 = the next layer's slot 0: D8)
bool prof_on_ = false;
@@ -198,12 +367,17 @@ private:
VerifyHits hits_;
const NativeHead* head_ = nullptr;
int max_t_ = 0;
float* ple_key_ = nullptr; ///< STRATA_PLE_BATCH: the window rows' PLE key / value projections
float* ple_val_ = nullptr;
int last_t_ = 0;
int64_t last_pos0_ = 0;
int32_t last_tokens_[8] = {};
int64_t n_vocab_ = 0;
cudaStream_t cs_ = nullptr;
cudaStream_t sh_cs_ = nullptr;
cudaEvent_t ev_fork_ = nullptr, ev_join_ = nullptr;
cudaGraphExec_t exec_[9] = {};
cudaGraphExec_t exec_nr_[9] = {}; // #871: the doorbell variant of a stage that is all-resident otherwise
cudaGraphExec_t commit_exec_ = nullptr;
// mapped staging (host pointer, device alias)
@@ -243,6 +417,7 @@ private:
float *ple_ = nullptr, *emb_ = nullptr, *R_ = nullptr, *mixed_ = nullptr, *bo_ = nullptr;
float *inj_ = nullptr, *inj2_ = nullptr, *lo_ = nullptr, *rs_ = nullptr, *xn_ = nullptr;
uint8_t* xq_ = nullptr; // T columns of q8_1
uint8_t* sh_xq_ = nullptr; // T columns of q8_1 for shared expert branch
float *qkv_L_ = nullptr, *h_L_ = nullptr, *gate_L_ = nullptr, *beta_L_ = nullptr; // per GDN layer
float *z_ = nullptr, *y_ = nullptr, *y_dummy_ = nullptr;
float *qfull_ = nullptr, *qcur_ = nullptr, *kcur_ = nullptr, *vcur_ = nullptr, *idx_raw_L_ = nullptr;
@@ -261,6 +436,8 @@ private:
void* hit_scratch_ = nullptr;
float *head_mixed_ = nullptr, *head_inj_ = nullptr, *head_logits_ = nullptr;
uint16_t* sh_bf16_ = nullptr;
uint8_t* arg_scratch_ = nullptr; ///< argmax_rows' partials and counters
int32_t* one_ = nullptr; ///< device {1}: the n_keep of a one-token window, which commits itself
float *sh_gate_ = nullptr, *sh_up_ = nullptr, *sh_g_ = nullptr;
float* hist_snap_ = nullptr; // T * NG_HIST * NG_HC_DIM
int64_t cap_ = 0, max_blocks_ = 0, attn_scratch_floats_ = 0;
+76
View File
@@ -0,0 +1,76 @@
#pragma once
// CUDA virtual memory (the elastic K/V, --kv-grow): an address range reserved once, whose physical memory is mapped and
// unmapped in chunks of the device's granularity (2 MiB). The K/V pools grow with the context by taking chunks the
// expert cache gives up, and neither moves: every captured graph and stored pointer stays valid. The driver calls
// are reached through the runtime's entry-point query, so nothing links the driver library. Not under HIP.
#include <cstdint>
#include <vector>
namespace strata::core {
/// A physical chunk: the driver's allocation handle (CUmemGenericAllocationHandle); 0 = none.
using VmmChunk = unsigned long long;
/// The current device supports it and the entry points resolved.
bool vmm_available();
/// The chunk size (0 when not available).
uint64_t vmm_granularity();
/// A new physical chunk on the current device (0: out of memory), and its release.
VmmChunk vmm_chunk_new();
void vmm_chunk_free(VmmChunk h);
class VmmRange {
public:
VmmRange() = default;
~VmmRange() { release(); }
VmmRange(const VmmRange&) = delete;
VmmRange& operator=(const VmmRange&) = delete;
/// Reserves `bytes` (rounded up to whole chunks) of addresses, nothing mapped.
bool reserve(uint64_t bytes);
/// Unmaps and frees every chunk, then the addresses.
void release();
uint8_t* base() const { return (uint8_t*) (uintptr_t) base_; }
int64_t chunks() const { return (int64_t) h_.size(); }
bool mapped(int64_t i) const { return i >= 0 && i < chunks() && h_[(size_t) i] != 0; }
int64_t mapped_count() const;
/// Maps chunks [lo, hi) that are not mapped yet, each to `take()` or, when that returns 0, to a new chunk.
/// Readable and writable when it returns; false: out of memory (what was mapped stays mapped).
template <class Take> bool map_range(int64_t lo, int64_t hi, Take take);
/// Unmaps chunk i and hands back its physical chunk (0 when it was not mapped).
VmmChunk unmap(int64_t i);
private:
bool map_one(int64_t i, VmmChunk h);
bool set_access(int64_t lo, int64_t hi);
/// Readable and writable [lo, hi) (all newly mapped); when that fails they are unmapped and freed again, so no
/// chunk counts as mapped without access.
bool commit_run(int64_t lo, int64_t hi);
unsigned long long base_ = 0;
std::vector<VmmChunk> h_; // per chunk: its physical chunk, 0 = unmapped
};
template <class Take> bool VmmRange::map_range(int64_t lo, int64_t hi, Take take) {
lo = lo < 0 ? 0 : lo;
hi = hi > chunks() ? chunks() : hi;
int64_t run = -1; // access is set once per run of newly mapped chunks (it costs as much as the map)
for (int64_t i = lo; i < hi; ++i) {
if (mapped(i)) {
if (run >= 0 && !commit_run(run, i)) return false;
run = -1;
continue;
}
VmmChunk h = take();
if (h == 0) h = vmm_chunk_new();
if (h == 0 || !map_one(i, h)) {
if (h != 0) vmm_chunk_free(h);
if (run >= 0) commit_run(run, i);
return false;
}
if (run < 0) run = i;
}
return run < 0 || commit_run(run, hi);
}
} // namespace strata::core
+3
View File
@@ -90,6 +90,9 @@ struct WeightRef {
const void* native_data = nullptr;
void* native_q8_1 = nullptr;
int native_type = -1;
/// S23 experiment (STRATA_HC_Q8=1): a hyper-connection projection's Q8_0 bytes as the GGUF stores them, owned by
/// NativeDense, for the verify window's read (every other path keeps the pack's BF16 `data`)
const void* hc_q8 = nullptr;
/// Plan v0.3 P1: false when the loader SKIPPED this tensor's canonical bytes because another form serves it
/// (native GGUF projections, the native head). The metadata above stays valid; `data` is null.
bool resident = true;
+41
View File
@@ -1,5 +1,45 @@
#pragma once
#include <hipblas/hipblas.h>
#include <hipblas/hipblas-version.h>
#if hipblasVersionMajor == 0
// Ubuntu's hipBLAS 0.54 has no workspace API or compute-type enum. Use its
// underlying rocBLAS API directly, including ownership of the real handle,
// so the engine's explicit workspace contract is preserved.
#include <rocblas/rocblas.h>
#define CUBLAS_COMPUTE_32F rocblas_datatype_f32_r
#define CUBLAS_DEFAULT_MATH 0
#define CUBLAS_GEMM_DEFAULT rocblas_gemm_algo_standard
#define CUBLAS_OP_N rocblas_operation_none
#define CUBLAS_OP_T rocblas_operation_transpose
#define CUBLAS_STATUS_SUCCESS rocblas_status_success
#define CUDA_R_16BF rocblas_datatype_bf16_r
#define CUDA_R_16F rocblas_datatype_f16_r
#define CUDA_R_32F rocblas_datatype_f32_r
#define cublasCreate rocblas_create_handle
#define cublasDestroy rocblas_destroy_handle
#define cublasHandle_t rocblas_handle
#define cublasSetStream rocblas_set_stream
#define cublasSetWorkspace rocblas_set_workspace
#define cublasStatus_t rocblas_status
inline rocblas_status cublasSetMathMode(rocblas_handle h, int mode) {
// This API has only its default arithmetic; GEMM below explicitly selects
// FP32 accumulation. Reject any request for a different math mode.
if (!h) return rocblas_status_invalid_handle;
return mode == CUBLAS_DEFAULT_MATH ? rocblas_status_success : rocblas_status_invalid_value;
}
inline rocblas_status cublasGemmEx(rocblas_handle h, rocblas_operation trans_a,
rocblas_operation trans_b, int m, int n, int k, const void* alpha,
const void* a, rocblas_datatype a_type, int lda,
const void* b, rocblas_datatype b_type, int ldb, const void* beta,
void* c, rocblas_datatype c_type, int ldc,
rocblas_datatype compute_type, rocblas_gemm_algo algo) {
return rocblas_gemm_ex(h, trans_a, trans_b, m, n, k, alpha,
a, a_type, lda, b, b_type, ldb, beta, c, c_type, ldc,
c, c_type, ldc, compute_type, algo, 0, 0);
}
#else
#define CUBLAS_COMPUTE_32F HIPBLAS_COMPUTE_32F
#define CUBLAS_DEFAULT_MATH HIPBLAS_DEFAULT_MATH
#define CUBLAS_GEMM_DEFAULT HIPBLAS_GEMM_DEFAULT
@@ -17,3 +57,4 @@
#define cublasSetStream hipblasSetStream
#define cublasSetWorkspace hipblasSetWorkspace
#define cublasStatus_t hipblasStatus_t
#endif
+14
View File
@@ -10,6 +10,7 @@
#endif
#endif
#include <hip/hip_runtime.h>
#include <hip/hip_version.h>
// Do not let HIP's legacy macro corrupt libstdc++ attribute names.
#ifdef __noinline__
#undef __noinline__
@@ -19,6 +20,7 @@
#define cudaDevAttrClockRate hipDeviceAttributeClockRate
#define cudaDevAttrComputeCapabilityMajor hipDeviceAttributeComputeCapabilityMajor
#define cudaDevAttrComputeCapabilityMinor hipDeviceAttributeComputeCapabilityMinor
#define cudaDevAttrIntegrated hipDeviceAttributeIntegrated
#define cudaDeviceGetAttribute hipDeviceGetAttribute
#define cudaDeviceCanAccessPeer hipDeviceCanAccessPeer
#define cudaDeviceEnablePeerAccess hipDeviceEnablePeerAccess
@@ -39,8 +41,13 @@
#define cudaEventSynchronize hipEventSynchronize
#define cudaEvent_t hipEvent_t
#define cudaFree hipFree
#if HIP_VERSION_MAJOR < 7
#define cudaFreeHost hipHostFree
#else
#define cudaFreeHost hipFreeHost
#endif
#define cudaFuncAttributeMaxDynamicSharedMemorySize hipFuncAttributeMaxDynamicSharedMemorySize
#define cudaFuncAttributePreferredSharedMemoryCarveout hipFuncAttributePreferredSharedMemoryCarveout
#define cudaGetDevice hipGetDevice
#define cudaGetDeviceCount hipGetDeviceCount
#define cudaGetDeviceProperties hipGetDeviceProperties
@@ -54,13 +61,20 @@
#define cudaGraphUpload hipGraphUpload
#define cudaGraph_t hipGraph_t
#define cudaHostAlloc hipHostMalloc
#if HIP_VERSION_MAJOR < 7
#define cudaHostAllocDefault hipHostMallocDefault
#define cudaHostAllocMapped hipHostMallocMapped
#define cudaHostAllocPortable hipHostMallocPortable
#else
#define cudaHostAllocDefault hipHostAllocDefault
#define cudaHostAllocMapped hipHostAllocMapped
#define cudaHostAllocPortable hipHostAllocPortable
#endif
#define cudaHostGetDevicePointer hipHostGetDevicePointer
#define cudaHostRegister hipHostRegister
#define cudaHostRegisterMapped hipHostRegisterMapped
#define cudaHostRegisterPortable hipHostRegisterPortable
#define cudaHostRegisterReadOnly hipHostRegisterReadOnly
#define cudaHostUnregister hipHostUnregister
#define cudaLaunchHostFunc hipLaunchHostFunc
#define cudaMalloc hipMalloc
+33 -2
View File
@@ -1,8 +1,9 @@
#pragma once
// CUDA device intrinsics used by Strata kernels that HIP does not provide on RDNA2 / RDNA3 / RDNA4 (wave32).
// CUDA device intrinsics used by Strata kernels on supported wave32 HIP targets.
// This header is included only from the HIP cuda_runtime compatibility shim.
#if defined(__HIPCC__)
#include <hip/hip_version.h>
#include <cstdint>
@@ -15,7 +16,8 @@ __device__ __forceinline__ int signed_byte(uint32_t word, int lane) {
// CUDA's signed __dp4a: four signed byte products accumulated modulo 2^32.
__device__ __forceinline__ int dp4a(int a, int b, int c) {
#if (defined(__gfx1100__) || defined(__gfx1101__) || defined(__gfx1102__) || defined(__gfx1200__) || \
#if (defined(__gfx1100__) || defined(__gfx1101__) || defined(__gfx1102__) || defined(__gfx1150__) || \
defined(__gfx1151__) || defined(__gfx1200__) || \
defined(__gfx1201__)) && __has_builtin(__builtin_amdgcn_sudot4)
// RDNA3 and RDNA4 expose the signed/unsigned dot4 form (v_dot4_i32_iu8). Mark
// both packed operands signed to preserve CUDA __dp4a semantics; keep the
@@ -25,6 +27,21 @@ __device__ __forceinline__ int dp4a(int a, int b, int c) {
// RDNA2 has no sudot4 (that is gfx11+), but it has the plain signed v_dot4_i32_i8 (dot1-insts): the same
// signed x signed byte products accumulated modulo 2^32, no clamp.
return __builtin_amdgcn_sdot4(a, b, c, false);
#elif defined(__gfx1012__) && !defined(STRATA_GFX1012_PORTABLE_DOT)
// RDNA1 has no native signed dot4. SDWA selects and sign-extends byte
// operands directly, avoiding separate shift/mask/sign-extension work.
// Adapted from pinned llama.cpp ggml-cuda/common.cuh (MIT; see
// third_party/ggml/LICENSE), same modulo-2^32 accumulation contract.
int lo, hi;
asm("v_mul_i32_i24 %1, sext(%3), sext(%4) dst_sel:DWORD dst_unused:UNUSED_PAD src0_sel:BYTE_0 src1_sel:BYTE_0\n"
"v_mul_i32_i24 %2, sext(%3), sext(%4) dst_sel:DWORD dst_unused:UNUSED_PAD src0_sel:BYTE_1 src1_sel:BYTE_1\n"
"v_add3_u32 %0, %1, %2, %0\n"
"v_mul_i32_i24 %1, sext(%3), sext(%4) dst_sel:DWORD dst_unused:UNUSED_PAD src0_sel:BYTE_2 src1_sel:BYTE_2\n"
"v_mul_i32_i24 %2, sext(%3), sext(%4) dst_sel:DWORD dst_unused:UNUSED_PAD src0_sel:BYTE_3 src1_sel:BYTE_3\n"
"v_add3_u32 %0, %1, %2, %0\n"
// c changes before the high-byte reads; it must not alias a or b.
: "+&v"(c), "=&v"(lo), "=&v"(hi) : "v"(a), "v"(b));
return c;
#else
const uint32_t ua = static_cast<uint32_t>(a);
const uint32_t ub = static_cast<uint32_t>(b);
@@ -84,6 +101,17 @@ __device__ __forceinline__ void require_full_wave_mask(uint32_t mask) {
if (mask != 0xffffffffu) __builtin_trap();
}
// Older HIP has no __syncwarp. A wave barrier alone does not order memory.
// Release/acquire fences cover the shared-memory exchange used by attention.
#if HIP_VERSION_MAJOR < 7
__device__ __forceinline__ void syncwarp(uint32_t mask = 0xffffffffu) {
require_full_wave_mask(mask);
__builtin_amdgcn_fence(__ATOMIC_RELEASE, "workgroup");
__builtin_amdgcn_wave_barrier();
__builtin_amdgcn_fence(__ATOMIC_ACQUIRE, "workgroup");
}
#endif
template <typename T>
__device__ __forceinline__ T shfl_xor_sync(uint32_t mask, T value, int lane_mask, int width = 32) {
require_full_wave_mask(mask);
@@ -125,6 +153,9 @@ __device__ __forceinline__ unsigned ballot_sync(uint32_t mask, int predicate) {
#define __shfl_up_sync(...) (::strata::hip_compat::shfl_up_sync(__VA_ARGS__))
#define __shfl_sync(...) (::strata::hip_compat::shfl_sync(__VA_ARGS__))
#define __ballot_sync(mask, predicate) (::strata::hip_compat::ballot_sync((mask), (predicate)))
#if HIP_VERSION_MAJOR < 7
#define __syncwarp(...) (::strata::hip_compat::syncwarp(__VA_ARGS__))
#endif
// AMD's sleep instruction accepts only 0..15; the synchronization loops use it as a
// backoff hint, so use its smallest portable delay independently of CUDA cycle counts.
#define __nanosleep(cycles) __builtin_amdgcn_s_sleep(1)
@@ -0,0 +1,4 @@
#pragma once
// CUDA spelling used by the attention kernels; included only in HIP builds.
#include <hip/hip_math_constants.h>
#define CUDART_INF_F HIP_INF_F
+10
View File
@@ -44,9 +44,19 @@ void bf16_gemv_split(const uint16_t* x, const uint16_t* w, float* y, int64_t n_i
/// No allocations or synchronization, including when stream is null (the CUDA default stream).
void bf16_gemv_fp32_mmvf(const float* x, const uint16_t* w, float* y,
int64_t n_in, int64_t n_out, void* stream);
/// `ncols` columns at once: x is [ncols][n_in], y is [ncols][n_out]; each column bitwise equal to a
/// `bf16_gemv_fp32_mmvf` call on it.
void bf16_gemv_fp32_mmvf_cols(const float* x, const uint16_t* w, float* y, int64_t n_in, int64_t n_out, int ncols,
void* stream);
/// bf16_gemv_fp32_mmvf for n_tok (1..8) activation rows x[t * ldx], outputs y[t * ldy + j]; one launch,
/// the weight read once, every output bit-identical to its own single-row call.
void bf16_gemv_fp32_mmvf_multi(const float* x, int64_t ldx, const uint16_t* w, float* y, int64_t ldy,
int64_t n_in, int64_t n_out, int n_tok, void* stream);
/// S26 STRATA_LFUSE: bf16_gemv_fp32_mmvf_multi(x, ldx, w, y, ldy, n_in, n_out, n_tok) plus the 1-row
/// bf16_gemv_fp32_mmvf_multi(x, ldx, w_aux, y_aux, ldy_aux, n_in, 1, n_tok) in ONE launch, every output bitwise the
/// two calls'. Returns false (nothing launched) unless the rows path applies (STRATA_MMVF_ROWS=1, n_out >= 64, 2-8 rows).
bool bf16_gemv_fp32_mmvf_multi_aux(const float* x, int64_t ldx, const uint16_t* w, float* y, int64_t ldy,
int64_t n_in, int64_t n_out, int n_tok, const uint16_t* w_aux, float* y_aux,
int64_t ldy_aux, void* stream);
} // namespace strata::kernels
+16
View File
@@ -67,6 +67,14 @@ struct ActQ {
int32_t sum[MAXC];
float hx[MAXC];
int nchunks;
/// The AVX-2 "bit-plane" image of `q` (src/kernels/cpu/q2_avx2.cpp explains it), filled only by
/// `act_quant_q8_1_avx2` with STRATA_Q2_BITPLANE=1 and read only by `q2_0_gguf_rows_multi_avx2`. Per pair p of 64-value blocks:
/// `qp[128p + 32k + 16h + i] = q[128p + 64h + 4i + k]`, `psum[8p + 4h + m]` = the sum of q[128p + 64h + 16m ..
/// + 16), `pscale[8p + 2c + e] = scale[4p + c]`. `bp_pairs` is 0 when the image is absent (n % 128 != 0).
alignas(32) int8_t qp[H];
alignas(32) int32_t psum[H / 16];
alignas(32) float pscale[H / 16];
int bp_pairs = 0;
};
/// Per-worker scratch. Owned by the caller and passed in, so the token path performs NO allocations
@@ -172,6 +180,14 @@ void q2_0_gguf_rows_multi(const uint8_t* w, size_t row_bytes, int nblocks, const
/// The same two for CPUs without AVX-512 (src/kernels/cpu/q2_avx2.cpp, compiled for AVX2 only).
void q2_0_gguf_rows_multi_avx2(const uint8_t* w, size_t row_bytes, int nblocks, const ActQ* const* a, int nt,
float* const* out, int r0, int r1);
/// STRATA_Q2_BITPLANE=1 (read once): the AVX-2 Q2_0 kernel reads the bit-plane image instead of the legacy one.
bool q2_bitplane_enabled();
/// The pre-bit-plane AVX-2 kernel (the default), kept for A/B and parity.
void q2_0_gguf_rows_multi_avx2_legacy(const uint8_t* w, size_t row_bytes, int nblocks, const ActQ* const* a, int nt,
float* const* out, int r0, int r1);
/// The same rows with AVX-VNNI on or off, not as cpu_avxvnni_ok() says (tests and benches; on only where it holds).
void q2_0_gguf_rows_multi_avx2_v(bool vnni, const uint8_t* w, size_t row_bytes, int nblocks, const ActQ* const* a,
int nt, float* const* out, int r0, int r1);
void act_quant_q8_1_avx2(const float* x, int n, ActQ& a);
void s2_expert_scalar(const uint8_t* blob, const float* x, float* out, bool quant_acts);
@@ -44,9 +44,41 @@ struct ExpertLayout {
/// Plan v0.3 P6: whether this CPU (and its OS) runs the AVX-512 kernels (F, BW, VL, VNNI, VBMI). Probed in a
/// file compiled without AVX-512, so asking is safe everywhere; STRATA_FORCE_AVX2=1 answers no (for tests).
bool cpu_avx512_ok();
/// Whether this CPU runs AVX-512F + AVX-512BW + AVX-512VL + AVX-512DQ (e.g. Skylake-X i9-9920X) for iq_avx512.cpp.
bool cpu_avx512bw_ok();
/// Whether this CPU (and its OS) runs the AVX2 kernels (AVX, AVX2, FMA, F16C): the floor of every expert kernel
/// (q2_avx2.cpp, iq_avx2.cpp, and ggml-cpu in the portable build). STRATA_FORCE_AVX2 does not change it.
bool cpu_avx2_ok();
/// Whether this CPU (and its OS) runs AVX (Sandy Bridge and newer): the AVX1 router dot (kq_avx1.cpp) and ggml-cpu in
/// a STRATA_ISA_FLOOR=avx build. The other expert rows then run on ggml-cpu (experimental, slow).
bool cpu_avx1_ok();
/// SSE4.2 + POPCNT: ggml-cpu in a STRATA_ISA_FLOOR=none build.
bool cpu_sse42_ok();
/// STRATA_FORCE_ISA=avx2|avx|sse caps the probes above (3 = no cap, 2 = AVX2, 1 = AVX, 0 = SSE4.2), so the older-CPU
/// paths run on any PC: tests and the experimental builds' checks. ggml-cpu keeps what it was compiled for.
int cpu_isa_cap();
/// The experimental older-CPU build this engine is ("avx", "sse4.2"), or "" for the normal build (AVX2 floor).
const char* isa_floor_build();
/// STRATA_IQ256_GATHER, the AVX-2 i-quant kernels' gathered grid decode (iq_avx2.cpp): -1 unset (auto, see
/// cpu_gather_fast_here), 0 the scalar decode on every core, 1 the gathered one on every core.
int iq256_gather_setting();
/// Whether this CPU's performance cores gather the IQ grid entries faster than they assemble them from scalar loads:
/// an Intel CPU with AVX-VNNI, i.e. Alder Lake / Sapphire Rapids or newer, and not one of the E-core-only parts.
/// The older Intel cores gather slowly (Haswell, Broadwell) or under the Downfall (GDS) microcode (Skylake to Tiger
/// Lake), AMD Zen 2/3 gather slowly, and Zen 4/5 run the AVX-512 kernels. STRATA_FORCE_ISA answers no.
bool cpu_gather_fast();
/// cpu_gather_fast() and the CALLING THREAD runs on a performance core: CPUID 1Ah core type 40h on a hybrid CPU (its
/// E-cores, 20h, gather slower than they assemble). Probed once per thread: the pool pins each worker to one core.
bool cpu_gather_fast_here();
/// Whether the AVX-2 expert kernels take their AVX-VNNI forms (vpdpwssd / vpdpbusd: Alder Lake, Sapphire Rapids and
/// later, P- and E-cores alike; the same integer sums): cpu_avx2_ok() and CPUID 7.1:EAX[4], not capped by
/// STRATA_FORCE_ISA, and not an older-CPU build (STRATA_ISA_FLOOR, which stays on its floor and AVX2).
/// STRATA_NO_AVXVNNI=1 answers no.
bool cpu_avxvnni_ok();
/// Whether a native pack's layer of this ggml type runs on Strata's own Q2_0 kernels (AVX2 / AVX-512). On a
/// CPU without AVX2 (a native, non-portable build: ggml-cpu compiled for this CPU's SSE) a Q2_0 layer takes
/// ggml-cpu's own Q2_0 vec_dot like every other native type: slower, but it runs.
inline bool q2_native_kernels(int type) { return type == 42 && cpu_avx2_ok(); }
/// The CPU's brand string (CPUID 0x80000002..4), for messages; "unknown" when it has none.
std::string cpu_name();
/// Q2_0 GGUF rows / activation quantizer on the kernels this CPU has.
+18
View File
@@ -18,9 +18,27 @@ void iq256_gu_rows(int ggml_type, const uint8_t* blob, size_t gu_row, size_t up_
void iq256_rows(int ggml_type, const uint8_t* w, size_t row_bytes, int n, const void* const* act, int nt,
float* const* out, int r0, int r1);
/// The kernels' variants, a mask. Every variant computes the same bits; they differ in speed from core to core.
/// kIq256Gather: IQ3_XXS, IQ3_S and IQ2_S read their grid entries with one gather (STRATA_IQ256_GATHER).
/// kIq256Vnni: AVX-VNNI's vpdpwssd / vpdpbusd, every format and iq4nl256_down_rows (cpu_avxvnni_ok).
inline constexpr int kIq256Gather = 1;
inline constexpr int kIq256Vnni = 2;
/// The variant iq256_gu_rows / iq256_rows take on the calling thread.
int iq256_variant() noexcept;
/// Every variant bit this build and this CPU can run (tests and benches).
int iq256_variants() noexcept;
/// iq256_gu_rows / iq256_rows in a given variant (tests and benches; only bits of iq256_variants()).
void iq256_gu_rows_v(int variant, int ggml_type, const uint8_t* blob, size_t gu_row, size_t up_off, int n,
const void* const* act, int nt, float* const* ff, int r0, int r1);
void iq256_rows_v(int variant, int ggml_type, const uint8_t* w, size_t row_bytes, int n, const void* const* act,
int nt, float* const* out, int r0, int r1);
/// out[t][r] = w_r . h[t] for IQ4_NL (type 20) rows against Q8_0 activations (ggml's block_q8_0).
/// IQ4_NL is a 32-value-block format, so this does not go through iq256_rows (QK_K blocks, Q8_K acts).
void iq4nl256_down_rows(const uint8_t* w, size_t row_bytes, int n, const void* const* hq, int nt,
float* const* out, int r0, int r1);
/// The same in a given variant (only the kIq256Vnni bit matters here).
void iq4nl256_down_rows_v(int variant, const uint8_t* w, size_t row_bytes, int n, const void* const* hq, int nt,
float* const* out, int r0, int r1);
} // namespace strata::kernels::cpu
+18
View File
@@ -0,0 +1,18 @@
// include/strata/kernels/cpu/kq_avx1.hpp - the AVX1 router dot for older CPUs (from the Strata_Dirigo fork).
//
// bf16_rows_dot_multi (kq_avx2.cpp) is compiled with -mavx2 and uses instructions an AVX-only CPU does
// not have (`vpmovzxwd`, `vfmadd`), so an AVX-only host cannot call it at all. This is the same
// arithmetic on AVX1 instructions: 8 float lanes per token, one weight stream per row shared by all
// `nt` tokens, one horizontal sum per (row, token). AVX1 has no FMA, so the multiply and the add stay
// separate; it has no 256-bit integer ops, so the bf16 -> f32 widening runs in 128-bit halves.
#pragma once
#include <cstdint>
namespace strata::kernels::cpu {
/// Mirrors bf16_rows_dot_multi's arithmetic (same accumulation shape, mul+add instead of FMA).
/// Requires cols % 8 == 0 and nt <= 8, exactly like the AVX2 kernel it stands in for.
void bf16_rows_dot_multi_avx1(const uint16_t* w, int rows, int cols, const float* x, int nt, float* out);
} // namespace strata::kernels::cpu
@@ -39,6 +39,9 @@ void native_quant_act(const NativeFmt& f, const float* x, void* dst);
/// h (n_ff floats) -> the down activation (h_bytes).
void native_quant_h(const NativeFmt& f, const float* h, void* dst);
/// From how many tokens native_gu_rows gives this gate/up type to a multi-token kernel (#152; ggml-cpu's per-token dot
/// below that). 1: a token's rows are the same alone and in any group.
int native_gu_mt_min(int gu_type);
/// ff[t][r] = silu(gate_r . a[t]) * (up_r . a[t]) for rows r in [r0, r1), `nt` tokens.
void native_gu_rows(const NativeFmt& f, const uint8_t* blob, const void* const* act, int nt, float* const* ff,
int r0, int r1);
+31 -4
View File
@@ -76,12 +76,25 @@ struct CpuTopology {
int p_cores = 0; ///< Physical performance cores
int p_threads = 0; ///< Total logical threads on performance cores
int e_cores = 0; ///< Efficient cores
std::vector<int> worker_cores; ///< Ordered logical core IDs for workers (excluding host if skip_first)
std::vector<int> worker_cores; ///< Ordered CPU IDs; on Windows, group * 64 + processor within the group
int host_core = -1; ///< Logical core reserved for host thread
};
/// Which core the host thread - the layer loop that spins on the GPU's flags - takes when `skip_first` reserves one
/// (--host-core first|last, STRATA_HOST_CORE). `First` is the layout the pool has always used. `Last` (eddoursul's
/// fork, F12) puts the host on the last physical core and lets the workers have the first: Windows sends a GPU's
/// interrupts to one logical processor, usually the first, and every copy that lands raises one (~13 us in the ISR
/// and a DPC), which delays a spinning host there. The placement changes no result, only where threads run. A hybrid
/// CPU keeps `First` (its first core is the best P-core).
enum class HostCore { First, Last };
void set_host_core(HostCore where);
HostCore host_core_setting();
CpuTopology detect_cpu_topology(bool skip_first, PoolAffinity affinity = PoolAffinity::All);
/// The logical processor the host thread is pinned to under the current setting (-1: no topology).
int planned_host_core(PoolAffinity affinity = PoolAffinity::All);
/// One logical processor per PHYSICAL core, so a worker is never scheduled onto an SMT sibling of another
/// worker. On the 6-core/12-thread machine this project measures on, `hardware_concurrency()/2` workers on
/// logical processors 0..5 would put every worker on a sibling pair and halve the useful bandwidth - which is
@@ -100,9 +113,23 @@ std::vector<int> physical_cores(bool skip_first, PoolAffinity affinity = PoolAff
/// - exactly 5/6 of L9's 44.14 on 6 - and at **26.9 GB/s inside the host loop**, where the unpinned spinning
/// host is free to land on a worker's core or its SMT sibling. That 1.35x is not the kernel.
///
/// Returns the PREVIOUS affinity mask, or -1 if the platform refused; pass it to `restore_thread_affinity`.
long long pin_current_thread(int core);
void restore_thread_affinity(long long previous);
/// The previous host placement. `valid` is false when querying or setting placement failed.
/// Windows uses a reversible CPU Set selection, leaving hard affinity (including Windows 11's implicit
/// all-group eligibility) untouched. An empty selection restores inheritance from the process defaults.
/// Linux stores the complete, dynamically sized native CPU mask, including CPU IDs above 63.
struct ThreadAffinity {
#if defined(_WIN32)
std::vector<unsigned long> cpu_sets;
#else
std::vector<unsigned long> mask;
#endif
bool valid = false;
};
/// Selects one encoded processor for the caller; Windows CPU Sets respect existing hard-affinity limits.
/// Restore on the same calling thread. Pool-owned workers use hard group affinity instead.
ThreadAffinity pin_current_thread(int core);
void restore_thread_affinity(const ThreadAffinity& previous);
class ExpertPool {
public:
+4
View File
@@ -52,6 +52,10 @@ bool cvec_replicate(std::string& err);
void cvec_set_enabled(bool on);
bool cvec_enabled();
/// The CURRENT device's tables (dir n_layers x n_embd, s n_layers, the request flag), for a caller that applies the
/// vector inside its own kernel (the prompt path's STRATA_CVEC_FUSE). False without a vector on this device.
bool cvec_tables(const float** dir, const float** s, const int** on);
/// Layer `layer`'s vector on T tokens' residual stacks (`R + t * r_ld`, hc streams of n_embd). With `write`, the
/// pending FFN write `R += bo * 2 sigmoid(inj / hc)` (the fused read's arithmetic) is applied first, for callers
/// whose writes are folded into the next layer's read; without it, R must already hold the layer's output.
Loaded 100 of 543 files, more files were not shown because too many files have changed in this diff. Show more