Rarely a window's logits become NaN and the reply degenerates into token 0
('!') repeated (#606); the poison sits in the conversation's K/V and
recurrent state, so with the prompt cache every later turn stays poisoned
until it is read again from token 0. The q8_1 clamps (2b726ba, f6601bf,
7a0e302) remove the overflows found so far; this guard catches whatever
cause is left, and drops the poisoned state instead of serving it again.
On by default, as a fix: STRATA_NAN_GUARD=0 turns it off. It only reads
flags, so an answer without a NaN is the one the engine gives with the
guard off (compared byte for byte on a GPU, below).
Engine:
- every verify-window graph checks its head logits beside the greedy pick
(logits_nonfinite_rows: 16 blocks a row, one flag word each, into mapped
memory); Verifier::note_nonfinite reads them after the sync the window
already does (run, run_slot_rows, batch_poll, pl_finish). No extra sync.
With STRATA_NAN_GUARD=0 no kernel is recorded.
- --serve: a flagged window emits none of its tokens, one stderr line names
the position and row, and the request ends with `DONE ... nonfinite` (a
batch slot: `BDONE <slot> <n> nonfinite`; an admission: BADM <slot> 0).
The next request first drops the live session, every checkpoint, every
parked conversation (ConversationCache::clear) and every idle slot, and
reads its prompt from token 0; a slot still running then finishes its
reply and is not kept as a cache after it. A request that ends on it does
not keep its conversation live (#1620's cancelled-read path is skipped
for it).
Covered: the serial decode loop (one GPU or a layer split), the
--pipeline-windows decode loop of a layer split, the prompt's short tail
(read_windows, and read_windows_pl with --pipeline-windows), and both
batch-slot loops (run_slot_rows, and the --batch-groups pipeline's
batch_poll). Not covered: the one-shot command line (no --serve) and the
SYCL port in sycl/, which has its own verify.cpp and generate.cpp.
- STRATA_NAN_INJECT=<n>[,<n>...] (debug): the n-th window the guard checks
reports row 0 as non-finite.
Server: EngineNonFinite on that finish. A request that has sent nothing yet
is sent again once (its usage is the retry's: the failed pass's DONE is
dropped); a reply that had started ends as "length"; a second fire on the
retry ends with an error (503 before an answer, an error event in a
stream). With replicas the router forgets what that replica held.
repeat_stop_tokens stays as the backstop.
Checks: serve tests (python -m unittest discover -s serve -t . -p
"test_*.py": 697 tests, OK, 11 skipped; NonFiniteLogits,
test_nonfinite_in_batch_mode and test_nonfinite_forgets_the_replica are
new). ctest with CUDA_VISIBLE_DEVICES=-1: 24 pass (conversation_cache_test
with clear() among them), 3 skip for want of a GPU, and the 43 GPU parity
tests were not run on a GPU. Built for CUDA 13.0 on Linux (GCC, sm_75 +
sm_86); generate.cpp, verify.cpp and verify_kernels.cu compile with
MSVC 2022 + CUDA 13.0, and conversation_cache_test and conv_cache_test
pass there; built for HIP (ROCm 7.0, gfx1100, compile only) and SYCL
(oneAPI 2026.1.1; it takes only the new declaration in verify_kernels.hpp
and the new inline ConversationCache::clear(), and calls neither).
On a GPU (RTX 3080, RVN IQ3_S, strata.service's one-GPU config):
- Answers, guard on vs STRATA_NAN_GUARD=0, at fixed expert sets
(--pcie-frac 0 --adapt-every 0 --adapt-swaps 0 --mtp-max-t 1
--suffix-draft 0, STRATA_IQ_MT_MIN=1 STRATA_PREFILL_CPU_SHARE=0), a chat
replay of 2 x 4 turns on a 16K-token system prompt: byte-identical in 3
pairs (one with STRATA_ROUTE_TAIL_SKIP=0, one with STRATA_SH_STREAM=0),
and two guard-on runs 8 of 8. One more guard-off run differed from every
other run from its first answer, and a second guard-off run matched the
guard-on runs: a difference between runs of this config, not the guard.
- Cost, Nsight Systems: logits_nonfinite_kernel 424 launches, 4.77 us on
average (7.78 us at most), 2.0 ms against 21.6 s of decode: 0.009%.
- STRATA_NAN_INJECT=1,60: the first window of a new chat flagged, the
server sent the request again from token 0 and the client saw one normal
answer; the 60th, mid-reply in the next turn, ended that reply as
"length" after 58 tokens; the turn after it reused 0 cached tokens and
answered normally. The engine logged exactly the two injected windows.
route_resident_k is one warp per token and the form it replaced walked all 512 experts on a single
thread. Nothing in the tree checked that the two land on the same expert and leave the same ids,
weights and counters, and the option's whole justification is that it moves misses across PCIe and not
the model.
src/kernels/route_resident_parity.cu: 240 random cases over 1 to 8 tokens, a 40-84% resident table,
random rank windows, margins from -0.5 to 1.5, and half the cases rounding the logits onto a coarse
grid so the tie rule (the smallest expert index wins) is exercised. Half the cases get the router's
own top-10 and half get ids with repeats, which is what the already-picked test is for. The reference
is the spec written out plainly - a serial walk with the same comparisons - and not a copy of the
kernel this branch replaced; it runs on the device so that expf, and so the softmax bits, are the same
function on both sides.
ids and the four counters have to match bit for bit. The weights are a softmax ratio and this
reference is a second compilation of it, so they are compared within 8 ulp and the worst seen is
reported; the kernel itself is bit-identical to the serial form it replaced, which was measured with
both kernels side by side in one translation unit. A case that swaps nothing still has to leave the
weights untouched, which the sentinel they start from checks, and the test fails if no case swapped at
all.
route_resident_parity: 240 cases, 205 with swaps (2004 swaps), 0 mismatches, weights within 6 ulp
Registered in ctest next to the other router parity tests, TIMEOUT 120.
The swap kernel is the only work STRATA_ROUTE_RESIDENT adds to a window, and it was written as one
thread per token: with a verify window of 1 to 8 rows that is up to 8 threads of a block, each doing,
per rank in [lo, hi], a 512-expert scan with a ten-way "already picked" test inside it. On this
machine that is about 4 x 512 x 12 scalar operations running on six threads, on the critical stream,
once per layer, and it shows up as its own cost in the window.
STRATA_VERIFY_PROFILE, three legs in one session, same prompts, same expert cache (8083 slots, one
learned profile), medians of the per-window stage table:
configuration hc-read1+router waitB ms/window pcie_e
option off 2.25 9.30 21.85 1.87
option on, one thread per token 5.00 5.01 21.97 1.00
option on, this commit 2.58 4.87 19.75 0.95
The option redirects about a miss per layer-window away from PCIe and saves 4.3 ms of waiting for it;
the serial swap spends 2.75 ms back, which is why the option measured as a wash. In the warp form the
swap costs 0.33 ms, so the waiting it saved stays saved.
End to end, twelve matched pairs of engine restarts, nine decode samples a leg, three greedy 150-token
prompts, the learned expert profile copied back before every leg so both arms start from the same
8083-slot cache. The first six pairs ran the unpatched build first, the last six the patched build
first, so a within-pair order effect would show up as the two halves disagreeing:
pair order decode unpatched patched delta ms/window unpatched patched delta
1 A->B 104.1 115.7 +11.1% 18.64 17.01 -8.7%
2 A->B 103.5 107.4 +3.8% 18.47 18.12 -1.9%
3 A->B 111.4 107.7 -3.3% 17.71 18.26 +3.1%
4 A->B 101.4 113.0 +11.4% 19.11 17.03 -10.9%
5 A->B 105.8 107.6 +1.7% 19.05 18.05 -5.2%
6 A->B 104.8 105.6 +0.8% 18.60 17.83 -4.1%
7 B->A 106.6 113.4 +6.4% 18.70 17.16 -8.2%
8 B->A 103.1 110.2 +6.9% 18.14 17.29 -4.7%
9 B->A 106.9 111.5 +4.3% 17.91 17.26 -3.6%
10 B->A 104.3 114.7 +10.0% 17.81 17.11 -3.9%
11 B->A 108.3 115.9 +7.0% 18.47 16.61 -10.1%
12 B->A 104.5 113.6 +8.7% 18.43 17.76 -3.6%
twelve legs each: unpatched decode median 104.7 (101.4-111.4) ms/window median 18.47 (17.71-19.11)
this commit decode median 112.2 (105.6-115.9) ms/window median 17.27 (16.61-18.26)
decode median +6.6% mean +5.7% faster in 11 of 12 pairs (sign test p = 0.006)
ms/window median -4.4% mean -5.2% shorter in 11 of 12 pairs (sign test p = 0.006)
first six pairs: +2.7% decode, -4.7% window last six: +7.0% decode, -4.3% window
pcie_e, the engine's own count of experts per layer-window that cross PCIe, is the same in both arms
(leg medians 0.63 against 0.64), so the same experts are routed and the difference is the kernel.
The swap's cost, and so its removal, scales with the tail experts the option actually redirects. Four
more pairs with a 4000-slot cache instead of the automatic 8083 (pcie_e 1.83 against 1.78, i.e. about
three times the traffic of the series above), two pairs each way:
pair order decode unpatched patched delta ms/window unpatched patched delta
1 X->Y 83.8 92.0 +9.8% 21.94 20.60 -6.1%
2 Y->X 84.6 92.5 +9.3% 23.97 20.53 -14.4%
3 X->Y 82.2 85.5 +4.0% 23.69 22.65 -4.4%
4 Y->X 81.0 87.7 +8.3% 24.54 22.21 -9.5%
decode median +8.8%, ms/window median -7.8%, 4 of 4 pairs, both orders.
What this changes is the shape, not the choice. One warp per token: lane j reads experts j, j+32, ...
(coalesced instead of a serial walk), the warp reduces to the best logit in a shuffle tree, and the
ranks are still visited in order, because a swap changes the set already picked. The choice is the
same bits: a strictly greater logit wins, the smallest expert index breaks ties, the already-picked
test runs against the ids as they are at that rank, a token with no swap keeps the router's weights
untouched, and the softmax is the same expression.
Parity. 400 random cases - half of them built with many equal logits, random lo/hi, margins from
-0.5 to 1.5, 1 to 8 tokens, about 60% of the experts resident, routed ids with repeats - compare the
two implementations bit for bit on the ids, on the weights (their raw bits) and on the four counters.
The harness carries both kernels side by side; 0 mismatches.
For context, the option's price is the margin's and not the kernel's, and this commit does not touch
it. Teacher-forced log-probabilities over 7677 positions of one 7.6k-token text (technical prose,
source code and Chinese), top-64, against the same text with the option off:
margin argmax same KL (top-64) teacher-forced loss perplexity
off - 0.000 3.2033 -
0.25 84.9% 0.235 3.2238 +0.6%
0.50 83.0% 0.293 3.4360 +7.3%
1.00 63.5% 1.001 4.7751 +49%
Two runs with the option off are bit-identical, so that spread is the option and not the machine.
0.25 keeps nearly all of the waitB saving for a fraction of the distortion; the default 1.0 is not
worth its price.
Tests: the parity harness is a standalone .cu (both kernels, the random cases, the bit comparison).
Happy to wire it into the test suite next to the other kernel tests if you want it in the tree.
The QSA step records of a chunk were uploaded with an unchecked cudaMemcpyAsync from a
std::vector. On gfx1100 the first text prompt after a picture one read garbage records
(5 of 5 faults on a two-request repro, 0 of 5 with a pinned, event-guarded source). The
token id upload has the same shape and now takes the same path; both check the result.
Reported and traced by mantovaniluca91, #1712.
IlQ3K runs q3_q8_dot_impl's arithmetic with the scales and signed values done once per chunk; bitwise native_mmvq's output
for T 1-4 and rows 1/2/4 (mmvq_il_parity). Q2_0 pack: the Q3_K multi-column kernels were 14% of GPU kernel time; the new kernel
takes 2560x10240 at 4 columns from 120 to 70 us on an RTX A4000. The shared table (sm_80+/sm_120) is unchanged.
Bitwise equal to the CUDA layout on all 48 layers at 1-4 tokens a group (native_expert_bench); a 28-group VRAM call 17.6 -> 15.5 ms over the layers at T=1, 24.1 -> 22.6 at T=2, even at T=3, +2.6% at T=4.
Bitwise equal to native_mmvq (mmvq_il_parity, T 2-4, rows 1/2/4); table read off mmvq_il_parity --bench --emit-table on a Tesla P100 (three identical runs).
The pipelined decode loop asks both stages' windows for service millions of times a second while they wait on their
doorbells, and every call switched the device (OnDevice: cudaGetDevice and two cudaSetDevice) before finding nothing
to do. On a 0.1.41 split (RTX 3060 + RTX 5070 Ti, Windows) the host stalled inside one of those cudaSetDevice calls
after 919 million loop passes, with both stages' windows waiting on it (stall dump: the serving thread in cudart from
Verifier::service, every other engine thread idle). Now a call with nothing rung, no WDDM flush due (2 ms) and no
timeout due returns 0 first - the same answer the loop below gives - so the driver is called only to serve a layer,
to flush, or to time out.
The pages had no <link rel="icon"> and serve/web had no icon file, so browsers asked for
/favicon.ico, got a 404 and showed the default blank icon in the tab.
serve/web/favicon.svg is the layer stack the app already uses (serve/web/sprite.svg, i-layers),
redrawn on a 32-unit viewBox so it stays legible at 16 px, in the existing accent token #10b981.
Checked: curl /web/favicon.svg -> 200 image/svg+xml, both pages come back with the icon link,
and the icon rendered at 16/32/48/64 px in Chrome.
_Nvml.read() reports pcie_gen, pcie_gen_max, pcie_width and the two
throughput counters; _Amd.read() (the amdgpu sysfs reader from #301)
reports none of them, so on Linux the Monitor's PCIe tile shows only
dashes while the NVIDIA tile fills in.
This adds the link the card actually gets: the narrowest and slowest hop
between the root port and the card, from each hop's max_link_speed and
max_link_width. A card that negotiates Gen4 on its own hop but sits
behind a Gen3 root port really runs at Gen3, and that is the number a
bandwidth budget needs. pcie_own_gen keeps the card's own hop aside so
the gap is visible, and pcie_path reports how many hops could be read --
the bottleneck is only claimed when all of them could, because a kernel
or a container that hides part of /sys/devices would otherwise report the
card's own hop as the whole path.
Measured on one MI50 32 GB in a Gen3 x16 slot (lspci: the root port at
00:02.0 runs at 8 GT/s x16, the bridge above the card is downgraded, and
the card's own hop negotiates 16 GT/s), /metrics reports pcie_gen 3,
pcie_gen_max 3, pcie_own_gen 4, pcie_width 16 and pcie_path 4/4.
Throughput is deliberately left out: amdgpu's pcie_bw is three counters
whose documented use is a total, not a direction, while _Nvml's
pcie_rx_mb is receive-only, so filling that name from pcie_bw would
label a combined figure as one direction. Happy to follow up separately
if you want the tile to show a rate.
Hardware the numbers come from: one Radeon Instinct MI50 32 GB (gfx906,
PCI 1002:66a1, VBIOS 113-D1631711-100) on a Xeon E5-2696 v4 (Broadwell,
no AVX-512), in a PCIe 3.0 x16 slot, 128 GB RAM, Ubuntu, kernel
7.0.0-34-generic, ROCm 7.14 in a container.
Prefill::arm_cpu_share() asks "is this AMD?" with
#if !defined(STRATA_USE_HIP), but the gfx906 compat build (PR #638)
compiles as HIP *without* STRATA_USE_HIP -- see src/core/expert_cache.cpp
:5-6, which guards the same question with
#if defined(STRATA_USE_HIP) || defined(STRATA_HIP_GFX906).
On gfx906 the CUDA branch is therefore live: the CPU share is armed by
default for chunks below 1,024 tokens and prints its banner, against this
file's own note at :161 ("the layer split, batch slots and HIP stay off").
Seven other sites already carry the || defined(STRATA_HIP_GFX906) form.
Measured on one MI50 32 GB (gfx906), CLI generate, a 4-token prompt, one
binary with one set of weights and one config, only the environment
differing, power saving off:
default experts streamed 309 (295 by DMA, host 174.0 ms), resident 607,
on the CPU 197 (share 0.90) prefill 928.3 ms
=0 experts streamed 506 (492 by DMA, host 177.3 ms), resident 606,
on the CPU 0 (share 0.00) prefill 929.5 ms
Output token ids are identical (271 248068 198 760) and the 0.13%
difference is inside this machine's run-to-run spread (1.2-1.7%), so on
this card the share engages and buys nothing. #1595 and #1729 report the
default costing on CUDA; this is the mechanism that turns it on for AMD.
CUDA and SYCL do not define STRATA_HIP_GFX906, so the added check is a
no-op there and nothing on those backends changes.
Hardware the numbers come from: Xeon E5-2696 v4 (Broadwell, 22C/44T, no
AVX-512), one Radeon Instinct MI50 32 GB (gfx906, PCI 1002:66a1, VBIOS
113-D1631711-100, 34,342,961,152 B of VRAM), PCIe 3.0 x16 effective (the
root port runs 8 GT/s x16 and the bridge above the card is downgraded;
the card's own hop negotiates 16 GT/s, about 15.75 GB/s end to end),
128 GB RAM, Ubuntu, kernel 7.0.0-34-generic, ROCm 7.14 in a container,
built with -DSTRATA_HIP_GFX906=ON and -DCMAKE_HIP_ARCHITECTURES=gfx906.
spark(id, values, max) sets the axis top to max(|max|, the series' own
maximum, 1e-9). gpu_util, temperature, cpu, vram and power pass an
explicit maximum, but the pcie, tok/s, prefill and disk cards do not, so
those axes zoom onto whatever the series happens to hold.
An idle amdgpu pcie counter reads 0.01-0.02 MB/s (50 packets x 256 B per
second), and that becomes a full-amplitude square wave in the Monitor
while the tile's own text says 0.0 MB/s. Measured in the live DOM: the
pcie line's y values are only {4.0, 30.0}, i.e. the baseline and the top.
A floor of 1 only ever lowers the axis - the series maximum still wins -
so a real burst keeps its scale, and the cards that pass a maximum are
untouched.
IQ4XS.test_amd_is_not_asked mocks get_prebuilt_hip, the engine setup.main takes on Windows. On Linux setup.main
compiles the HIP engine instead (build_engine_hip), which the test did not mock. Without a usable system ROCm 7, as
on the tested machine, rocm_root then fetched AMD's ROCm index
(https://rocm.nightlies.amd.com/v2/gfx120X-all/rocm/), wrote Path(sys.prefix)/.strata-rocm.json and tried to run
<python dir>/rocm-sdk. Where sys.prefix is not writable (the system Python) that is a PermissionError. Where it is
(a venv), the test passed while setup.main stopped at the ROCm step with exit code 1, and a stamp with the same
version and index can make a later setup in that venv skip the ROCm install.
The test now mocks build_engine_hip the same way, so on Linux setup.main runs to the end (exit code 0) with no
network request, no subprocess and no write outside the test's temp folder. setup.py is unchanged; the Windows path
never reaches build_engine_hip, so it runs as before.
Tests: python3 -m unittest tools.test_setup_unsloth (WSL2): with the system Python 3.12.3 FAILED (errors=1) before,
38 tests OK after; in venvs on 3.10.22, 3.12.3, 3.13.7 and 3.14.8 OK before and after, and no stamp after. With the
system Python the other 27 tools/test_setup_*.py files give the same results as before.
The `[16.0,18.0)` range is CUDA 13's requirement, but it was applied to every caller: with the
only Visual Studio installed being 2026 (version 18), vswhere finds nothing and find_vcvars()
returns None. That makes the CPU image encoder unbuildable on such a machine - build_vision_cpu()
cannot run - so --vision cpu stays unreachable there even with the gate in hip_vision() open.
#985 relaxed the range for CUDA 13.3+ by adding `cuda_v`, which is right for CUDA callers. A
caller with no CUDA now passes nothing and gets no range, which is what
tools/hip/build_windows.bat already does when it calls vswhere directly.
- `cuda_v is None` -> no -version argument at all. CUDA callers are unchanged: cuda_v=(13, 0)
still refuses VS 18, cuda_v=(13, 3) still takes it.
- STRATA_VCVARS=<vcvars64.bat> picks an install by hand, for a vswhere that cannot be used.
Verified on the RX 6800M / gfx1031 machine of #881 (Windows 11, Chinese locale, the only C++ tools
are Visual Studio 2026 / 18.4.11605):
find_vcvars() -> <VS 18>\VC\Auxiliary\Build\vcvars64.bat
find_vcvars(cuda_v=(13, 0)) -> None
find_vcvars(cuda_v=(13, 3)) -> <VS 18>\VC\Auxiliary\Build\vcvars64.bat
With a draft-vocab head (rt/draft_vocab.bin) the slot drafters of --batch-mtp share the main drafter's head but kept
dhead_type_ at -1, so the first draft_first() of a batch admission threw 'mtp: unsupported native MMVQ GGML type'
and the engine exited (#1121). The CUDA source got this line in #1110; the SYCL port did not.
A periodic loop emitted as digits (a 24k-digit string cycling an 11-digit
pattern) is a single word to the word count and never one token twice
(the tokenizer has no multi-digit tokens), so both #606 and #728 passed
it and the run burned the whole reasoning budget. The reasoning's last
2,048 chars are now checked for a period of at most 64 chars, and a
periodic tail reads as full coverage. The opt-in path only: the default
path and the word count are unchanged.
With the Intel port the configured exe is sycl/serve/strata-sycl.sh, a shell script that runs the repo's
STRATA_SYCL_BIN (default build-sycl-aot/strata) in a container. effort_end_args looked for --tail-role-token in the
script, never found it, and fell back to the top-of-prompt effort with "needs engine 0.1.39 or newer", even on a
0.1.41 engine that has the option. engine_binary() resolves the launcher to the binary it runs; any other exe is read
as before. tools/test_effort_end_args.py covers both paths.
Follow query-swizzle #1661 with the existing reduce-scatter pairing for
12 heads. Keep default paths and softmax/value/merge unchanged. Public
component parity and timing passed; model evidence uses frozen stack.
Record archived-control output parity, verified candidate process, tools, CPU vision and cache restoration. No fresh 200K speed comparison or production promotion is claimed.
Preserve FP operation order and invert a bijective shared-query layout at its vector loads. Limit the path to the current gfx906 wave64 device, INT8 pools and warp scoring. Default remains off.
Add deterministic GPU parity/timing and host-guard probes, plus scoped component and model receipts. The clean upstream build passed both-card byte checks; model gains are recorded separately on the frozen experimental stack.
Pinned memory reads the link 1.7x faster (A750 10.3 -> 17.2 GB/s, B70 13.6 -> 25.6), which moves the derived default pcie_frac
(A750 0.28 -> 0.47) and cost 10% of decode there (median 12.07 -> 10.67 tok/s, 4 of 5 interleaved pairs slower). The B70 is unchanged
(0.37 -> 0.55, 31.4 vs 31.4). The fault the PR fixes is a layer split's.
Co-authored-by: maxious <[email protected]>
server_intel.py replaces StrataEngine._pump to keep LP lines beside their token, but its copy predates --batch: the
slots' BT / BDONE lines went to the main queue, so with "parallel" every request stopped after one token. It also
missed the fatal-line handling and last_err. Both are now as in serve/server.py's _pump.
The host mirror holds the experts missing from VRAM at the start. An adaptive swap moves an expert that was resident
then out of VRAM, so it is in neither; the device plan then waits for the host to serve it, and with
STRATA_VERIFY_NO_HOST (the xe default in strata-sycl.sh) nobody does. Seen after a --batch window, whose host path
counts the misses the tier swaps on: the next single request on one card gave token 0 ("!!!!") at 0.8-1.3 tok/s.
The start already refuses experts in neither under NO_HOST; this keeps that true after the start.
The prompt path dequantizes each dense Q8_0 projection (GDN qkv/gate/out,
QSA q/k/v/out, shared expert, hc) into the GEMM scratch before cuBLAS.
dequant_kernel runs one thread per 32-value block, so a warp's 2-byte
stores land 64 bytes apart: about 70 GB/s of writes on a V100.
dequant_q8_0_kernel gives each block to four threads, 8 values each,
written with 16-byte stores: a warp reads 8 consecutive blocks and writes
512 consecutive bytes. The arithmetic per value is unchanged (d * q), so
the output is the same bits. It is used for Q8_0 when the output rows are
contiguous (ld == cols) and the pointers are aligned (blocks to 2 bytes,
output to 16); everything else takes dequant_kernel as before.
V100-SXM2-16GB, every 2-D Q8_0 tensor of Qwen3.8-Flash-Next UD-Q4_K_XL
(498 tensors), each call timed on its own:
attn_qkv 10240 x 2560, FP16 0.740 -> 0.106 ms (70 -> 495 GB/s written)
attn_q 12288 x 2560, FP16 0.887 -> 0.126 ms
ssm_out 2560 x 6144, FP16 0.451 -> 0.065 ms
all 498 tensors, FP16 138.5 -> 20.4 ms
all 498 tensors, BF16 140.8 -> 20.5 ms
all 498 tensors, FP32 190.9 -> 35.4 ms
An FNV-1a hash over all outputs is equal before and after for FP32, BF16
and FP16. Whole prompts on 4x V100 (where this was first used), 4,096-token
chunks: 18K 4,537 -> 4,708, 65K 6,540 -> 6,766, 123K 6,783 -> 6,985 tok/s.
The whole i-quant family (IQ3_XXS, IQ3_S, IQ2_S, IQ4_XS gate/up, IQ4_NL down) and the
fork's Q2_0 down have x86 dots only under __AVX2__ in ggml-cpu; on an AVX1 floor
(STRATA_ISA_FLOOR=avx, issues #595/#623/#1375: Xeon E5 v1-v2, and AVX-512-only packs)
the CPU-computed experts fall to the scalar _generic dots. iq_avx1.cpp is the older
CPUs' copy of iq_avx2.cpp in the kq_avx1.cpp pattern: a separate TU compiled
-msse4.2 -mavx -mno-avx2 -mno-fma -mno-f16c, gated at runtime behind cpu_avx1_ok() and
only when AVX2 is absent, so no mainstream path changes. 128-bit integer lanes
(pshufb/sign/maddubs are SSSE3), 256-bit float accumulation, software half-to-float,
no gathers (the Fmt32 non-gather path), one IEEE mul per value like the AVX-2 copy.
Plus q8k_quant_avx1, the byte-identical Q8_K activation quantizer in SSE4.2/AVX1.
iq_avx1_parity: all six formats, nt=1..8, bit-exact across widths (#152) and within
1e-5 aggregate of ggml's generic dots; q8k_quant_parity now checks the AVX1 copy too.
gu rows take the AVX-2 mt_min policy (STRATA_IQ_MT_MIN), down rows always; kill switch
STRATA_NO_IQ128=1. E5-2470 v2 (Ivy Bridge): 1.2-3.1x per expert at nt>=2, 2.0x on the
Q8_K quantizer; serve prefill +36-41%, warm decode 22-25 -> 46 tok/s.
Known pre-existing gap found while testing: pool_tasks_test SIGILLs on non-AVX-512
machines even on a clean tree (it predates the ISA floor and is not gated).
q2_rows_any and act_quant_any had two rungs, AVX-512 and AVX2, and the else branch
checked nothing: an AVX-only CPU was handed AVX2 code and died with SIGILL (found on
an E5-2470 v2; pool_tasks_test and q2_bitplane_parity crash on a clean tree, and any
future caller inherits the trap). The third rung: q2_avx1.cpp in the iq_avx1.cpp
containment pattern (-msse4.2 -mavx -mno-avx2 -mno-fma -mno-f16c, a #error guard,
called only behind cpu_avx1_ok() && !cpu_avx2_ok()). 128-bit integer lanes (maddubs,
madd), 256-bit float accumulation, software fp16 scales; the legacy kernel keeps the
AVX-2 kernel's lane pairing and reduction tree (its FMA becomes a mul+add, last bits),
and the bit-plane kernel and its image builder port as-is (pure SSE). The AVX-2 and
AVX-512 paths are untouched. q2_bitplane_parity now runs on the AVX-2 pair where the
CPU has AVX2 and on the AVX1 pair below it, and skips cleanly without AVX.
E5-2470 v2: pool_tasks_test 168 bitwise comparisons pass, q2_bitplane_parity PASS -
both SIGILL'd before.
A helper GPU's expert cache already holds a copy of the blobs the prompt path
streams, and the helpers compute nothing while a prompt is read on a serial
request path. Add RemotePrefillSource, an ExpertSource that wraps the file
source and asks the helpers first: a blob a helper holds is copied
cache-to-host on that helper's own stream, misses are merged into one call to
the fallback's existing batched reads, and every other decision is forwarded
unchanged.
Opt-in through STRATA_PREFILL_HELPER_SOURCE; it applies only where the serve
path is serial (one GPU, no layer split, no batch slots) and the prompt path
streams every expert the main card does not hold. Same-day A/B on an
11-request fixture: 91.5 s -> 65.7 s of prefill (-28.3%) and 92.1 GB (64.8%)
fewer bytes read from the files.
The share is picked from wall-clock timings, so the same short-prompt
request can split the experts differently each run and the answer moves
in its last bits - a resumed conversation's re-read need not match its
first (31 of 1,536 teacher-forced argmax tokens differed in the
reporter's test). A fixed share repeats; 0 gives a run without it
byte-for-byte.
Five simulated agent sessions over the repository's own files (system prompt, four tools, real files as tool
results, at most 2000 lines per read): 23 turns from ~2.5K to ~100K tokens, consecutive turns sharing their prefix.
Arms as ab_engine.py takes them; rounds mirror the arm order on fresh servers; prints the median of the per-turn
decode ratios to the first arm and the turns it won. Nothing personal and nothing downloaded.
A missed expert that every token of the verify window routes at rank R or lower in weight contributes zero
(no renormalisation): less PCIe traffic and CPU expert work. Counted in the request log. Opt-in, changes
the output.
The top-10 is picked again with a logit bonus lambda for the experts resident in VRAM; the weights stay the
router's probabilities renormalised over the 10 picked. After 'Mixture of Cache-Conditional Experts'
(arXiv 2412.00099), without training. Complements STRATA_ROUTE_RESIDENT (a tail swap within a margin):
every rank can move, by an amount the logits decide.
With --resident-experts (setup's low-RAM mode when the experts the GPU does not hold fit the RAM) the start copies
the profile fill of the GPU cache and the RAM copy (pin_cache_complement) through the mapped view of experts.bin.
Every page it touches sits in the process working set beside the page-locked copy until #467's trim after the fill
and the VirtualUnlock after the copy: on a PC short of RAM that pages other memory out, and the copies run at the
page-fault rate (~0.85 GB/s here).
FileExpertSource::begin_startup_unbuffered closes the view, the section and the buffered handle, opens the
unbuffered handles (open_direct) and reads the first blob both ways; if the open fails or the two differ, the view is
mapped again and the start copies through it as before. The fill (prefetch_pairs) and the copy (pin_cache_complement)
then take their existing unbuffered paths (read_direct: requests merged up to 32 MiB). end_startup_unbuffered maps
the view again (map_view, the size checked) and closes the unbuffered handles and the stage buffers, so the file tier
is chosen as before. Same bytes in the same places.
Windows and experts.bin only: the call is under _WIN32, and the GGUF in place, an unbuffered file tier and runs
without a RAM copy are unchanged. STRATA_STARTUP_UNBUFFERED=0 gives the mapped copy back; STRATA_VERIFY_COMPLEMENT=1
prints checksums of the filled cache slots and of the RAM copy. pin_cache_complement no longer VirtualUnlocks the
range of a closed view.
IQ2_XS (33 GiB experts.bin), RTX 5090, Samsung 9100 PRO, --resident-experts --expert-cache auto (~15,200 slots, a
16.4 GiB RAM copy), interleaved against fb58e0db:
- 21 GiB available (a RAM ballast; #467's 32 GB PC had 20.7 GiB), 6 pairs: ready 42.5 -> 9.6 s (chat), 40.7 -> 10.2 s
(32K); the fill 842 -> 6,914 MB/s, the copy 13.7 -> 3.3 s; lowest available 0.25 -> 4.5 GiB; pages written out
~60,000 -> 579-3,340; 3 of 12 base starts held available RAM under 1 GiB for 2 s, none of 12 here.
- 128 GB: peak working set 32.6 -> 18.3 GiB; a restart with the file cached 13.6 -> 8.3 s; the 32K prompt and decode
unchanged.
- Same bits: p2k 200 tokens and first-token logits, 32K 64 / 256 tokens, as fb58e0db; STRATA_VERIFY_COMPLEMENT=1
checksums identical to STRATA_STARTUP_UNBUFFERED=0 (12,595 and 15,438 slots).
Based on RealScaniX's sorted-by-name change; the first request of a set stays byte for byte as the client sent it.
Co-authored-by: RealScaniX <[email protected]>
RulesAgreeWithTheWrittenConfig.test_the_streaming_rule_is_the_flag_setup_writes runs setup.main() through
test_setup_golden's install(), which mocks is_wsl() to False, and then asked setup.kv_streaming_wanted() outside
that mock. Inside WSL the real is_wsl() is True, so the rule answered "no streaming" while the config setup wrote
for the mocked PC has --kv-resident, and the test failed at its first 131072 case (Q2_0, int8, auto). With is_wsl()
the same on both sides, setup and the rule agree in all 54 cases, inside WSL and out.
The test method now runs under the same is_wsl mock as install(), the test-only fix the unsloth tests got for #974
(d1ec32b). setup.py is unchanged; where is_wsl() is already False (Windows, native Linux) the test runs as before.
Tests: python3 tools/test_setup_risk.py (WSL2, Python 3.12.3: was FAILED (failures=1), now 40 tests OK); the other
27 tools/test_setup_*.py files give the same results as before.
A request with "temperature": "0.7", "top_k": 40.0 or "seed": true was
answered 200 and sampled greedy: the value replaced the configured
default in run()'s merge, then sampling_keys dropped it as not a number.
check_request_sampling refuses it by name before anything is sent, as
POST /settings and the run config already do. Only types are checked;
ranges, null fields and in-type values behave as before.
A chat "file" or "input_audio" part, and a messages "document" block
(also inside a tool_result), were dropped without a word: the request got
a 200 and the model answered about a file it never saw. _check_parts
refuses them while the request is converted, naming the type and where
it sits, with /v1/responses' wording. Text, images, thinking, tool_use
and tool_result read as before; redacted_thinking is still dropped; a
chat "refusal" part reads as its text, as on /v1/responses.
The row-count sort of a fully resident layer's experts skipped native-layout packs, though slot/src/off are
built the same way for both layouts. 4x GP100 (sm_60): 1.2K prompt 7.27 -> 5.32 s, 35K 63.3 -> 54.1 s, the
same bits. The switch stays opt-in.
kvg_trim refilled the slots the K/V gave back and then uploaded host_res with a plain cudaMemcpy from pageable memory.
That returns once the table is staged, before its DMA lands, and nothing after it waited for stream 0: the next
request's session_zero syncs main_stream only, and a prompt part of --short-read tokens or fewer goes straight to
ver.run, whose graph reads hits_.d_res on the verifier's cudaStreamNonBlocking stream. res_put (#550) is the upload
every other residency change uses: the same copy, then cudaStreamSynchronize(nullptr).
kvg_ensure's upload needs nothing: qsa_kv_elastic_grow ends in cudaDeviceSynchronize before anything reads the table.
Only --kv-grow (opt-in) runs kvg_trim, so the default path is unchanged.
With stream_tools an array or object argument is held until it is whole,
so a large one (a measured 66 KB edit_file call) left the stream silent
for minutes while tokens were generated - a client's idle timeout then
ended a healthy request (Forge's is 120 s).
The token loop now tracks when anything last went out and sends the
engine's own keep-alive ping after KEEPALIVE_S (10 s) of silence - the
same ping the quiet-engine heartbeat sends, so clients already ignore it.
The reporter's reproduction measured a 27.8 s longest gap on a 7 KB
array (minutes on the 66 KB one); with this change it is 10.0 s and the
call arrives intact.
Change and live measurements from the issue; adds the ping-cadence unit
test (a patched clock over a held-back <tool_call>).
The default router, publish and copy_or_zero kernels are the f-142 ones. The probes set the variable (and
STRATA_DOORBELL_CHECK=1 for publish_res, which copies x only on a miss otherwise).
Port the fork's fast router with a bracketed reciprocal and serial fallback. Add bitwise comparisons against the existing router, including ties, NaN tails and the forced serial path. On the A770's 256-expert/top-10 shape, measured host submission plus execution fell from 395 to 119 us per call.
Pair activation payload stores and mapped result reads into 64-bit uncached transfers, preserving the checksum and unaligned/odd-tail fallback. Register router and payload probes with CTest. The two probes, elementwise_parity and router_top10_parity passed on the A770.
Add the fork's opt-in host-clock stage profiler for further investigation. Profiling remains experimental: enabled runs encountered a payload-arrival timeout, and review flagged concurrent atomic host writes versus non-atomic device clock reads as a synchronization concern. The profiler is not needed for normal inference.
With CPU-computed misses, cache borrowing and an 8.98 GiB VRAM cache, unprofiled Coder IQ1_M runs generated 128 tokens at 11.9-13.5 tok/s and returned final arithmetic answer 41. These runs retained 128K context capacity but used short prompts; the 20 tok/s goal has not been achieved.
The admission checks prompt + max_new <= max_context, but the spec loop
opened every window at S_mtp rows, so a request that fits ended with
"ran out of context" (rc 2) once p + S passed the end, and printed
nothing. The window now takes only the rows the context still holds.
MtpDrafter::draft wrote draft j at cell p + a + j for every step, past
max_cells near the end, where the page table has no entry; it now drafts
only the cells that exist. Far from the end nothing changes.
CUDA 12 refuses a GCC newer than 14, and the docs already tell the user
to run CXX=g++-14 ./setup.sh - but the environment only reaches CMake on
a build folder's FIRST configure, so on a retry (or the encoder's own
folder) the flag never landed: the reporter's run died at the last step.
host_compiler_defs() maps them onto -DCMAKE_C_COMPILER /
-DCMAKE_CXX_COMPILER / -DCMAKE_CUDA_HOST_COMPILER for the engine and the
CUDA image encoder; CUDAHOSTCXX defaults to CXX, as CMake's does. A
C++-only build (the CPU encoder) is not given the CUDA one.
STRATA_VERIFY_STEPPED=1 (Arc A-series): the doorbell window is captured as graph segments ending at each layer's
publish; the host serves the CPU experts between segments, pipelined with the plan copy + VRAM groups on a helper
thread pinned to the host thread's SMT sibling. No kernel then reads a host store while it runs and no host reads a
kernel store while it runs, so the window's mapped buffers are ordinary host USM and the publishes use plain stores
(STRATA_VERIFY_HOST_UNCACHED=1 keeps both uncached). A770, Coder IQ1_M, 8K prompt: GPU wait per window ~70 -> ~43 ms,
decode 22.8-25.0 -> 31.8-32.5 tok/s; 32K 27.2 tok/s.
The pipelined slot groups never run the adaptive tier, so --adapt-async 1 with --batch on a layer split and no
--batch-groups resolves auto to one group, like --batch-mtp (batch_groups.hpp, batch_groups_test); --batch-groups G
or auto given on the command line is honoured and turns --adapt-async off through the existing gate.
batch_step advances the async round one step per window (never between a prompt's chunks), commands drain the round in
flight before they run, a round with nothing to swap ends at its first step, and --adapt-min-gain sets the bar a swap
must clear (default 1.5, as before) for both tiers. --adapt-async is no longer refused with --batch slots (still with
--batch-groups).
batch_step now runs adapt() every --adapt-every windows on a thread beside the commit and the drafts, lands the swaps
(apply_pending) before the window ends, and pauses while a prompt holds a loan of cache slots. The timing line adds the
adaptive tier's rounds/swaps and wait, each stage's GPU-reach wait and pool time, and the VRAM/PCIe/CPU entry split.
The 0.1.41 default --batch-groups auto, the pipelined path's lack of slot drafts and the helper-cache refusal are
written down in docs/BATCHING.md (the limits list and the --batch-mtp paragraph).
--batch-groups auto became the default for a layer split with --batch >= 2, but the --batch-mtp gate ran before
the groups were resolved: it saw 1 group, kept batch-MTP on, and the groups were then raised to 2 - pipelined groups
do not run the slots' MTP drafts (#1413). The resolution is now a pure helper (include/strata/program/batch_groups.hpp,
CPU test src/program/batch_groups_test.cpp): with --batch-mtp and no --batch-groups the default is one group and
the log says why; --batch-groups N (N > 1) or --batch-groups auto given on the command line pipelines the groups and
turns --batch-mtp off, as before.
- generate.cpp: --batch-mtp accepted with a layer split on separate GPUs (refused with --batch-groups); slot drafters
load from the last stage's slot sessions under the last stage's device guard and bind to its weights/head; their
residual buffers, row copy (async on the drafter's stream) and draft-KV copies run on the drafter's device; every
stage verifier gets the 8-row window size and the batch graph limit; slot-drafter logits are not charged to CUDA0.
- verify.cpp: stage_batch's layout rules move to include/strata/core/batch_rows.hpp (CPU test batch_rows_test); grouped
rows are allowed on a stage that hands rows on, the hand-off bound is kVerifyMaxT rows.
- docs/BATCHING.md, --batch-mtp help text.
Step 1 warns below 4 GB of page file (issue #60) and advised "Set it to System managed". In #60 that did not help:
"System managed" and 4096-32768 MB still ran out of commit, and a fixed 64 GB worked - a page file Windows grows on
demand may not grow in time while WDDM charges the card's memory to the commit.
- The advice is a fixed size: Custom size, the same initial and maximum size, 65536 MB (PAGE_FILE_FIXED_MB).
- page_file_setting() reads the Virtual memory setting (PagingFiles; "?:" is the drive in ExistingPageFiles). When a
file is system-managed or its initial size is below its maximum, the warning says that it grows on demand and that
a growing page file may not keep up (#60).
- page_file_gb() still counts only what is there for sure: the commit limit minus RAM, as before, or a file's
configured initial size when that is larger (within its drive's free space). Never the maximum or what a
system-managed file may grow to. The 4 GB threshold is unchanged.
- docs/AI_SETUP.md and docs/DETAILS.md give the same advice.
- tools/test_setup_page_file.py: the parsing, the reading, the advice, and step 1 under --check on the golden
harness's 32 GB PC (registry, drives and GlobalMemoryStatusEx mocked; runs on Linux too).
The MCP server's FALLBACK_MODELS carries its own copy of the model
table, so #1688's setup.py fix leaves the MCP install path rejecting
--family swift --model IQ3_S. Drop the families gate there as well,
flip the validation test, and update the three doc lines that say
IQ3_S is original-model-only.
Measured on an RTX 3090 pair (engine 0.1.40.3, Swift IQ3_S, 200K
context, MTP spec on): 91-137 tok/s decode, expert cache hit rate
96-99%, KV streaming 98.6-99.6% VRAM hits.
UkisAI published a Swift 1.5 IQ3_S tier on 2026-10-08; the
"Swift 1.5 has no IQ3_S" restriction is stale. Both shards match the
family's file pattern exactly (names/sizes/SHA-256 verified against the
Hub listing), the shard count and the shared BF16 mmproj are unchanged,
so it takes the same path the other Swift tiers already use.
The IQ2/IQ3/IQ4_NL/IQ1_M decoders now write values [8 tid, 8 tid + 8) of a superblock (coalesced runs, as Q2_0
already did); iq_xmx_grouped still passed the IQ4_XS-style (sub-run, 32-block) index for them, so the B tile held
the wrong values (xmx_group_parity relerr 7-17 on types 18/20/21/22). All six types back to ~1e-6.
- xmx_moe: joint_matrix FP16 GEMM over a group of up to 16 experts per launch (grf 256).
Standalone vs oneMKL on uneven routing: down 1.75-6x, gate/up 0.8-3x (oneMKL kept above
STRATA_PF_XMX_GU_MAX=250 tokens/expert).
- STRATA_PF_XMX=2: iq_xmx_grouped decodes IQ3_XXS/IQ3_S/IQ2_S/IQ4_XS gate/up and IQ4_NL/Q2_0
down rows in-kernel (no FP16 staging); xmx_group_parity: rel err < 2e-6 vs dequant+oneMKL.
- End to end (8K/32K prompt, A770): +2-5% only; default off.
--pipeline-windows 2 now runs on any layer split with two or more stages. Every
stage but the last is a front stage: the guessed window follows the verified
one through them one card behind (an odd verifier and an odd hand-off on every
boundary, a pair of GDN snapshots on every front stage, commits and the undo
per stage), and only the last stage waits for the verdict. The pipelined prompt
read (--pipeline-windows 1) and --adapt-async stay two-stage.
HIP: the draft layer's per-row gr_read used the last stage's session scratch
(ss.block.gr), which that stage's verifier also uses in lm_head_mix. A chain
launched at a verdict and the next window's last stage can run on the card at
once after a guess that held, and the window's head then read a clobbered mix.
The draft layer now carves its own GrWorkspace.
Measured on 4x RX 7900 XT (Flash-Next GSQ-RCO IQ3_S, split 12,24,36, every
expert in VRAM, greedy, reasoning off): decode 59.4 -> 67.6 tok/s at 4K and
51.8 -> 65.6 at 32K. Greedy text identical to the serial loop on four prompts
with STRATA_IQ_MT_MIN=1 --pcie-frac 0 --adapt-every 0, also with every guess
forced wrong. Debug knob STRATA_PIPELINE_SPEC_DEPTH. Two-stage path not re-run
on two cards.
probe_pcie_h2d_gbps: a pageable 256 MiB source made the copy engine fault
inside the driver's bounce path on a layer split (755x bcs VM_NOT_FOUND at
the fixed VA 0xffffc001ffa10000, L4 = the region never mapped for that VM,
intermittent on fresh boots, wedging the whole load in both the 0.1.40
port and 0.1.41). Pin the source (sycl::malloc_host) and free it with the
matching sycl::free. Validated: two consecutive loads reached the window
without the wedge.
All the split-fault instrumentation (guard watch, stage stamps and the
STRATA_STAGE_WAIT serial gate, terminate dumps, coredump grabbers) lives
on scratch/split-fault-instruments.
dpct turned `cudaStream_t cs_ = nullptr` into `&dpct::get_in_order_queue()`: a drafter that never loads keeps it, and
its destructor destroys the device's default queue (exit 139, sometimes UR_RESULT_ERROR_UNINITIALIZED, after the output).
Arc Pro B70 at fb58e0d without --mtp: 3 of 3 runs crashed before, 0 of 3 after; output unchanged with and without --mtp.
0.1.41 report (RTX 5090, tight RAM): chats failed and the engine restarted until STRATA_PREFILL_CPU_SHARE=0.
- share_rules: default_share_memory_verdict. The default is not armed when the RAM this process can get
(cgroup-aware) is below the share's buffers + 3 GiB, when the Windows commit left is below buffers + 4 GiB,
or when the experts it would read are file pages the free RAM cannot keep. One log line says why and that
STRATA_PREFILL_CPU_SHARE=auto turns it on anyway. An explicit setting is kept and only warns.
- prefill: the share's two pinned buffers are taken once at their largest (an expert takes at most MAXT
tokens' rows); a failed allocation ends the share for the run instead of failing the request.
- unit test, docs/DETAILS.md.
The counter block was one process-wide cudaMalloc on whichever device was current at the first Verifier::init, unreachable from
the other stage's kernels on a layer split without P2P. Each verifier now takes its own device's block in init (outside graph
capture), keeps the pointer, and a launch never picks by the host thread's device; a failed allocation runs without counters.
The exit report prints one block per GPU when there are several (single GPU: the same lines as before). The reporter's rare
fault with --pipeline-windows 2 after this fix is not reproduced here (needs two GPUs).
STRATA_ROUTE_RESIDENT_MTP=1 (experimental, opt-in, changes the output): the MTP draft's router gets the same residency bias,
with the last layer's residency row as its table (single GPU).
The refills of the slots a prompt borrowed (and the VRAM-resize refill) queue hundreds of cudaMemcpyAsync out of blob_stable()
pointers, which for a GGUF source are the recycled stage buffers. With STRATA_STAGE_PIN=1 the 257th assembly overwrote a
buffer whose copy had not run: IQ3_XXS lowmem (budget 12) decoded token id 0 after a 4096-token prompt. Each queued copy now
records the fence of its buffer. exact_pp 4096 and 20000 tokens with STRATA_STAGE_PIN=1: EXACT.
A busy slot flag with no owner kept every later request waiting for a slot nothing
would free, and a request waiting for the fifo (a load, an image encode, /v1/vram)
was counted nowhere. Now: slots have owners (claims / BDONE waits) and an ownerless
busy flag is freed after 3 s; the request waiting for a slot is counted
(admitting) and, past the stall time, named in the log; a parallel request on a
loaded engine no longer takes the fifo, and one that waits for it stays queued.
Tests with the fake engine.
On an RX 7900 XTX the first long prompt at 8192-token chunks charged ~70 GB of
commit and the engine died (0xC0000409); 7500 and below were flat. A bare auto
now stops at 6144 on Windows HIP builds. A chunk the user names stays, with a
warning from 8192 up.
Windows: the serve start prints the commit left and warns when the planned host
allocations plus a 4 GiB floor do not fit; the conversation cache's admission
reads min(available RAM, available commit).
With every expert resident nothing streams, so the share has nothing to take and
only moves the staged-chunk limit (+4.8% at 1,000 tokens on a V100). An explicit
STRATA_PREFILL_CPU_SHARE is honoured as before; the default is unchanged where
experts are not all resident.
A stage with one layer holds a handful of expert slots (a 12 GB 3060: 512), and the
prompt chunk of every card in the pipeline is clamped to what that cache can lend.
When the pick has such a stage, search again among placements that give every
stage at least three layers; keep the new pick and print both and why. With none
startable the first pick stays. Identical cards pick balanced stages and never
reach the second search.
available_host_bytes() = min(MemAvailable, the room under every cgroup limit above the engine, the operator's cap).
Parking and session save/restore read MemAvailable alone: in a container whose real limit sits on a parent cgroup it
cannot see, MemAvailable is stale (about 25 GiB shown when about 1 GiB was left) and the engine was OOM-killed.
conversation_available_memory() is replaced by available_host_bytes() / sample_host_memory() (the SYCL port's
generate.cpp too). The cgroup term covers memory.max and memory.high (v2) and memory.limit_in_bytes (v1) of the
process's group and every visible ancestor, with clean inactive file cache credited; an unparsable cgroup file makes the
sample unknown (fail closed). --memory-limit-mib / STRATA_MEMORY_LIMIT_MIB names the total RAM the container or cgroup
may use. Refusals say how much was available and from which source; one start-up line reports the limit.
tests/core/memory_guard_test.cpp runs on fixture directories (CPU only). Docs: DETAILS.md.
A profitable shorter chained lookup returned before the stale full-size check, so the full chain could stay excluded for good
although the policy re-probes stale sizes in the other selection paths (#1252). Check a confident stale full size before
returning the profitable shorter chain. The 300-round policy, the EMA, the header and plain lookup are unchanged; a regression
test covers the bounded re-probe spacing and the MTP confidence gate.
0.1.39 took this PR's first part with #583 (the xn / grs count, STRATA_RING_BYTES). What is left: bytes_needed
counted the fused layout's smaller MoE buffers for every caller (fused_layout(T, true)), while init takes them only
when it gets an ExpertSource (fused_layout(T, m.src != nullptr)). A prompt path without one was lent fewer bytes than
its unfused buffers take. bytes_needed, bytes_needed_no_ring and (0.1.40) bytes_needed_owned - STRATA_OWNED_PRICE=exact's
sizing - now take the same src the callers pass to init.
When the best split and a different one (boundaries at least 3 layers away, startable) are within 4% predicted decode window
(STRATA_SPLIT_NEAR_TIE_PCT, 0 = off) and the cards differ in speed or link, print the other K and --layer-split K. The model
prices decode only, not the prompt. Print only: the choice is unchanged.
A prompt read on a layer split with no slot decoding was one run, so the BYIELD of the batch_interleave_test step 4 was
dropped at the end of the read; the long prompt kept its slot and the next BGEN failed with 'ERR BGEN: no such free slot'
(pre-existing since 0.1.40.4). That read now goes in pieces of STRATA_BATCH_YIELD_CHUNKS chunks (default 8, 0 = one run), the
same chunks a single run reads, with the yield check between pieces. batch_read_piece holds the arithmetic; batch_read_test covers it.
A page-locked stage buffer is the source of the cache fill's cudaMemcpyAsync, which the DMA reads after the call has
returned; a pageable one is staged by the driver before it returns. The pool recycles a buffer after kStageAge layers or 256
assemblies, so a 4096-token prompt read (up to ~500 distinct experts per layer) could overwrite a buffer whose copy had not run
yet: the corrupted IQ3_S decode of #1237. note_async_read(blob, stream) records an event per pinned buffer after each queued
copy (hit-path fill, peer fill, remote-expert swap); claim_stage waits for it before it hands the buffer out again.
Pageable buffers and every other source are unchanged. STRATA_STAGE_PIN stays opt-in.
The reasoning ended at the first </think> in the output, also when the model only
quotes the tag while it thinks (`</think>`, "</think>\n\n2" from a test). The rest
of the thinking then came back as the answer, and a tool call quoted in it could
be run.
A </think> right after a quote or backtick and followed by anything but a line
break is now reasoning text; the end of the thinking is followed by a line break
and ends it as before. Until the character after such a tag arrives, the tag is
held like a partial one, so streamed and whole outputs agree. If the turn ends
right after it, it was the end of the thinking: a forced tool call still opens
there, and the #1053 retry is not triggered, as before.
Parking happens on a switch and every parked conversation costs one of
--conversation-cache-slots whatever its length. A client that interleaves
short side requests (an agent's tool-result follow-ups, often under 1k tokens)
with one long conversation therefore spends the cache on the side requests and,
once it is full, evicts the long one oldest-first - the reuse it wanted.
Measured on an RTX 4070 SUPER / 32 GB / Q2_0, --conversation-cache-slots 2,
one 19,198-token conversation interleaved with three ~700-token requests, then
the same conversation again:
min 0 skip nothing, side requests park, L is evicted
re-send L: 0 reused + 19198 read in 41,059 ms (wall 41.4 s)
min 12288 skip parking (703 tokens, below the 12288 minimum) x3
re-send L: 19193 reused + 5 read in 135 ms (wall 0.3 s, 304x)
The default 0 keeps the previous behaviour, so nothing changes without the flag:
wants() answers for the length alone and the gate stays enabled() && wants(),
exactly as the existing byte-budget and RAM-floor skips are. The check runs
before make_room(), so a skipped park neither evicts nor stores anything, and it
returns the same non-error path those skips take.
Tests: conversation_cache_test covers the inclusive boundary, 0, a negative that
never reaches the CLI, and that a disabled cache still answers for length; both
faults were reintroduced by hand to confirm the checks fail on them.
AMD_HIP.md, RDNA4 speed switches: STRATA_PF_FUSED=1 now runs the native
IQ packs' prompt experts on the int8-WMMA kernels on gfx1200 / gfx1201.
Says that it is opt-in and why (it rounds differently, so it is not an
arch default), that STRATA_PF_FUSED_NATIVE=0 turns the native path off,
the start-up line it prints, the R9700 measurements (prompt time, kernel
vs MMQ, first-token KL against the FP16 path, retrieval checks), and
what was not run (gfx1200, Windows, K-quant packs).
DETAILS.md: the STRATA_PF_FUSED=1 sentence says where the native kernels
run on HIP. CMakeLists.txt and gfx_arch.hpp: the comments that said the
native prompt-expert kernels are gfx11 only now include gfx12 (comments
only, no build change).
The native IQ / K-quant prompt-expert kernels (moe_fused_iq.cu,
native_w11_kernel) were gfx11-only; on RDNA4 a pack with
STRATA_PF_FUSED=1 silently kept the MMQ path. They now compile and run
on gfx1200 / gfx1201. Still opt-in: STRATA_PF_FUSED=1 stays off by
default (it changes rounding, so it is not an arch_defaults switch), and
STRATA_PF_FUSED_NATIVE=0 still turns the native kernels off.
Compile gating
- STRATA_NAT_W12 for __gfx1200__ / __gfx1201__, STRATA_NAT_WMMA for
either family. The kernel calls the _gfx12 spelling of the iu8 WMMA
builtin (the un-suffixed one does not compile there). Any other
target keeps the trapping body.
Kernel (same arithmetic, other lane layout)
- gfx12 WMMA fragments are 8 int8 per lane and the two lane halves hold
different k (8*hi .. 8*hi+7), not a replica as on gfx11: B and A are
loaded as uint2 at the matching offsets, and every 16-value k step is
still one WMMA, so the per-16 scales of the IQ2_S / IQ2_XS paths are
untouched.
- A lane's 8 accumulator rows are consecutive (8*hi + i), so a feature's
gate and up are in the same lane: SwiGLU needs no __shfl_xor, and the
down tile is stored as two float4 per lane. NW_DROW(i, hi) gives the
row of element i for the per-row scales.
Host side
- gfx_arch_is_gfx12_wmma(): exact gfx1200 / gfx1201 match, never a
prefix, like the gfx11 helper.
- dev_info() takes the gfx11 list or gfx12, and also checks that the
kernel the build loaded is the real one: the trap stub (a
gfx12-generic or SPIR-V build on a listed card) declares no LDS and
still passes hipFuncGetAttributes and the occupancy query.
- native_supported() reads the STRATA_PF_FUSED=1 opt-in itself:
fused::available() / requested() belong to the Q2_0 pack's
expert_w11_kernel, which is not ported and stays false on gfx12. Off
gfx12 the result is what it was. prefill.cpp fused_ring() follows:
a native pack asks native_supported() alone (it implied enabled()
before), so on gfx12 it gets the fused buffer layout and ring.
- Grid factor per format: gfx12 holds 3 blocks per WGP for IQ2_S / IQ2_XS
gate/up (LDS and VGPRs), 4 for the rest. The occupancy API cannot say
(it models 64 KB of LDS per multiprocessor), so it is a small table;
STRATA_PF_OCC_W=N overrides it. gfx11 keeps 4.
- experts_native() refuses a dm that is not 16-byte aligned on gfx12
(the float4 stores); the prompt path's buffers always are.
- moe_fused.cu compares the gfx11 list with the helper instead of the
"gfx11" prefix (no change on any listed part).
Tests (prefill_fused_iq_test)
- Runs on gfx12 (the skip gate also asks native_supported()), covers the
other four pairs of the Coder IQ1_M pack, S20_E sets the expert count,
and the real-shape check of fused against MMQ is now rel RMS <= 1e-3
and worst row <= 2e-2 instead of rel RMS <= 0.05.
Measured on a Radeon AI PRO R9700 (gfx1201), Coder IQ1_M pack:
- parity: all 10 IQ pairs plus Q4_K / Q5_K pass, rel RMS <= 9.1e-5.
- kernels vs MMQ, 48 layers: 1.48x / 1.20x / 1.14x at 4K / 16K / 32K.
- prompt speed in the engine (server-reported, alternating starts per
arm): 4K +12% to +19% depending on the start, 16K +8.3%, 32K +8.5%.
- first-token KL against the FP16 prompt path is 1.10x MMQ's (48 prompts
of 4K), top-1 agreement 48/48; 4/4 long-context retrieval checks.
Not run on gfx11 or CUDA hardware here.
unsloth/Qwen3.8-Flash-Next-GGUF UD-Q5_K_XL shard 1 holds metadata only and
is 10,946,618 bytes; its 32-byte-aligned data start is 10,946,624, so the
engine refused it with 'GGUF: data section starts past EOF'. llama.cpp's
gguf-py reads the same file (0 tensors, data_offset 10,946,624).
A file with tensors is still refused. A file without them gets data_start
clamped to the file size, so file_size() - data_start() stays 0 in the
payload checks downstream.
On one GPU the prompt path computes the draft layer's K/V for each chunk in batches (Prefill::draft_kv, E-9). A layer
split always ran the drafter's own per-group pass instead, because the drafter sits on the last stage. With
STRATA_SPLIT_MTP_BATCH=1 the last stage's prompt path runs E-9 there. Default off: the default path is unchanged.
The drafter's K/V come from Q8_1 x Q8_0 MMQ instead of its own mmvq, so its drafts can differ; the target's tokens are
decided by the verify window.
Prefill::draft_kv says why it declined (optional out-parameter, also in the SYCL port's prefill.cpp), and the split
logs once what the variable did. Which path runs is a pure header (include/strata/program/draft_kv_plan.hpp) with a
CPU test. tools/split_mtp_batch_parity.py compares the greedy texts of the two arms on a live server. Docs: MULTI_GPU.md.
Two synthetic cases: no tensors and the file ends at its header (should
open; data_start() clamped to the file size), and one tensor with the same
truncation (still refused). The first fails on 0.1.40.3: unsloth's
UD-Q5_K_XL shard 1 is that case.
Header-only placement (include/strata/platform/aux_cpus.hpp): the spare CPU set (the host core's SMT siblings, then
CPUs of physical cores with no worker; never a CPU that shares a core with a worker), the list parser, threads the
engine creates pin themselves, threads somebody else created (the CUDA driver's) are moved by a sweep over
/proc/self/task after the pool exists and at every request; pool workers and the host thread are never moved.
--aux-cpus off|auto|LIST / STRATA_AUX_CPUS; per request strata_tune aux_cpus=0|1 (serve/server.py).
Default off: the hooks do nothing. tests/platform/aux_cpus_test covers the derivation and live threads (CPU only).
Builds on #1190. The layer split sized its lend regions (the RAM copies of the experts the prompt path borrows) with
Prefill::bytes_needed before the split's ring override was applied (Prefill::set_ring_override says it must be set
before any bytes_needed), so with STRATA_SPLIT_RING=384 the prompt path borrowed 5396 slots and the copy kept 4662.
The override is now decided first (decide_split_ring); include/strata/program/split_ring.hpp holds the pure helpers
(split_ring_slots, split_lend_slots - the scan's own tail-slot arithmetic, now one function for the region and the
borrow), src/program/split_ring_test.cpp covers them on the CPU. A start line reports borrowed vs kept slots and warns
when kept < borrowed. Docs: MULTI_GPU.md.
Engine 0.1.40 (fe9c10ca, #848) keeps the resident RAM copy on a layer split -
every stage's cache left out of it, the rest ranked by the whole expert
profile - and --resident-budget-gib runs through the same code
(pin_cache_complement), so the old engine refusal of the budget on a split is
gone. setup.py's split_mmap() got the matching exemption (43c8cfa9), but the
budget's three paths still assumed the old engine (#498):
- split_budget(): a start of a budget config on several GPUs deleted
--resident-budget-gib (and asked for ~135 GB of RAM below that);
- offer_together(): a budget config was offered both cards only with ~135 GB
of RAM, and "on both it has no budget";
- unsloth_together() / q4_split at install: --gpus, or "2" when asked, dropped
the budget, and an explicit --resident-budget-gib kept one GPU.
From RESIDENT_SPLIT_ENGINE the budget is kept in all three, at any RAM. The
recommendations stay as they were (the golden configs are unchanged): one GPU
by default at install, "n" by default when a start offers both cards, --yes
alone one GPU; the split is taken only when asked for.
Tests: the #498 tests run with MIN_ENGINE pinned before RESIDENT_SPLIT_ENGINE
(as test_setup_risk's StartOnSeveralGpus does for #642), three new ones run the
kept budget; tools/test_setup_*.py on Linux: 421 tests, the 2 failures of
test_setup_engine_hash are the same without this change (no ready-made 0.1.41
engine to download in the test box).
Measured with engine 0.1.41 on an RTX 3090 (x16) + RTX 3080 Ti (x4), 91 GB of
RAM, UD-Q4_K_XL, budget 64 GiB: the auto split gives the x4 card layers 0-1
(678 slots) and the 3090 layers 2-47 (4,182 slots); 57.6 GiB of experts in RAM,
no file-tier reads per request; decode 45.0 / 45.1 tok/s (two runs) against
38.4 on the 3090 alone, a 20K prompt in ~18 s on both. Answers checked by hand.
A pip launcher whose Python was removed stays on PATH ('ninja --version'
fails) and stopped every build there, though setup had just installed a
working copy in .venv.
UPDATE.bat currently prints a command using `&&` for users to copy and
run manually. `&&` is not supported by Windows PowerShell 5.1, which
is still included with Windows 11.
Print the two Git commands separately instead.
dpct migrated cudaStream_t ext_stream_ = nullptr to a pointer to the
in-order queue, evaluated at construction on whichever device is current.
Two places then misread it: init() compared ext_stream_ against the
*init-time* device's in-order queue, so a stage built on one device and
init'ed on another adopted the constructing device's queue
(cross-context submit failures, UR_RESULT_ERROR_DEVICE_LOST); teardown's
'cs_ != ext_stream_' ownership test drifted the same way.
Restore the CUDA contract: nullptr means 'no stream until set_stream()',
matching include/strata/core/verify.hpp:319 and src/core/verify.cpp:604.
Follow-up to the >4 GiB GEMM staging.
- gemm_stage freed its scratch as soon as a larger operand arrived, but the
in-order queue is asynchronous: a previous staged GEMM could still read the
buffer. Wait for the queue before the free.
- The staging used dpct::get_in_order_queue() while the GEMM runs on the
descriptor's queue (strata::q_of(stream_)); pass the descriptor's queue so
the copy is ordered before the GEMM for a non-default stream, and so two
Gemm objects on different queues do not share the thread_local scratch.
- Staging that could not allocate fell silently back to the direct (wrong)
GEMM; it now says so.
- device_alloc_map had no unregister, so a freed cache base left a stale
entry that device_offset_end could mis-attribute after the VA was reused.
Add device_alloc_unregister and call it where the expert cache frees base_.
setup reads the Intel cards from sysfs but to_sycl() decided the expert
mode from the driver alone, so a card's device id was thrown away and an
A-series on xe (the A770) could not be told from a Battlemage. Carry the
PCI device id through intel_gpus() into to_sycl(), and treat every
Alchemist id (A770/A750/A580/A380/A310/A770M) as an A-series: it has no
FP64 hardware, so its config carries the driver's FP64 emulation (a kernel
on the sampled path declares double and is refused without it).
On i915 (the A750) the experts still load into a CPU-computed RAM arena
(--mmap-experts + STRATA_VERIFY_NO_HOST=0) - a single pinned host
allocation past ~3 GiB fails on that die. On xe (the A770) the A-series
now takes the ordinary streamed path: the mirror is built in chunks (so
the >3 GiB limit is not hit) and the build passes
-ze-opt-greater-than-4GB-buffer-required, so the prompt path is correct
past the cache's 4 GiB. tools/test_setup_sycl.py passes.
On an Arc A770 every prompt answered "!!!!" (token 0). The experts in the
last 0.46 GiB of the 4.46 GiB VRAM expert cache dequantized to NaN in the
prompt path. The slots' bytes were right (a memcpy read back equal to the
GGUF, and a fresh copy dequantized clean); only kernels reading them in place
got wrong bytes. In a JIT build IGC addresses kernel pointer arguments as a
32-bit offset from the allocation's base.
The JIT (non-AOT) build now passes -ze-opt-greater-than-4GB-buffer-required
to the spir64 backend, in the same argument as the divide/sqrt option (CMake
drops a repeated -Xsycl-target-backend=spir64). Measured on the A770, Coder
IQ1_M, --stream-experts: prompts of 73-542 tokens have 0 non-finite values
under STRATA_DBG_NAN=1, and the 542-token prompt answers right. The AOT build
is unchanged. Speed cost not measured.
oneMKL's GEMM reads the last, partial tile of an operand from the wrong
place when that operand lies more than 4 GiB into its device allocation
(measured on an Arc A770, oneMKL 2026.1: a BF16 B past 4 GiB gave its last
21 columns wrong, none below 4 GiB). The prompt path's GEMMs meet this:
their buffers come from the top of the multi-GiB expert cache / prefill
region. This is the leading explanation for the A770's garbage on prompts
longer than a few dozen tokens - it repros with every expert
device-resident, so it is not the CPU handshake.
Register every big device allocation (malloc_device_guarded, and the
segmented expert cache's reserved VA) in a small registry, add
device_offset_end(ptr, bytes), and in Gemm::bf16 / Gemm::f16 copy either
operand into a scratch buffer of the queue's own (a fresh allocation, so
at offset 0) when its end lies past 4 GiB, then run the GEMM on the staged
operand. STRATA_SYCL_GEMM_NO_STAGE=1 is the negative control.
Not validated end to end: the run that showed the garbage was an
all-resident mirror, which no longer fits in this machine's RAM, and the
runnable --mmap-experts config crashes on this A770 with or without the
staging (exit 139, UR_RESULT_ERROR_DEVICE_LOST - verified a separate,
pre-existing fault with the negative control), so the staging has not yet
been shown to change an answer.
The decode handshake publishes a payload (the layer's activations, the
routed expert ids and weights) and then rings a sequence number the host
polls. The fences alone do not order the ring after the payload stores:
measured on an Arc A770, 14 of 2,832 rings in one run became visible
before the last activation store, so the CPU expert pool sometimes
computed on a partly stale payload. This is the fix the port's own
comments call for (elementwise.hpp: "the host can observe the ring and
then read a payload that has not landed").
The publish kernels now write, next to the sequence number, the ring it
belongs to and a checksum of the payload (the wrapping sum of every 32-bit
word mixed with its position, murmur3's finalizer - a plain sum missed a
reordering, [3,7] -> [7,3], of the expert ids) in a per-ring slot modulo 4,
and the payload is stored with the uncached write hint (a plain store to
host USM reaches the host only when the kernel ends on xe). The host waits
for the whole payload (doorbell_wait_payload) before the pool reads it, in
session_loop and both verify-window loops; the checksum is accepted in
either publish shape (with or without the routing weights), so a variant's
choice need not be plumbed to the reader. The sequence word grows from 1 to
9 mapped words (layer.cpp, verify.cpp) and the ring slot is reset with it.
Note this is a robustness fix, not the cure for the A770's garbage on
longer prompts: that one repros with every expert device-resident (no CPU
handshake) and points at oneMKL's >4 GiB operand bug, which needs the BLAS
staging the CUDA/HIP shims have.
At a long context this port borrows expert-cache slots for the prompt
path. Those buffers are carved from the top of the ~10 GiB expert-cache
allocation, and oneMKL's BF16 GEMM reads a wrong tile when an operand lies
more than 4 GiB into its allocation (docs/INTEL.md). The borrowed operands
hit that, so from the first full prefill chunk the router read garbage.
Measured on an Arc A770: a 236-token prompt failed with "prefill: routed
id out of range", and a 452-token one segfaulted the engine (exit 139),
on both the mirrored and mmap configurations. With the prompt path on its
own buffers (its own allocation, reserved exactly by owned_prefill_mib)
and an automatic chunk capped at 2048 tokens, 452 / 1,748 / 3,476-token
prefill chunks ran to completion with no prefill failure (prompt reads
22 (cold) / 676 / 835 tok/s). Whether the *answers* of that configuration
are correct is a separate, still-open issue: the base port has no doorbell
payload checksum and its longer-prompt path garbles on some inputs, with
or without borrowing.
Scoped to Alchemist (PCI device ids, the same table setup_intel.py uses):
Battlemage's long prompts were measured correct with borrowing, so it
keeps the old default. --prefill-borrow still borrows, for testing a
future BLAS staging fix (the fix that lets borrowing return, as the
CUDA/HIP shims already stage).
Two Alchemist (Arc A-series) fixes for the streamed-expert mirror:
- gguf_expert_source: a single sycl::malloc_host past ~3 GiB fails on DG2,
so the mirror (thousands of experts, tens of GiB) could not be built on
an A770 at all. It is now allocated in <=2 GiB chunks; the per-(layer,
expert) address, the layer's first blob and close() use the chunk
pointers. A cleanup guard frees every chunk on an allocation failure,
a failed read, or any exception; the read phase is unchanged.
- strata-sycl.sh: docker's default memlock is 8 MiB, far below what pinning
the mirror or the resident arena needs; pass --ulimit memlock=-1.
Measured on an Arc A770: the Coder IQ1_M (8,474 experts missing VRAM,
16.15 GiB) mirrors and the engine starts; before, it refused with
"8474 experts are neither in VRAM nor mirrored".
The decode handshake publishes a ring sequence the host polls while the
window graph still runs. On an Arc (xe) a system-scope atomic store, even
with a release fence, is served from the GPU's cache and reaches the host
only when the kernel ends, so a host polling mid-window never sees the
ring. That is why the verify window had to wait for the whole graph
(STRATA_VERIFY_NO_HOST) and every expert had to be device-resident or in
the pinned mirror.
STRATA_DOORBELL_ATOMIC_LOAD already fixes the load side the same way; this
does the store side with the Intel uncached L1+L3 write hint. The release
fence stays (ordering only: a later payload store follows the ring).
Measured with sycl/probe/doorbell_store.cpp on an Arc A770: the host sees
the store at 230 ms with the kernel running to 2030 ms; with the old atomic
store it sees it at 2038 ms, exactly when the kernel ends (exit code 3).
-DSTRATA_DOORBELL_ATOMIC_STORE restores the atomic store.
This is a prerequisite for running with the CPU expert pool on xe (the
per-layer handshake); it does not by itself fix longer-prompt prefill.
psutil.disk_io_counters() returns None when it finds no disk (Modal's gVisor
has no /proc/diskstats; psutil's docs say Windows can need `diskperf -y`).
_disk() then raised AttributeError at the first sample and the sampler thread
ended: /metrics' hardware stayed empty, the Monitor tab had nothing to show,
and the frozen-engine check's gpu_busy() read a GPU load of 0. It now returns
no disk reading and the other readings go on.
The READY-read timeout from #1317 kills the engine, but the request that
was loading it ended with no response: the RuntimeError from __init__
matched none of the request path's handlers, and on a lazy first start
restart()'s finally crashed on slot_cv (created only after READY), so a
misleading 400 masked the failure instead.
- raise EngineDied (still RuntimeError) for the start failure, so the
request ends 503 'did not report READY ...; the next request restarts
it' on the load, restart and /v1/load paths alike
- skip the slot_cv wakeup when a lazy first start never created it
serve/test_ready_timeout.py covers both paths with silent fake engines:
the lazy load (unfixed: 400) and a restart that never reports READY
(unfixed: connection closed with no answer).
#1352 orders an auto layer split faster-card-last because the last stage
runs the head, the draft layer and the verify. On a VRAM-asymmetric pair
that rule can put the heaviest stage on the card least able to hold it:
the reporter's 12 GB 4070 Ti ended up with 31 layers + head + verify and
a 3 GiB expert cache (decode ~58 -> ~44 tok/s, the 'card this small'
warning on).
Skip the reorder when it would put the last stage on a card with less
total VRAM than the one the config placed there (NVML / amdgpu sysfs;
unreadable cards keep the reorder). scores/vrams stay injectable for
tests.
The VRAM reuse stays, but bytes_needed_impl counts bo again by default, so the auto chunk and the borrowed cache slots
are those of 0.1.40.3 and the prompt residuals stay byte-identical (on the RTX 3060 the planner otherwise picked a
6656-token chunk instead of 6400). STRATA_EMB_REUSE_ACCOUNT=1 lets the planner use the saved bytes (+3.9% prompt there);
documented in docs/DETAILS.md.
The previous change skipped the DRM-fdinfo subtraction whenever the driver reported usage. On a single Arc Pro B70 that
made the expert cache 12168 slots instead of 11338 (23.09 vs 21.52 GiB), so a one-GPU run was not unchanged. The
subtraction is now skipped only when the process sees more than one GPU (where it double-counts the other card) and the
driver does not report the whole card as free.
The bound of the device's spins on host flags was a build constant: 20,000 reads for a bmg AOT build, 2,000,000
for anything else. A B70 built without AOT (SPIR-V, JIT: the Windows/OpenCL build, or a Linux build with no AOT
target) got the A750-sized bound. The issue reports a 37% drop in decode between 0.1.40.2 and 0.1.40.3. On a Linux B70 JIT build the two bounds decode
alike (--spec 2, 3 pairs: 19.4 vs 19.5 tok/s), so this makes the bound independent of the AOT flag but is not shown to be that drop's cause.
spin_max(queue) decides per device: 20,000 under the xe driver (the B-series on Linux), 2,000,000 elsewhere (the i915
A-series needs it for the host's per-layer expert work; a Windows/OpenCL B-series build measured faster with the long bound).
STRATA_SPIN_MAX=<reads> overrides it at run time; -DSTRATA_SYCL_SPIN_MAX=<reads> still fixes one bound for the
whole build. The three waiting kernels take the bound as an argument.
- generate.cpp: with a layer split the host mirror walked the later stages' layers too (#1054, fixed in #1111 but
lost in the re-migration): 20 GiB of pinned RAM for experts the other card holds, or one 39 GiB pinned allocation
that fails. Both loops stop at the first stage's last layer.
- expert_cache.cpp: device_free_bytes() subtracted the DRM bytes of every card of the process from the card it asked
about, so on a layer split the second card was charged the first one's cache and its expert cache would not open.
The fallback is for drivers that report the whole card as free (Arc A750/i915); it now runs only then.
Reported by crobe201 with both diffs; MaineTim confirmed the second on 2x B60.
Co-authored-by: crobe201 <[email protected]>
The three sites fixed by the previous commit stored a clamped (d, sum) but still
quantized against the unclamped d, so an overflowing block stored int8 values
that no longer matched its stored scale. They now use q8_1_finite(d) and
q8_1_quant like native_quantize_q8_1_kernel, the swiglu quantizer and the CPU
pool. A block whose amax is finite and below 8.3M is stored bit for bit as
before.
Co-authored-by: Matthew Hines <[email protected]>
Three kernels still emit the (d, sum) pair of a q8_1 activation block
with a raw make_half2(d, sum), bypassing the finite helper the rest of
the tree uses (q8_1_finite.hpp, from 19de7f4 / f6601bf):
fused_gr.cu gr_q8_tail - the fused gate-reduce tail (QFUSE)
verify_kernels.cu gdn_q8_1_store - the batched GDN verify store (QFUSE)
iq_kernels.cu s26_swiglu_q8_1_kernel - the S26 swiglu path
A block whose amax overflows (or that reads a non-finite input) emits
d = inf/NaN; fp16 makes the pair NaN (or the pathological (0,0)). Nothing
downstream tests for it: a NaN scale poisons the fp16 dot of the quantized
activation and every sum downstream of it, and a fully non-finite logit
row ends at the sampler's all-NaN fallback - vocabulary id 0, the '!'
face of the degeneration reported in #606. Same defect class already
fixed at the other emit sites; these three were missed.
q8_1_ds() clamps both halves into the fp16-finite range (finite d,
non-negative finite sum - the sum is a mean of |x|), producing for the
GPU paths the same bits the CPU pool and the clamped GPU sites produce
for the same block. A clamp at the producer, content-blind: nothing
downstream of it is model-text-aware.
Criteria and the wider carried-state RCA: docs/Q8_1_FINITE_DS.md and
docs/DEGENERATION_RCA.md.
2e4ddf6 (the #904 port) removed __restrict__ from the activation pointers of
native_quantize_q8_1_kernel, bf16_f32_mmvf_multi_kernel and
native_mmvq_multi_kernel on every CUDA architecture, and turned on the weight
prefetch before pdl_wait(). Both only matter for PDL (sm_90+). On sm_61 the
missing restrict loses the read-only data path (LDG.CI) and decode halves
(36.4 -> 17.7 tok/s on a Tesla P40, 32-36 -> 17.7-18.6 on a GTX 1080 + 1070).
STRATA_PDL_RESTRICT is __restrict__ and kPdlPrefetch false again for
__CUDA_ARCH__ < 700 in the device pass. sm_70 and newer compile to the same
SASS as before (cuobjdump -sass of native_mmvq.cu and native_bf16.cu at sm_70,
86, 90 and 120 is identical); the host pass is unchanged.
Reported and bisected by paulhothersall and lineape.
2e4ddf6 (the #904 port) removed __restrict__ from the activation pointers of
native_quantize_q8_1_kernel, bf16_f32_mmvf_multi_kernel and
native_mmvq_multi_kernel on every CUDA architecture, and turned on the weight
prefetch before pdl_wait(). Both only matter for PDL (sm_90+). On sm_61 the
missing restrict loses the read-only data path (LDG.CI) and decode halves
(36.4 -> 17.7 tok/s on a Tesla P40, 32-36 -> 17.7-18.6 on a GTX 1080 + 1070).
STRATA_PDL_RESTRICT is __restrict__ and kPdlPrefetch false again for
__CUDA_ARCH__ < 700 in the device pass. sm_70 and newer compile to the same
SASS as before (cuobjdump -sass of native_mmvq.cu and native_bf16.cu at sm_70,
86, 90 and 120 is identical); the host pass is unchanged.
Reported and bisected by paulhothersall and lineape.
Found while running it against a live engine: the line said 0.35 as 48.7 on round 3 while the three draws were 57.0, 48.7 and 35.5, because it printed the running median. Reading a sweep's drift is the whole point of the line, so it has to be the draw. The medians and the ranges are still in the line below it.
Phase 1 measures each PCIe share once and takes the maximum, with the three prompts of that rate all at the one
setting. Neighbours cannot be ordered that way: on this box 0.75 through 0.95 come out at 72.7, 69.0, 71.0 and
71.9, which is 0.2% apart, while the same settings moved 8-28% between engine starts. A value that was worth a
real gain can lose its single sample and never be measured again, because phase 1's winner is the share phase 2
then sweeps spec_min_p at, and the confirm step only puts that one resulting pair against the default.
sweep() measures every value SWEEP_ROUNDS times inside the one engine start, one round visiting all of them and
every other round in reverse order, and the winners come from the medians. Drift inside a sweep then lands on
every value the same number of times, and one bad sample cannot decide. The same function runs phase 2, and the
confirm step uses the same round count instead of its own.
Cost: the sweep multiplies by SWEEP_ROUNDS, 3 by default. A rate is three prompts of 128 tokens, so on this box
(six values, 95-100 tok/s) phase 1 goes from about 40 s to about 120 s, on a run that already loads the model more
than once. A machine whose two leaders stay inside each other's noise can raise the constant.
Not in this change: phase 4 needs a restart per worker count and stays sequential, so a count is still taken from
one measurement each.
Tests: python -m unittest tools.test_calibrate (the stand-in engine is a pure function of the tune keys, so the
medians and the winners are the same as before); tools.test_setup_config and tools.test_setup_golden
PCIE_FRACS stops at 0.75, so setup --calibrate never measures the value that takes the CPU pool out of the decode
window completely. 1.0 is a separate operating point rather than 0.75 carried further: the share is applied as
m = (nmiss * pcie_num) >> 8, so at any value below 1.0 at least one miss still goes to the CPU pool, and only at
1.0 does every non-resident expert come over PCIe. A CPU expert sits on the layer's critical path while the GPU
waits for it, and on a slow CPU that is the whole difference.
On a Core Ultra 9 290HX Plus with an RTX 5090 Laptop on PCIe 5.0 x16, 0.55 gives 51.1 tok/s against 71.3 at 1.0,
with cpu_exp falling from 1.06 to 0.00 per layer-window and the window time falling by the 11.3 ms of PCIe waiting
plus the CPU's expert time. A sequential sweep puts 1.0 at 78.8 against 72.7 for 0.75 and 51.5 for 0.35.
0.9 is added beside 1.0 so a machine where 1.0 is too much gets an interior point. pick() keeps the default unless a
candidate beats it by MIN_GAIN, so this cannot make a calibration worse. The cost is one rate per value, about 8 s
here.
Tests: python -m unittest tools.test_calibrate; tools.test_setup_config and tools.test_setup_golden
With a second toolkit beside the distribution's one, CMake compiled with the chosen nvcc but took cudart and cuBLAS from /usr (Ubuntu 24.04, STRATA_NVCC=/opt/cuda-13.0/bin/nvcc: the engine needed libcudart.so.12 while built with the 13.0 headers). Pass CUDAToolkit_ROOT for the engine and the CUDA image encoder; the distribution's own /usr/bin/nvcc keeps CMake's search as before.
Two gfx906 build failures on v0.1.40.2. Both are one-line gates that know
about the wave32 HIP backend but not about STRATA_HIP_GFX906 -- the same
class as #808 and #1083. Neither is fixed on main (e8ca9afd03).
1) src/prefill/prefill.cpp:399 builds an event with
cudaEventDisableTiming | cudaEventBlockingSync, but the gfx906 compat
header include/strata/platform/hip_compat/strata_hip.h maps only
cudaEventDisableTiming and cudaEventDefault. The wave32 header
include/strata/hip_compat/cuda_runtime.h:39 has the mapping.
-> add #define cudaEventBlockingSync hipEventBlockingSync there.
A diff of the whole set of cuda* defines between the two headers lists
one other name the gfx906 header lacks, cudaDevAttrIntegrated, and that
one is already gated upstream (#if defined(__linux__) &&
!defined(STRATA_HIP_GFX906) in src/core/expert_cache.cpp:31), so it is
deliberately not touched here.
2) Link error: undefined symbol
void mul_mat_q_case<(ggml_type)14>(ggml_backend_cuda_context&, mmq_args const&, ihipStream_t*)
(14 = GGML_TYPE_Q6_K).
src/prefill/moe_mmq.cu:219 calls it under
#if defined(__HIPCC__) || defined(STRATA_Q6K_EXPERTS), so any HIP
compilation references it; mmq.cuh declares the case as an extern
template, so the definition must come from
template-instances/mmq-instance-q6_k.cu. The CUDA branch of
CMakeLists.txt adds that file only under STRATA_Q6K_EXPERTS.
STRATA_HIP_GFX906=ON forces STRATA_ENABLE_CUDA=ON, so gfx906 takes the
CUDA branch while its .cu files are compiled as HIP.
-> widen the gate: STRATA_Q6K_EXPERTS OR STRATA_HIP_GFX906.
This adds one MMQ instance to the static library. It does not enable
STRATA_Q6K_EXPERTS (the grouped Q6_K expert kernels that allocate extra
VRAM), and STRATA_HIP_GFX906 is only ever ON in the gfx906 build, so
CUDA and wave32 builds are untouched.
Verified on a single Instinct MI50 32 GB (gfx906, wave64), ROCm/HIP 7.14,
-DSTRATA_HIP_GFX906=ON -DCMAKE_HIP_ARCHITECTURES=gfx906
-DSTRATA_MMQ_KQUANTS=ON:
- the engine builds and serves UD-IQ4_XS (Qwen3.8-Flash-Next) end to end:
/v1/models, chat completions, tool calls, MTP drafts accepted;
- nm -C shows mul_mat_q_case<(ggml_type)14> in the binary (13 instances:
7 8 12 13 14 16 17 18 20 21 22 23 42);
- change 2 is load-bearing: removing mmq-instance-q6_k.cu.o from
libstrata_mmq.a and relinking with the same link command line fails with
exactly the undefined symbol above (the archive holds one U reference
and that one W definition), while the untouched archive relinks cleanly.
Two variants of the same sweep (code-explanation prompts and Russian prose) on
one PC and one server config, engine 0.1.40.3 built from source for sm_89.
Each folder carries the 0.1.40.2 run, the 0.1.40 baseline and an
identical-binary control run so the noise band is in the report.
Result: no measurable change from 0.1.40.2 on this machine.
Same laptop, model files, configuration and scripts as the 0.1.40.1
entry (2026-10-06-community-rtx-5090-laptop, #1263); only the Strata
version changed. Raw data for 0.1.40.3, medians for 0.1.40.2 in the
README. Decode and prompt throughput unchanged within run-to-run
variation, all needles found, coding check 10/10 on 0.1.40.3. A 0/10
coding check on 0.1.40.2 is traced to a greedy reasoning loop that
occurs in 0.1.40.1 as well (re-runs documented). Results only, no
engine changes.
Coder IQ1_M, one RTX 5080 16 GB, the repository's own Dockerfile and entrypoint
on a Windows host (Docker Desktop 4.84.0 / WSL2), two context limits on the same
loaded model: 131,072 and 65,536.
- Speed: three runs at each of 4,096 / 8,192 / 16,384 / 32,768 / 65,536 /
130,560 prompt tokens (128K limit) and at 4,096 / 8,192 / 16,384 / 32,768 /
65,024 (64K limit), greedy, reasoning off, 256-token output cap, zero reused
prompt tokens in all 33 runs.
- Correctness: tools/needle_bench.py 9 of 9 (32k/64k/128k) at the 128K limit and
6 of 6 at the 64K limit, plus a vision check with the encoder on the CPU.
- Provenance: all three model files SHA-256-verified against the repository's
published LFS oids at the pinned revision; MTP manifest and inventory included.
- Environment: Docker Desktop's default WSL2 memory cap makes setup.py read ~31 GB
of a 64 GB host and silently select the low-RAM mode; the report documents the
.wslconfig change used instead. Also documents the 8-token headroom a request
needs (prompt + max_tokens + 8 <= max_context) and the engine's own VRAM warning
at a 128K context on a 16 GB card.
Results-only; no engine changes.
Nine files following docs/COMMUNITY_BENCHMARKS.md. Engine v0.1.40.3
(tag d5ea713) built from source for sm_75, IQ3_S at the model native
262,144-token window, with --vision and --reasoning-budget-tokens 12000.
Three runs each at 4,096 / 32,768 / 128,000 prompt tokens: decode
70.9 / 77.3 / 71.1 tok/s, prompt 868.9 / 1548.9 / 1626.9 tok/s, medians
in the index and full ranges in the report. needle_bench 6/6 at 32k and
128k. Records that NVLink is present but unused by the engine, so a bridge
on consumer boards is not worth buying.
The flag acts only on --expert-cache-device1..3; a plain layer split got a no-op flag that the docs listed as a reason
for the pipeline and the async tier to switch off. Built on f-141-quick's #1352 pipeline-windows rule (same hunk).
MULTI_GPU.md: the two combine (only STRATA_PIPELINE_ADAPT_ASYNC=0 turns async off); adds the 2x3090 table.
Co-authored-by: adambenhassen <[email protected]>
The function name was everything between `<function=` and `>`, untrimmed. A model that writes a space after the `=`
(seen from Qwen3.8-Flash-Next IQ2_XS in Zed's agent panel) got:
- inside the thinking: the name " capture" matched no declared tool, so the #804 rescue kept the call as reasoning
text; the reply ended with finish_reason "stop" and no tool_calls, and the agent stopped mid-task;
- after the thinking: a call named " capture", which the client does not know, its arguments typed without the
tool's schema.
The name is now stripped where the call is parsed (parse_tool_call), where the #804 rescue checks it against the
declared tools, and where a finished call looks up its schema (_finish_call). A name with surrounding whitespace is
never meaningful, so no text becomes a call that was not one: the #804/#1058 rules still decide whether a call
inside the thinking is an act.
serve/test_tool_name_space.py: both places, streamed and not, fed in 3-character pieces; fails 4/4 before, passes
after. test_reasoning_rescue, test_reasoning_tools and test_frontend unchanged and passing.
NTFS serves unbuffered reads of a mapped file one at a time (WD SN580: 2 MiB reads at QD32
2.35 GB/s mapped vs 3.57 GB/s unmapped). With the file tier unbuffered (--resident-budget-gib
or STRATA_UNBUFFERED_LOAD=1) the mapped view is only a fallback after startup, so it is closed
(STRATA_KEEP_MAPPING=1 keeps it). The prompt path's stager claims runs of consecutive jobs and
reads them with one read_direct batch (STRATA_STAGER_BATCH, default 8 for file-backed blobs).
RTX 5070 Ti, 32 GB RAM, IQ3_XXS, 47.6K prompt, --resident-budget-gib 18 --prefill auto:32768:
1260 -> 1370 tok/s (batch 8) -> 1669 tok/s (view closed).
Ported from #833 onto the rewritten main. Two fixes on the way: unmapped_ is declared on every
platform (mapped_blob reads it outside the Windows block, so Linux did not compile), and
copy_blobs asks resident_blob, which follows the exchange rotation (STRATA_EXCHANGE_ROTATE).
The fast kernels build for CUDA; sm_70 defaults to them, other CUDA cards keep
the plain ones. gr_up_fast_kernel ends with gr_q8_tail like gr_up_multi_kernel,
so STRATA_QFUSE gets its q8_1 image from it too. In the engine (staged read)
only the up projection changes: hc-read0's up 1.08 -> 0.96 ms per window on a
V100-SXM2. fused_gr_bench bitwise, gr_multi_parity OK.
Read off mmvq_il_parity --bench on a V100-SXM2-32GB with the table's own rule
(every measured shape of a class at least 3% faster, else native_mmvq). At 3
columns: the Q5_K head 1094 -> 821 us, Q6_K 12288 rows 68.9 -> 47.9 us, Q4_K
10240 rows 43.9 -> 32.5 us. Bitwise native_mmvq's output (mmvq_il_parity OK).
The AMD grouped-expert layouts (STRATA_EXP_MODE) now build for CUDA too. sm_70
defaults to mode 8 - the codebook grid and the group's q8_1 activations in
shared memory, SwiGLU + q8_1 fused into the gate/up epilogue - every other CUDA
card keeps mode 0. On CUDA, IQ2_XXS gate/up joins the shared-memory kernels,
a format without one falls back to the CUDA layout (not R2), and a strided call
(the verify window's PCIe call) keeps the CUDA layout.
V100-SXM2, setup's IQ2_XS, a verify window's VRAM call (28 groups): IQ2_S
gate/up layers 180 -> 150 us at 3 tokens, IQ2_XXS 170 -> 134 us; bitwise mode
0's output on all 48 layers (native_expert_bench), native_grouped_parity 0
failures in modes 0/2/7/8.
A staged blob (the GGUF role slices assembled into one buffer) is handed to
ExpertCache::fill_slot's cudaMemcpyAsync as-is. From a pageable buffer the
driver stages every copy through an internal bounce buffer - an extra copy at
a fraction of the transfer rate, on the calling thread - so the admit path,
the profile prefill and the lent-slot refills all pay it per expert. Allocate
the pool's buffers with cudaHostAlloc instead, falling back to pageable for a
buffer when the driver refuses (pinned memory is scarce on the small-RAM
machines this path serves).
DETAILS.md's STRATA_PREFILL_CPU_SHARE section: chunks below 3,072 tokens are staged when it is on
(STRATA_PREFILL_CPU_SHARE_MAX=1024 keeps the old limit), the experts the CPU may take (the arena, the
page-locked copy, the mapped experts.bin's pages), the layer split, and a table measured on an RTX 5070 Ti
alone and an RTX 3060 + RTX 5070 Ti split (PCIe 3.0 x8, DDR4-3200, --mmap-experts): 512-token prompts
-49% / -41%, 3,000-token -22% / -24%; a recorded 100K-token coding conversation on the two cards 137.4 ->
130.6 s.
STRATA_PREFILL_CPU_SHARE handed the CPU pool its share of a chunk below 1,024 tokens only, only experts in
page-locked RAM, and only on one GPU. With it set:
- the chunks staged after their routing (the only ones the share applies to) go up to
STRATA_PREFILL_CPU_SHARE_MAX tokens (default 3072, at least 1024) instead of 1024; Prefill::arm_cpu_share
sets the limit before any loan is sized, and init sets it too;
- an expert whose blob the CPU reads from RAM without assembling it (the arena's unpinned part, the mapped
experts.bin's pages) can go to the CPU, not only a page-locked one; the blob pointers are taken when the
experts are chosen, and one without a stable blob stays on the GPU;
- on a layer split every stage has the pool (serve, no --batch), and one stage at a time takes it for a
chunk while the other reads its chunk on the GPU alone.
Unset, nothing changes.
auto balanced the CPU's time per expert against the GPU's per streamed expert, both measured with the share on, so
nothing compared a layer with and without it. On jase100k's RX 7900 GRE + Ryzen 7 5700X3D the host's time per
streamed expert rose 107 -> 200 us with auto (0.47): the streaming side saved nothing (0.53 x 1.87 = 0.99) and the
CPU's work came on top, -4.5%. Now every eligible layer is timed with CUDA events from before its routing sync to its
combine, per non-resident expert; adjacent layers alternate no share / the measured share until three ratios are in,
then auto shares while the median of the last five is below 1, with one layer in 29 in the other arm. Layers with a
one-time cost and the first layer are left out. STRATA_DBG_CPU_GATE=1 prints each measurement.
RTX 5090 + 9950X3D, IQ2_XS, 250 / 600 / 900 tokens against off: DMA streaming -9.8/-9.9/-10.4% -> -8.8/-9.1/-10.5%;
an emulated host-copied stream -18.0/-16.7/-14.4% -> -16.1/-15.2/-13.0%; one core + one worker (where the old auto
lost up to +11.5%) +1.4..+2.4% -> +2.0..+2.3%, the cost of the first measurements. Unset: byte-identical.
The parking path has two gates in series: make_room() balances the cache's own budget, then conversation_memory_admit() checks the host's free RAM against the snapshot plus --conversation-cache-min-free-mib. The second was final: a failure dropped the snapshot even though the cache held conversations whose RAM it could have, and the next request read its whole prompt again (270k tokens, 96 s, measured by the reporter in #1347).
Evict the least recently active parked conversations until the gate admits the snapshot, bounded by the slot count; entries that hold a pinned shared prefix are not evicted. The failure line says how many were evicted and how many remain.
Reported with the fix by alanthinker in #1347.
The 16 dense GEMM geometries a Qwen3.8-Flash-Next prompt logs on a Radeon 890M (gfx1150, Strix Point) at
T = 64 .. 16384, 144 rows, calibrated with tools/hip/tune_hipblaslt.cpp against AMD's TheRock ROCm 7.14.1 for
gfx1150 (hipBLASLt 1.4.1 cd957402). Two passes back to back; each row keeps the solution with the lowest median of
all six repetitions (a single repetition stalled now and then, and the tool's mean-of-three picks differed in 26 rows
between the passes).
Every row beats plain hipBLAS, 1.55-22.6x per GEMM. End to end on GSQ-RCO IQ3_XXS, three interleaved rounds: prompts
read 1.5-1.7x faster (3.6K tokens 124 -> 216 tok/s, 7K 130 -> 226 tok/s), decode unchanged; hip_prefill_hipblaslt_gemm
passes with it and a server run logged no fallbacks. Setup does not install for gfx1150, so the table is set by hand
(STRATA_HIPBLASLT_TUNING). Method and data: bench/results/2026-10-07-community-gfx1150.
A verify window's graph waits on the GPU for flags the host raises once
cudaGraphLaunch has returned. A blocking launch returns only when the
graph has finished, so with CUDA_LAUNCH_BLOCKING=1 the first window after
a prompt never ends and the watchdog stops the engine, whatever the prompt
length, GPU count or --pipeline-windows. The engine now says so at start,
and the watchdog's line names it. Nothing changes without the variable.
A pipelined group's window spans its slots up to its last busy one, and the engine keeps no pipelined slot as a
conversation cache, so the least-recently-used pick left leading holes ([_ B]) that cost a whole row each. pick_slot
now fills such a hole first, then follows the spreading order. The engine reports INFO batch_groups so the server
knows the groups really run pipelined. serve/test_slot_alloc.py covers the allocator.
Co-authored-by: blange48 <[email protected]>
The miss cost (190 ms per unit of routed mass, fitted on x16 links) is one
constant for every stage, so --layer-split auto cannot see an asymmetric
pair: on 2x 4090 D with CUDA1 on a physical x1 link (1.6 GB/s measured) it
gives that card 26 layers (K=22) and reads a 29K prompt at ~350 tok/s.
Probe each stage's link inside the search (probe_pcie_h2d_gbps; the
stage-setup probes are skipped when --pcie-frac is given, and the search
needs the reading either way) and scale that stage's miss term by
clamp(20 GB/s / bw, 1, 64). 20 GB/s is pcie_frac_for_gbps' x16 reference;
the lower clamp keeps a symmetric machine on the exact formula the sweep's
K was fitted with (every scale 1.0 -> the objective is unchanged).
With it, auto picks the fit-in-cache boundary on that machine (K=38: the
narrow card's cache holds all of its layers' pairs, zero streaming there)
and reads the same prompt at ~1410 tok/s, ~4x.
The 190 ms mixes the decode CPU-pool cost with the prompt link cost;
scaling the whole per-stage term is the first-order fix (splitting the
constant would need a fresh fit on both a narrow and a wide link).
STRATA_PF_FUSED=1 reaches only chunks of stream_all_min() tokens or more (1,024, measured on a discrete card); a
smaller read runs its experts through MMQ. On Strix Halo every expert is resident and MMQ is ~3x slower on a
~1,000-token read, the size of an agent's turn: 441 / 855-token reads 3.1 / 4.0 s -> 0.9 / 1.2 s, a 12-turn agent
replay 3.4 -> 2.1 s per turn (C2T8, --no-prefill-borrow). It changes bits (the fused path's output, which reads of
1,024+ tokens already get), so section 5: teacher-forced KL against today's path 0.016 on reads under 1,024, the same
as fused vs MMQ on longer reads (0.019); reads of 1,024+ unchanged (KL 0).
Setup installs the ROCm 7.14.0a20260608 wheels for gfx1151 since 0.1.40.2 (#1267); their hipBLASLt is 1.4.0
(100400), which neither gfx1151 table matched, so setup's engine read the prompt through plain hipBLAS.
tune_hipblaslt --workspace-mib 32 on the 100401 table's 90 shapes, on a Ryzen AI Max+ 395 (Radeon 8060S, gfx1151,
128 GB, Fedora 44, kernel 7.2.8) with those wheels: 3.1-14.1x faster than plain hipBLAS per shape (median 6.2x).
hip_prefill_hipblaslt_gemm and hip_prefill_hcd_exact_parity pass with it (fallbacks=0); HIP ctest 61 of 62
(ple_parity needs the Q2_0 fixture). setup picks it up with no code change.
Below N of N lendable slots the uncovered experts are read from the pack during a long prompt (reporter: 18% uncovered = -26% prompt speed on gfx1100). A WARNING line now says so, and docs/AMD_HIP.md documents the headroom knob and the levers from the report. No behaviour change otherwise.
The gated delta rule in the WY form flash-linear-attention uses, all FP32: gdn_chunk_prep_kernel forms k.k / q.k and
T = (I + A)^-1 for every chunk of a 2048-token super-block at once; gdn_chunk_scan_kernel (128 blocks, a key head x
16 value columns each) walks the chunks with its state slice in registers. Every exponent <= 0 (the gates are
negative). The scratch (25.6 MB) is allocated once per device: freed and reallocated per call it cost more host time
than the kernels saved on short prompts. sm_80+, >= 128 SMs, enough shared memory, chunks of >= 128 tokens; else
gdn_rec_kh_kernel. Unset: unchanged.
gdn_rec_parity: against gdn_rec_kh_kernel <= 1.13e-6 (T = 1..4099, 8192, 32768, a non-zero starting state); against
an FP64 recurrence <= 1.05e-6 (the sequential kernel <= 3.2e-7) - FP32 rounding; 199 restarts of 7 shapes bit for
bit (NaN-filled scratch between). RTX 5090: 1.81x the recurrence at 2K-32K; IQ2_XS: the recurrence phase of a 32K
prompt 520-532 -> 306-334 ms (-210 ms, -3.7%), 8K -2.1..-3.6%; first-token KL 0.0016-0.0152, 1.15-1.83x a harmless
control arm (STRATA_PROMPT_ATTN_V1), the same top token.
The MoE input and the routing weights go to host memory on a stream beside the primary's compute stream, so its
own experts start at once instead of behind the copies over its link (mixed_h and w are rewritten only after this
layer's combine, which waits for the peer). STRATA_PF_PEER_XSTREAM=0: the compute stream. With
STRATA_PF_PEER_RING=160, prompt tok/s: 100K 1,854 -> 1,918; 8K and 3.4K unchanged.
Each group's tokens and their row lists (in the experts' order) are built on the host from the rows' pairs and
uploaded once per layer; peer_gather_add is eddoursul/Strata's moe_gather_add with the weight looked up through
the row's pair, so the sums are bitwise those of the per-expert launches. STRATA_PF_PEER_GATHER=0: per expert.
Prompt tok/s against the per-expert launches: 3.4K 825 -> 832, 8K 1,468 -> 1,500, 100K 1,821 -> 1,858.
Co-Authored-By: Eddoursul <[email protected]>
The MoE input goes out as the FFN input's existing FP16 image (mixed_h) and the per-token sums come back as
FP16, saturated at +-65504 (eddoursul/Strata f8de703's FP16 input, 2acfac4's sums_to_f16): half the bytes over
the primary's link both ways. STRATA_PF_PEER_F16=0: FP32.
Peer stream 0.6, prompt tok/s: 3.4K 805 -> 831, 8K 1,284 -> 1,495, 100K 1,578 -> 1,855 (the share off: 1,712).
The 8K and 3.4K answers stay byte-identical to the share off.
Co-Authored-By: Eddoursul <[email protected]>
The design is eddoursul/Strata's second-GPU prompt path (f8de703, 2acfac4, e8ade66): the second GPU adds its
experts' rows into each token's weighted sum and the first GPU's combine adds that to its own experts' rows.
Over the host route the peer scatter-adds its rows into per-token sums (peer_scatter_add, after the fork's
moe_scatter_add: one launch per expert in a fixed order, so the sums are repeatable) and sends T x N floats
back instead of rows_peer x N (up to K = 10 times more); moe_combine_peer adds them to the primary's rows. The
2a row buffers are not allocated (3.4 GB of pinned RAM at 32K-token chunks). STRATA_PF_PEER_SUMS=0: rows.
Same setup, peer stream 0.6, prompt tok/s against the share off: 3.4K 332 -> 805, 8K 762 -> 1,284, 100K
1,712 -> 1,578. The 8K and 3.4K answers are byte-identical to the share off (greedy, 256 / 3,221 tokens).
Co-Authored-By: Eddoursul <[email protected]>
set_peer refused a peer that cannot access the primary directly, so on GPU pairs without P2P (most GeForce
pairs) the prompt path stayed on the primary GPU. Without P2P the share now takes the route the layer split's
helper already uses: the activations and the result rows through mapped pinned host buffers read by copy
kernels, with the compact group buffers (the layer-sized path copies over P2P only). STRATA_PF_PEER_HOST=0
restores the refusal.
RTX 3090 (PCIe 4.0 x4) + RTX 3080 Ti (x16), no P2P, UD-Q4_K_XL, --prefill auto:32768, peer stream 0.35 / 1.0,
prompt tok/s against the share off: 3.4K 332 -> 492 / 551, 8K 763 -> 907 / 776, 100K 1,713 -> 1,072 / 734.
The peer's rows coming back over the primary's x4 link cost more than they save on long prompts; the next
commit sends sums instead.
- tools/check_isa_guard.py disassembles the CPU kernels (objdump + nm on the Linux kernel library, capstone + the /MAP
file of a linked binary on Windows) and fails when a function holding AVX-512 / AVX-VNNI-class instructions is
not a kernel of a wide-ISA file: a weak or COMDAT copy (the linker may pick it for every caller), a std:: /
template instantiation, a probe or helper that every CPU runs, or code in another object. MSVC's own
`__isa_available` vectorizer guard is accepted. ctest isa_guard_check runs it; exit 77 (skip) when the tools
are missing.
- -DSTRATA_SDE=<sde> adds sde_{hsw,adl}_* tests: the CPU parity tests on an emulated Haswell (AVX2 only) and Alder
Lake (AVX-VNNI, no AVX-512); SDE stops at the first instruction the chip lacks.
- tools/test_check_isa_guard.py: unit tests of the policy.
expert.cpp and iq_avx512.cpp are compiled for AVX-512, iq_avx2.cpp and kq_avx2.cpp for AVX2, and a file compiled
that way may use the wide ISA anywhere in its code. Code that every CPU runs was still defined in them:
- cpu_features, CpuFeatures::reason, cpu_require_expert_support (the function that finds out whether the CPU has
AVX-512) and expert_set_oracle_q8_0 / expert_oracle_q8_0_enabled (the first call of every generate, on any CPU)
were in expert.cpp;
- iq512_supported, iq256_supported and kq256_supported (a comparison native_gu_rows asks before it has checked
what the CPU can run) were in the kernel files; iq512_supported was called on every AVX2-only CPU for every
gate/up call.
They now live in expert_layout.cpp and native_expert.cpp, which are built for the baseline. No kernel changes;
the compiled code of the moved functions is identical, so default output is byte-identical.
RX 7900 XT 20 GB, IQ3_S, Windows HIP, auto cache, default reserve 700: 0.1.40 ended with 4,255 slots (8.12 GiB) and 2,170 MiB free, 0.1.40.2
with 5,512 slots (10.48 GiB) and 323 MiB free; WDDM paged and decode fell from 51.7 to 21-25 tok/s. With --vram-reserve-mib 2600: 4,299 slots
(8.20 GiB), 2,224 MiB free, long prompt 1,015.8 -> 1,355.9 tok/s (+33.5%), decode 24.3 -> 55.5 tok/s (+128%), back inside 0.1.40's ranges.
A reserve given on the command line is kept; a smaller card keeps its own sizing. The cache size changes no output bit.
amdhip64_7.dll imports rocm_kpack.dll, which imports the MSVC runtime. Only the two runtime DLLs were copied beside the exe, so a
full-path load of the bundled one failed with error 126, Windows fell back to System32's copy, and big prompts died with
hipErrorInvalidDeviceFunction (RX 7800 XT, gfx1101). The closure is now read from the PE import tables of the files in the zip
(tools/hip/dll_closure.py, no pefile), by setup.hip_runtime_beside_exe and tools/hip/package_windows.py. On the 0.1.40.2 HIP zip it is
amdhip64_7, amd_comgr, rocm_kpack, msvcp140, vcruntime140, vcruntime140_1. The engine, when the loaded runtime is not the one beside it,
also tries LoadLibraryEx on the bundled one and prints GetLastError. Diagnostic only. Not tested on a Windows AMD card.
The native router reads 512 logits a row; the window path two lines above already tested n_expert == 512 and K == 10, the
per-token fallback tested only native_router_enabled(), so a model with fewer experts read past each row. It now takes the
generic router_top10 like its siblings. native_multi_parity checks the generic router on 256-wide rows.
Reported-by: rwkeyes (#1357)
0.1.40.2 turned fork F4 on by default after single-GPU measurements (+4.2% on an sm_86, +1.3 to +2.2% on an sm_120). A
two-GPU Windows layer split (RTX 4080 SUPER + RTX 5070 Ti) was reported 15% slower with it on (#1373). The outputs are
bitwise the same either way, so only speed changes. STRATA_MMVQ_IL=1 turns it on; docs/DETAILS.md says why.
samp_acc_t is float by default (-DSTRATA_SYCL_SAMPLER_FP64=1 keeps the reference's double). On an Arc A750 one row of the
tail cost 451 us in double and 20 us in float; a sampled decode ran at 7.62 -> 8.10 tok/s (medians of 5 interleaved
pairs, runs 7.1-9.2). sampler_parity: 0 failures on the A750 (JIT build) and on the B70; both cards' 10-prompt gates pass
10 of 10, and the B70 gate's sampled cases pick the same tokens as before. Of the kernels that use double, only the
sampler runs on a native pack (the router, combine, gate and norm kernels there are float).
Probes: sycl/probe/fp64_cost.cpp (FP64 against float in the small reduction kernels), gemm_vary.cpp.
docs/INTEL.md: the aliased-pages bug, NO_HOST, the prompt-chunking numbers, draft-length sweep, FP64 and XMX findings;
docs/INTEL_ARC.md: a row for the B70/A750 runs.
With STRATA_PREFILL_CPU_SHARE on, the last rows_cpu rows of GU and H hold whatever the buffer held before; the check read
them and reported them as non-finite (about 1 in 256 garbage floats), with Dm and the layer output clean.
blob() bumps a plain reads_ counter and, for a file source, its mapped/staged paths; calling it from the share's
thread raced with the prefill thread's own blob_stable()/pinned() calls (the streamed experts). The blobs of the CPU's
experts are now taken before the thread starts, so the thread reads only the arena bytes, the activations and the
grouping tables, none of which the prefill thread writes until the thread is joined.
A chunk below stream_all_min() (1,024 tokens: an agent's tool result, a test's
output) streams every routed expert outside VRAM over PCIe while the decode CPU
pool sits idle and the arena holds the same experts in RAM. Opt-in,
STRATA_PREFILL_CPU_SHARE=auto|x:
- the non-resident experts routed by at most MAXT of the chunk's tokens,
fewest first, run on the pool on a thread while the GPU streams and runs the
rest; their unweighted rows go to Dm's tail (as a peer GPU's do) and the
combine weights them as any other row;
- auto: the share measured, each layer's CPU time per expert and the GPU's per
streamed expert (CUDA events around its expert work, read after the next
layer's routing sync), and the next layers hand the CPU g / (c + g), where
both end together - a small CPU takes a small share;
- Q2_0 layers take the pool's Q2_0 kernels' activations (ActQ, as decode's),
i-quant layers the native vec_dot type.
Serve without batch slots and generate, one GPU. Unset (the default): no
expert moves, byte-identical to v0.1.39 (IQ2_XS 600 and 2,100 tokens, fixed
cache, logits and 32 tokens).
RTX 5090 + 9950X3D, 600-token prompts, ms (off / auto):
- UD-Q4_K_XL: 1,548 / 1,369, 1,528 / 1,296 (share 0.65; KL between 0.006);
- Q2_0: 472 / 445, 491 / 448;
- IQ2_XS: 493 / 481, 502 / 460, 527 / 529; 250 tokens 424 / 355, 402 / 382,
379 / 367 (KL to STRATA_PREFILL_MMQ=0 0.0053 off, 0.0080 auto);
- IQ2_XS on 2 AVX2 workers (emulated): 512 / 487 (share 0.49).
Every prompt chunk streams the experts the VRAM cache does not hold, over PCIe, and a 4K prompt touches nearly all 512
experts of a layer. The short first chunk (STRATA_PREFILL_FIRST, 256 tokens, there to start the GPU while PLE rows are
read) therefore cost a whole extra pass of that stream whenever the cache holds only part of the model. Its default is
now 256 only when the cache holds every expert, else 0 (an explicit STRATA_PREFILL_FIRST still decides).
Arc Pro B70 32 GB, IQ3_S (11.5k of 24.6k experts in VRAM, the rest in the pinned mirror at 13 GB/s), a 4,095-token
prompt, MTP on, interleaved runs, medians of 3: 2048-token chunks 618 -> 730 tok/s, 4096-token chunks 784 -> 980.
sycl/setup_intel.py now writes --prefill 4096 for a card with 24 GB or more (it costs ~700 expert cache slots, about 3%
of decode on the 200-token story).
An Arc Pro B70 (xe) returned the 22 GiB expert-cache arena with two of its 2 MiB pages mapped onto the same memory:
a write to the page at +2 MiB showed at +1022 MiB, so cache slots there held other experts' bytes and the prompt
came out NaN at the first layer that routed to one of them (layer 4, expert 249). Which pages alias depends on the
allocation history (the context size, the MTP window), so it comes and goes between runs.
strata::arena_alias_check tags every 64 KiB of a fresh range and reads the tags back in a second kernel;
strata::malloc_device_guarded allocates again behind a growing spacer until the range is clean (and warns if it
never is). It guards the expert cache arena and every device allocation of 32 MiB or more (session arena, weights,
MTP, verify, head, prefill). STRATA_ARENA_ALIAS_CHECK=0 skips it. Two debug aids: STRATA_VERIFY_ALL_SLOTS=1 (with
STRATA_VERIFY_FIND=1, STRATA_VERIFY_DUMP=dir) reads every filled slot back and compares it with the GGUF, and the
probes sycl/probe/arena_rw.cpp and alias_scan.cpp.
A blocking read of the engine's READY line or of the vision encoder's pipe that
never returned held the request turn for good: the engine sat idle in its command
loop, so no watchdog could fire and every later request queued behind it (the lost
step class of #481). The READY read now has a 900 s timeout, the encoder's READY
and ENC reads 300 s each. A process that says nothing for that long is killed, the
read raises, and the next request starts a fresh one. Slow loads and slow encodes
stay far inside the limits.
Only the read timeouts of the reporter's patch are taken. The watchdog that ends
an engine after 90 s without output and CPU progress is left out for 0.1.41.
Tests: a silent engine, a silent encoder at READY and at ENC, and an in-time answer.
Reported-by: btc1000w
Co-authored-by: btc1000w <[email protected]>
A hint only: it never enables the kernel and says the output bits change. Silent when the variable is set to anything. Follows the R9700 numbers of PR #1318 (+31% prompt at 8.8K).
I calibrated a table for hipBLASLt 1.4.1 with tune_hipblaslt over the 32 geometries of
gfx1201-hipblaslt-100500.txt (R9700, system ROCm 10.1.0, hipBLASLt 1.4.1): 1.15x geometric
mean per GEMM over hipBLAS, but only 1,606 -> 1,613 tok/s median prompt over three matched
trials (a 1.008x geometric mean) and decode unchanged, so within noise - which is what the
R9700 paragraph above already reported, now from a second machine. The cold first prompt of
each start is left out of those numbers.
I dropped two rows from it: bf16 N=10240 K=320 ldy=10240 at T=4096 and at T=8192. hipBLAS
won T=8192 in five separate runs (0.609-0.619 ms against 0.648-0.677 ms for the best of 16
candidates), and dropping only the T=8192 row would send that call to the T=4096 row rather
than back to hipBLAS, because closest() takes the nearest T bucket.
Two calibration traps I ran into are recorded too: the first case of a fresh tune_hipblaslt
process times its hipBLAS baseline while the clocks are still ramping, and the first fresh
prompt after a model load is not a measurement (949 vs 1,423 tok/s in the two arms there).
On my R9700 (engine 0.1.40, IQ3_S at 431072 ctx, --kv int8, the hipBLASLt 1.4.1 table of
the previous commit, GPU idle): fresh prompts of 4,210 and 8,830 tokens, three matched
trials each and the cold first prompt of each start left out - 1,571 -> 2,064 and
1,568 -> 2,065 tok/s at 8,830 tokens (+31.4 and +31.7%) and 1,699 -> 1,990 at 4,210
(+17.1%), medians 1,571 -> 2,064, a 1.27x geometric mean over those three - with decode
unchanged (89.8 -> 86.9 tok/s). hip_prompt_attn_wmma passes on this card.
STRATA_SELECT_WMMA=1 on top of it changed nothing at 4-9K prompts (2,026 tok/s), which is
consistent with its +1.5% being a 16K/262K-ctx number.
The shipped gfx1100-hipblaslt-100401.txt calibrates "the 26 dense GEMM
geometries of the shipped gfx1100 table", all at T=4096/8192. A prompt on
Qwen3.8-Flash-Next reads a whole other geometry through the prompt path, and
it is issued once per expert group at whatever T the group has - so it never
matches a row:
STRATA_HIPBLASLT_VERBOSE=1, GSQ-RCO IQ2_XS, 192000 ctx, 3151-token prompt
hipBLASLt tuning enabled (26 rows, gfx1100, version 100401)
642 fallbacks, all f16 N=1280 K=2560 ldy=1280, over 642 distinct
T values from 1 to 1873
That is the small-T expert GEMM, and it is nearly every launch in the prompt.
TuningTable::closest() matches on (n, k, ldy) and takes the nearest t_bucket,
so the rows below are a spread of buckets over the small-T range rather than
one row per T; bf16 10240 2560 10240 (the hyper-connection projection, one
fallback) is covered the same way.
tools/hip/gfx1100-hipblaslt-100401.txt: 17 rows added with tune_hipblaslt
from this tree, against the engine's own 32 MiB workspace (gemm.cu:441), on
the same RX 7900 XTX and packaged ROCm 10.0.0 the existing rows were
calibrated on. No existing row is changed. 26 rows -> 43.
Same binary, same config, the table as the only difference, 3 interleaved
pairs at a 3151-token prompt:
691.6 -> 940.7 tok/s (+36.0%)
712.3 -> 943.2 tok/s (+32.4%)
727.7 -> 928.7 tok/s (+27.6%)
mean 710.5 -> 937.5 tok/s (+31.9%)
Decode unchanged (58.6 -> 60.6 tok/s, inside this box's 2.3% run drift), and
STRATA_HIPBLASLT_VERBOSE=1 reports fallbacks=0 with the 43 rows, which the
26-row table does not.
The pick becomes argmax_i p_i / E_i with E_i = -log(u_i) and u_i a hash of (seed, counter, token id), an exact
sample of p. The noise a token gets does not depend on which other tokens survived the draft's or the target's
cut, so a coupled draft (STRATA_SPEC_COUPLED=1) agrees with the target on the tokens they share. Off by default:
nothing changes without the variable. sampler_parity: kernel vs host reference under three chains, frequencies
vs softmax, the flag is read and greedy is unchanged.
Ported from #904 onto the 0.1.40 verify window. Every switch is OFF by default, so
default output and the default launch sequence are unchanged:
- STRATA_DF_PDL=1: programmatic dependent launch (pdl.hpp) for the window's
quantizations and dense projections (multi-column MMVQ, multi-row BF16 GEMV);
sm_90+ only, CUDA 12.3+; HIP and older cards keep ordinary launches.
- STRATA_DF_PLE=1: the window's hyper-connection write and PLE post-ops in one
pass, which also writes the commit's history snapshots (native_gr_post_multi
with strides, native_ple_postops_batch_snap).
- STRATA_DF_QB_Q4=1: a Q4_0 KV cache takes the batched QSA query path too.
- STRATA_DF_BRANCH=1: the mixer's side branches inside the window graph.
Left opt-in because the author saw a stall on Linux with NVML queries.
The q/gate split now uses copy_rows_strided (a kernel) instead of a
cudaMemcpy2DAsync so a PDL chain stays kernel to kernel; the bytes are the same.
Left out: STRATA_DF_KVAPP (kv_append_q4_step_multi). 0.1.40 already appends a
window's K/V cells in one launch (kv_append_q4_steps, #783 PR-d).
Merged by hand with the 0.1.40 forms of the same code: the exact-T BF16
multi-token kernel, the PAIR/TS MMVQ variants, QFUSE (q8_attn), the fused
norm+rope and the batched residual write.
Co-authored-by: Francesco Albano <[email protected]>
(PR #904)
The high index bits of a 32-value half are spread one per byte by a table and
punpcklbw puts them next to the low index bytes, so each grid lookup is a
16-bit load plus the table load. The sign vector is one broadcast load plus one
pshufb, and the (2s+1) scale vectors are table loads. Arithmetic is untouched
(same words in the same lanes). The decode is ported onto the 0.1.40 layout of
iq_avx2.cpp: the gathered IQ3_S/IQ2_S forms (Fmt32<121>, <122>) and the VNNI
rows are left as they are.
Co-authored-by: Stefanos Anastasiou <[email protected]>
(PR #930)
Opt-in (only with --expert-cache-per-layer). The same VRAM holds more experts
(7,008 -> 9,312 slots at equal VRAM per the author). --expert-cache N stays the
budget of N largest blobs; shrink_to keeps whole per-layer lists so a blob never
lands in another layer's range. The free-VRAM reading is device_free_bytes(), as
in the shared-cache branch. The PR's png is left out.
(PR #934)
Opt-in (default 0 keeps three tasks per participating thread, so default
output and timing are unchanged). Adds pool_tasks_test, which compares the
split against the default partition bitwise for 0..4096 tasks.
(PR #949)
The prefill Stager waited by spinning: a yield loop on `issued`, and
cudaEventSynchronize on events created without cudaEventBlockingSync
(which spins under the default scheduler). With --mmap-experts there
are 32 stagers. On Linux they inherit the host thread's single-core
pin and starve it: on a DGX Spark (GB10) prompts were read ~10x slower
(IQ3_S 8K 91 -> 1,222 tok/s, 30K 88 -> 1,388 tok/s with this change).
Unpinned instead, the 32 spinners took every core for the whole prompt
(process CPU ~1,800%); with the waits sleeping it is ~100%.
- ring wait: std::atomic::wait, with notify_all in issued_one() and
finish()
- dma_done events: cudaEventDisableTiming | cudaEventBlockingSync
- HIP shim: cudaEventBlockingSync -> hipEventBlockingSync (untested)
Decode speed and output tokens are unchanged.
(PR #1057)
ExpertCache::admit never evicts because eviction policy is a measured
question (R4.1's LFU-decay vs LRU sweep). This is the harness for that
measurement: a policy's hit rate depends only on the order (layer,
expert) pairs were routed, which --dump-routing records, so replaying a
trace through simulated slot pools needs no GPU. fill-only (the
engine's current behaviour), lru, lfu-decay and an oracle (Belady's
MIN, the upper bound); global or R4.2g per-layer scope matching
ExpertCache::layer_slot_range; any slot count; optional profile prefill;
per-window rates, so a topic shift shows up as a per-window dip.
tools/test_bench_eviction.py runs without a GPU (14 cases): each
policy's victim choice, the per-layer quota arithmetic, the windows
split, and profile prefill order.
(PR #991)
A one-int-a-column interleaved copy of the window's q8_1 blocks lets a warp decode each weight block once and apply it
to every token with the single-column dot's own expression (the same *_q8_dot_impl calls), walking the chunks of the
exact multi-column layout and adding the four virtual warps' sums in warp order, so every output is bitwise the
multi-column kernel's. IQ4_XS, Q4_K, Q5_K and Q6_K projections (and the head) of 2-4 tokens use it; the copy is made
from xq_ by one small kernel, so the fused HC-read producer works too. STRATA_MMVQ_IL=0 keeps native_mmvq's kernels.
CUDA only (HIP keeps its kernels). mmvq_il_parity checks every case bitwise for rows 1/2/4 and the table; --bench times it.
Ported by hand from Eddoursul's fork (738e166, b67a291), with the IQ3_S and Q8_0 parts and the 5-8 column widths left out.
Co-authored-by: Eddoursul <[email protected]>
kvg_trim took only the short request's cell count, so every short request in
a long conversation's session shrank the K/V to 8192 cells and gave the
expert slots back, and the conversation's next turn grew it again and moved
the experts again (a parked conversation is restored through kvg_ensure).
With STRATA_KV_GROW_HOLD=1 the trim keeps the longest parked conversation's
cells (+256) while it is parked. Opt-in: the default trims to the request
alone as in 0.1.40, so the K/V gives back its VRAM as before. Output is
unchanged; only --kv-grow is affected.
Ported from the community PR with the author kept. Raw dumps and logs were trimmed (TRIMMED.md in the folder lists them); the README, summaries and configs are unchanged. Indexed in bench/results/COMMUNITY.md.
Ported from the community PR with the author kept. Raw dumps and logs were trimmed (TRIMMED.md in the folder lists them); the README, summaries and configs are unchanged. Indexed in bench/results/COMMUNITY.md.
Ported from the community PR with the author kept. Raw dumps and logs were trimmed (TRIMMED.md in the folder lists them); the README, summaries and configs are unchanged. Indexed in bench/results/COMMUNITY.md.
Ported from the community PR with the author kept. Raw dumps and logs were trimmed (TRIMMED.md in the folder lists them); the README, summaries and configs are unchanged. Indexed in bench/results/COMMUNITY.md.
Ported from the community PR with the author kept. Raw dumps and logs were trimmed (TRIMMED.md in the folder lists them); the README, summaries and configs are unchanged. Indexed in bench/results/COMMUNITY.md.
Ported from the community PR with the author kept. Raw dumps and logs were trimmed (TRIMMED.md in the folder lists them); the README, summaries and configs are unchanged. Indexed in bench/results/COMMUNITY.md.
Ported from the community PR with the author kept. Raw dumps and logs were trimmed (TRIMMED.md in the folder lists them); the README, summaries and configs are unchanged. Indexed in bench/results/COMMUNITY.md.
Ported from the community PR with the author kept. Raw dumps and logs were trimmed (TRIMMED.md in the folder lists them); the README, summaries and configs are unchanged. Indexed in bench/results/COMMUNITY.md.
Ported from the community PR with the author kept. Raw dumps and logs were trimmed (TRIMMED.md in the folder lists them); the README, summaries and configs are unchanged. Indexed in bench/results/COMMUNITY.md.
Ported from the community PR with the author kept. Raw dumps and logs were trimmed (TRIMMED.md in the folder lists them); the README, summaries and configs are unchanged. Indexed in bench/results/COMMUNITY.md.
Ported from the community PR with the author kept. Raw dumps and logs were trimmed (TRIMMED.md in the folder lists them); the README, summaries and configs are unchanged. Indexed in bench/results/COMMUNITY.md.
Ported from the community PR with the author kept. Raw dumps and logs were trimmed (TRIMMED.md in the folder lists them); the README, summaries and configs are unchanged. Indexed in bench/results/COMMUNITY.md.
Ported from the community PR with the author kept. Raw dumps and logs were trimmed (TRIMMED.md in the folder lists them); the README, summaries and configs are unchanged. Indexed in bench/results/COMMUNITY.md.
Ported from the community PR with the author kept. Raw dumps and logs were trimmed (TRIMMED.md in the folder lists them); the README, summaries and configs are unchanged. Indexed in bench/results/COMMUNITY.md.
With --layer-split auto and STRATA_STAGE_TRIM=1 a later stage's weights load after the search, and the search priced them
as the full arena with no projections, so a small card was over-credited: P40 24 GB + RTX 3070 8 GB picked K=2 (prefill
107 / 112 tok/s at 4K / 32K) where K=16 gives 343 / 654 in the report. The carve is now priced for each candidate range:
the trimmed arena (WeightTable::pool_bytes) plus NativeDense::weight_bytes_for, a walk of load over the GGUF headers that
rounds each matrix to cudaMalloc's 2 MiB granule (+0.8% on the iq3_s pack, high on purpose). After each late stage loads, the
bytes priced are compared with the VRAM it really took and a warning says when they drift.
Only the late-weights arm changes (auto with the trim asked, opt-in); every other run has its weights loaded before the
search, carve_bytes is 0 there, and placements do not move. Ported from 572b607 of #880 onto this tree's loader (layer ranges
as arguments, STRATA_Q8_PACKED, STRATA_HC_Q8 untouched); not run on a mixed pair here.
Co-authored-by: gopinath87607 <[email protected]>
The report shows the bundled amdhip64_7.dll next to strata.exe and the System32 copy (a different build) in use, with the
prompt GEMMs failing. The import is bound before main, so the engine cannot swap it; it now prints a WARNING naming the
bundled file when the loaded one is another, so the log carries the fact. Nothing else changes. The fix itself (loading the
bundled runtime by full path, or a delay-loaded import) needs a Windows AMD machine to test and is not in this commit.
On Windows an engine with 313 MiB of VRAM free after a 15,104-token borrow exited silently inside the verify-window
capture. The cause is not reproduced here (no Windows AMD card), so this only leaves a trace and a hint, and changes
nothing: the capture prints how much VRAM is free before it starts, and a Windows start with less than 512 MiB free
and --prefill auto names --prefill auto:8192 (the reporter's workaround) and --vram-reserve-mib. The chunk choice is
untouched.
The build stopped with a FATAL_ERROR because gfx1150 was in none of the architecture lists, although the device code
(the WMMA guards, sudot4) already covers it. It is now in the unvalidated list: the build warns and goes on. The
report's machine (96 GB, TheRock 7.14.1, IQ3_XXS: decode 17-19 tok/s, prompt about 125-135 tok/s) is in docs/AMD_HIP.md.
Setup's own AMD path is unchanged.
Co-authored-by: zeriyoshi <[email protected]>
Phones save portrait photos as landscape JPEGs with an Orientation tag, and the encoder's decoder ignores it, so the
model saw them sideways. Such a JPEG now takes the Pillow route (exif_transpose, then PNG); JPEGs without the tag, PNG,
BMP and GIF still pass through byte for byte, and without Pillow nothing changes. Other formats that carry the tag
are turned too.
Co-authored-by: crazyaimachine <[email protected]>
On a layer split with --batch-groups, two concurrent requests ran at about half speed: the slot choice could put both in
one group, and a group's window carried a row for every slot, active or not. Slot choice now prefers the group with the
fewest busy slots (a held prefix of 512 tokens or more still wins), and a window covers the slots up to its last active
one. Without --batch-groups the slot choice is exactly as before.
Co-authored-by: crazyaimachine <[email protected]>
The search priced placements by decode time only, so at 512K context it could pick a split where a stage could neither
lend a 512-token prompt chunk nor afford its own buffers, and the engine ran out of VRAM at start while an explicit
--layer-split worked. A placement is now taken only if every stage can lend the last-resort chunk or afford its own
buffers (the serve path's own tests, priced from the same figures); the tie-break for balanced stages stays. A
start-time probe on the live free figures says which card is short.
Changed from the PR: nothing is refused. If no placement can start on paper, the search runs again without the gate
(the choice is then what it was before) and prints what is short and how to make room; the probe only warns.
Co-authored-by: fukc-gihtub <[email protected]>
Every layout of active slots keeps its own graph pair, and with the expert cache sized to the reserve the next window
graph can find no VRAM left; the engine then exited (#997, `parallel` 4 to 8). cudaGraphInstantiate that runs out of
memory now evicts the least recently used batch layouts, other than the one being captured, until it fits; they are
captured again when needed. The start prints how many were freed. Nothing changes while everything fits.
With the key on, a call of a declared tool is the call when the model writes the tool's name as a parameter tag,
a JSON object inside <tool_call>, or a bare <function=NAME> at the start of a line; two calls in one <tool_call>
are two calls. Anything else stays text. Without the key every reply is exactly as before. On the author's corpus of
1,462 agent turns: 92.9% read as intended before, 98.3% with the key. Re-files #966.
Codex's review request sends tools, an additional_tools item and text.format json_schema at once, and got a 400.
Now a tool call passes through as it does without the format (words written before the call go out as a message
ahead of it), and the schema is checked on the turn that ends in text, as json_schema works without tools. With
tools the system directive says the schema is for the final answer only. The chat route's rule is unchanged.
Tests in serve/test_responses.py reproduce the request shape: a call, a valid text answer, and (with jsonschema
installed) an answer that breaks the schema.
The wait for the engine's next line was 10 s, so a disconnect during a quiet prefill reached the engine only at the
next heartbeat. Both waits now poll the cancel event every 0.5 s; the HTTP heartbeat stays at 10 s, and an expired
silence deadline is enforced before another heartbeat. serve/test_quiet_cancel.py drives a fake engine that says
nothing until STOP. This is the Python half of #962; the engine half (stop between prompt chunks) is open.
--force now removes the old pack's markers (index.txt, manifest.json) before the build writes a byte. A forced build
that stops part-way leaves experts.bin without a manifest, which the next build or setup makes again, instead of the
old markers over partly new experts.
A string or list as a tool's schema passed the tools check and raised later, in parse_tool_call and the stream parser,
after the 200 and part of the reply. It is now a 400 naming the tool and the field. A missing or null schema still
passes.
A window size's cost was updated only when that size was chosen, and the re-probe covered only sizes seen fewer than
three times. A size priced high during one slow stretch (a prompt's cache refill) could stay priced out for the life
of the process. A size not measured for 300 rounds is now tried again when a confident lookup could use it, one
round per stretch. Output is unchanged (the policy only picks which drafts to verify); draft_policy_test gains a case
that fails without the change.
HIP keeps the shared expert on the draft's own stream; only CUDA branches. The stream and events were created and
never used, and the report's R9700 lost prompt speed after the first request.
STRATA_PF_FUSED=1 on gfx1100 gave prompts that answered only "!". Two causes, from the report:
- the stage loop wrote the next weight buffer with no barrier after it; one __syncthreads() now follows the write.
- the waves_per_eu(8) variant of native_w11_kernel computes wrong results with clang 22 (ROCm 7.10): there the cap is 1
(NW_LB; -DSTRATA_W_LB=N overrides it). Earlier compilers keep 8.
Default output is unchanged (the path is opt-in on gfx11).
An Arc Pro B70 (xe) page-faults when a kernel reads ordinary pageable host memory (devcoredump: ccs0, FaultType 0,
faulted address in the CPU's mmap range, then Timedout job and device lost). Without --stream-experts the experts
outside the VRAM cache sit in a pageable arena that the PCIe share hands to the GPU. The default share is 0 there
(the CPU computes those misses) with a one-line warning naming --stream-experts, as setup configures it. An explicit
--pcie-frac is kept and warned about. i915 (Arc Alchemist), CUDA and HIP are unchanged.
strata::intel_gpu_driver() reads the driver from sysfs; verify.cpp's i915 test uses it.
The mirror table (host-mirror pointers for the device plan) is filled only from pinned USM aliases under
--stream-experts, so it cannot hand out pageable pointers.
STRATA_QFUSE=1 (the gfx1151 arch default) has gdn_step_norm_multi write the
out-projection's q8_1 image itself and skips native_quantize_q8_1. The batched
recurrence (batch_rec_) calls gdn_step_norm_multi once per slot with no q8_1
destination, so with --batch the projection read stale bytes and every
concurrent answer was corrupt. The batched case now quantizes as without
QFUSE; the fused image holds the same bytes (gr_parity), so batched output
equals solo. Single-request paths are unchanged.
A stage that was 100% resident at init takes the doorbell graph while
a prompt loan/shrink/swap is in flight (ar_off_), and that graph waits
on host doorbells the skipped service never raises: one GPU spins at
100% while the host sits in cudaStreamSynchronize, until the watchdog
fires with the previous stage's stale label. Key the all-resident
service skip in run_slot_rows, batch_poll and service off ar_on(),
as run() already does.
PR #1102 (win10ogod) is the same fix for run_slot_rows and batch_poll;
this one also covers service().
Co-authored-by: win10ogod <[email protected]>
#783's batched append of a verify window's cells passed no host copy for the
hybrid K8V4 format (int8 and q4_0 pass st.host, and the per-token path passes
K8V4's two halves). With --kv-resident, a block that was not resident when its
cells were appended came back from the host copy without them, so the tokens a
window wrote were lost: free generation degenerated into repetition (#1188,
#1135) and parked conversations lost their decode-written turns (#1169). The
batched path now passes the same host halves as the per-token path.
wait_step and the chain wait polled ext_oneapi_empty() through DPCT_CHECK_ERROR, which is always 0, so the
loop left after 1024 spins and read draft tokens the GPU had not written (-1). Same migration bug as the
cudaStreamQuery fix in verify.cpp, missed in mtp.cpp. IQ3_XXS on the A750: 61 of 99 drafts accepted on the
story prompt (the 5070 gets 106 of 166); copy_'s readiness test in verify.cpp had the same flaw.
STRATA_DBG_DRAFT=1 prints each window's drafts and the target's tokens.
On an Arc A750 (i915) a verify window launched behind other GPU work in flight (the previous window's commit,
a prefill) often never started: no GPU breadcrumb, the 20 s ring timeout, the engine ends. queues_wait_and_throw
before the launch removes it (9 of 9 three-request runs pass, almost all failed before). Default on for i915,
off elsewhere (STRATA_WINDOW_SYNC=1/0/2 overrides). STRATA_RING_TIMEOUT_S raises the 20 s doorbell timeout.
Level Zero's free-memory query returns the constant total on an Arc A750 (i915); the expert cache and draft head
were sized from the whole card and the first launch failed with out-of-host-memory. device_free_bytes now lowers
the figure by what this process holds (drm-total-local0 / drm-total-vram0 in /proc/self/fdinfo) and says so once.
On the Arc A750 a kernel never saw the host's doorbell and result-row stores to ordinary host USM
(sycl/probe/doorbell.cpp: only the uncached allocation completes the handshake), so every ring wait
hit its bound and the window read stale rows: NaN logits. verify's mapped staging and the pool's
y_miss rows now use host_malloc_polled (sycl_queue.hpp; needs libze-dev's header, else sycl::malloc_host;
STRATA_HOST_UNCACHED=0 turns it off). IQ3_XXS on the A750 now answers 12*12 = 144.
Also: the expert cache and the MTP draft head check Level Zero's null return for a too-large
allocation (and an Arc A750 needs UR_L0_ENABLE_RELAXED_ALLOCATION_LIMITS=1 for a single allocation over
4 GiB); gather_rows no longer asks for a cooperative launch it does not use; the expert-cache
zeroing in generate.cpp is chunked too. Based on the probe in PR #423 (maxfridbe).
One memset of the 25 GiB expert-cache arena runs on the blitter engine and times out on the
Arc Pro B70 (xe: engine memory CAT error, GT reset). big_fill_zero zeroes in 256 MiB kernel
launches; used for the expert cache, the MTP and verify arenas and the KV reset.
The sycl/ copies were at 0.1.39 and did not build against the 0.1.40.x shared
headers (load_experts_gguf ready argument, fused_gr_read_multi bool, Gemm::bf16/native ldx,
13 symbols missing at link). Re-migrated the 5ffcce43 and 0.1.40.1 trees with migrate.sh,
merged with merge_upstream.py, hand-resolved the conflicts (the port's tuned expert and
MMVQ kernels kept; verify, mtp, generate adopt 0.1.40.1 with the port's queue handling).
Stubs: vmm (no driver VMM on Level Zero), --remote-expert-opt. Dropped upstream's
opt-in AMD S26/S27 grouped-expert kernels and STRATA_QFUSE/HC_Q8 reads on SYCL.
build.sh and migrate.sh source setvars.sh outside set -u (they exited silently).
The first version staged every uncached blob and made the layer wait for all of them before any expert computed, which
lost the overlap the kernels' own page faults had (-19% on a cold 16 GB lane). Now prefetch() hands the layer's uncached
blobs to the I/O threads and returns; the kernels that reach a blob before it lands wait on its pages, the others
compute. STRATA_IO_PF_STAGE=1 keeps the staging variant. Residency is probed on four sampled pages, not the whole blob.
The mapped file tier reads experts through page faults. With STRATA_IO_PREFETCH=1 (Linux, experts.bin or the GGUF
in place) a blob whose pages are not cached is read with one pread of the whole blob into a staging buffer, the
predicted experts of the next layer(s) are read ahead on I/O threads (STRATA_IO_PF_THREADS, STRATA_IO_PREFETCH_DEPTH,
STRATA_IO_PF_AHEAD=0 for demand reads only), and a cached blob is still handed out as the mapping. Same bytes, so the
same output; off by default.
Counters: each request now logs the OS's own view of the file tier (/proc/self/io read_bytes and major faults) beside
the engine's expert reads, and with STRATA_IO_STATS=1 or io prefetch a mincore split of cached and uncached bytes,
read-ahead used / unused / skipped, and the decode's wait in the fetches (--stats prints the same).
blob_stable(): a pointer that is not recycled, for the copies that run asynchronously (adaptive swaps, the GPU cache
fills, the prompt stager).
Asked with Accept: text/plain or ?format=prometheus; every other request keeps the JSON.
Latency histograms, a Grafana dashboard and scrape configs under docs/monitoring.
Only the Prometheus part of #793; the pipelined-window slot allocator needs a 2-GPU check and is left out.
Reads the routing ids first and queues the shared expert afterwards; staging
counts real transfers. STRATA_PREFILL_STREAM_AHEAD=0 restores the old schedule.
--adapt-async 1 no longer drops to the blocking tier with --pipeline-windows 2: the steps that overwrite
something a window in flight may read wait for those windows. STRATA_PIPELINE_ADAPT_ASYNC=0 keeps the
blocking tier there. Opt-in; default output unchanged.
When every placement holds the whole expert profile the predicted decode window is the same for all of them and the winner was floating-point noise: --trim-stage-weights on four GPUs chose layers 6/30/5/7 and read a 32K prompt at 1,750 tok/s, against 2,790 for 12/12/12/12 with the same decode speed and tokens (4x Radeon AI PRO R9700, IQ3_S). Among placements whose window time agrees to 1e-6 ms the smallest slowest stage is kept.
The engine's GEN line takes pin=N: the first N prompt tokens are a shared read-only prefix (a long document many
short questions follow). The prompt is read in two parts at N and the checkpoint there is pinned: retention never
evicts it, a parked conversation holding it stays parked, the session file keeps it, and a later request that resumes
from it does not park the branch it leaves, so N queries keep one prefix. One pin at a time. Without pin= nothing
changes.
The server takes "strata_prefix" in a chat, messages or responses request: {"messages": K}, {"message": I,
"chars": N} or {"tokens": N}, resolved to the longest common start of the prompt's ids. A prefix that cannot be
marked is said in the log and ignored; a malformed field is a 400.
Tests: conv_cache_test (pinned eviction), conversation_cache_test (a pinned parked conversation is not evicted),
conversation_file_test (the pinned checkpoint is the one saved), serve.test_server.SharedPrefix.
Speculative rejection sampling for the MTP drafts of a sampled request. The draft layer samples
its draft from the chain's distribution q (its own Philox stream, counter | 1<<61) and writes
q's (id, probability) list; the verify head computes the target's post-chain p per row from the
split sampler's block lists (penalties, top_k, top_p, min_p, temperature; at most 64 entries)
and accepts draft d with min(1, p(d)/q(d)), else samples the residual max(0, p - q) with its
own uniform. Row t writes the draft on an accept and the residual sample (never d) on a reject,
so the host's accept loop and the commit are unchanged. Rows past the MTP drafts (a lookup
chain's tail) and suffix windows are point masses: accept with p(d), residual p without d - no
q never means q = 0 (the llama.cpp #29924 class). Greedy requests never reach the new kernels;
the nine existing sampler kernels keep identical SASS.
- spec_prob.hpp: the rule, host reference; sample_tokens_spec / spec_draft_sample kernels
- STRATA_SPEC_PROB_GATE=pick, STRATA_SPEC_PROB_DT=x, STRATA_SPEC_MIN_TEMP=x: tunables, off
- spec_prob_test (CPU, 2M trials per case, chi-square 99.9%, controls that must fail) and
spec_verify_parity (GPU verifier vs host reference, token for token)
The per-request totals (drafts accepted / offered) hide where a window ends. With
STRATA_SPEC_DEPTH=1 the serve loop counts, for the windows drafted by the MTP layer, how many
times draft 0, 1, 2 ... was tested (every earlier draft was kept) and how many of those were
kept, and prints one stderr line per request. Off by default; nothing else changes.
Built on our RTX 5070 with nvcc 13.2.51 and 13.0.88 from the same source: with 13.2 the IQ3_S MMVQ product
(iq_multi_parity) and the IQ2_S / IQ3_S MMQ prompt path (prefill_fused_iq_test) are wrong, relative error 0.5 to 1.0; the
other formats are bitwise equal. setup now takes an installed 13.0 / 13.1 over 13.2 for sm_120 (STRATA_NVCC still wins) and
otherwise warns, saying what to install. Recommends, never refuses.
With the fused layout (STRATA_PF_FUSED=1, a chunk of 1024 tokens or more) layer 0 is treated as one the fused path does not
take, so a prompt must stop with the backstop's error instead of overflowing the shrunk buffers. The check that
the guard works at a 16,384-token chunk without needing a pack that really has such a layer.
Streams an answer, stops reading, resends or edits the prompt after 0-0.4 s (or with another request already running)
and checks every answer for real text, not a repeated token. Run the engine with STRATA_DBG_NAN=1 beside it.
--prefill auto:32768 lends 6,000 cache slots to the prompt path; on 31 GB of RAM only 3,700 of their experts stay in
RAM and the rest are read from the SSD on every chunk (prompts ~3x slower than --prefill auto). The engine now says
so at start, with the counts. A warning, never a cap: with the RAM for them the big chunk is 21-35% faster.
The LOW free-VRAM line at READY now also names the explicit --expert-cache (its slot count) and suggests a smaller
one or 'auto', which re-checks the free VRAM after the slots are written. Said, never changed.
gdn_step_commit_kernel reads n_keep from device memory. With STRATA_DBG_GDN=1 a count past the window's rows is
printed and the commit skipped, where a gfx1030 reporter sees a memory-aperture violation. A template parameter
keeps the default kernel unchanged.
fused_layout() and fused_ring() already keep the shrunk GU/H/Xq buffers and the per-layer expert path in step. If
they ever drift (a layer that takes MMQ or the FP16 path at a chunk of stream_all_min() tokens or more while the
buffers are the fused ones), the prompt now stops with 'the fused layout's MoE buffers are too small for layer N'
instead of writing past the buffer: an illegal access, or a kernel that never returns and shows up as
'prefill mmq: iota' one prompt later. Same check in the SYCL copy.
Co-authored-by: Krypto-Whitehat <[email protected]>
A deleted ROCm build named by HIP_PATH / HIP_DEVICE_LIB_PATH / LLVM_PATH, and a TEMP the comgr JIT cannot
create files in, both crash amdhip64 before the engine prints a line. Only a missing path and an unwritable
folder are touched; a healthy environment passes through unchanged.
uv's own managed Python loads pip wheels' libraries where Nix's Python cannot. Changed from the PR as the plan asked: uv is used only on NixOS or when STRATA_UV=1 is set (no silent toolchain change on other systems), and the setup.py hunk is left out. Not run on NixOS here. (PR #662)
"reasoning_close_retry": true in the config: when a reply ends on its end-of-turn token still inside <think>, with no answer and no tool call, the thinking is closed once and generation continues (the engine holds the prefix). Off by default; tested both ways and for a reply that already answered.
PleTable's Mmap+lock path now calls strata::platform::lock_resident (working-set minimum + VirtualLock, chunked per GiB) instead of refusing to start. A lock failure still falls back to touching the pages with a warning, same as the POSIX path.
Changed as the plan asked: only a full VirtualLock counts (a partial one is unlocked again and said so), a warning when the table is more than half of the RAM, the usage text indent fixed. Compiled on Windows here (MSVC, CUDA 13.0); not run with --ple-io ram yet.
(PR #813)
A reinstall put the model on the old drive again because setup keeps the last data folder in its settings file. The troubleshooting table now says where, and that --data-dir, --models-dir, --gguf-dir or deleting the data_dir entry change it.
An explicit budget bigger than the RAM the PC has free (or than its total less ~6 GiB) pushes experts to swap or the file tier; one report went from dead slow to fast by deleting the number. The server now says so at start and goes on (recommend, never force).
find_vcvars looked for versions [16.0,18.0) only, because CUDA 13.0-13.2 refuse a newer Visual Studio. The range now goes up to 19.0 when the toolkit found is 13.3 or newer (the reporter built with VS 18 and CUDA 13.3); older toolkits stay on 2019 and 2022.
Codex's compaction request has tools: [], so its prompt shared almost nothing with the cached conversation and was read again from the start. With "codex_compaction_cache": true the tools of that conversation's last prompt are kept (one entry, keyed by session and thread) and used for the compaction prompt only; the parser and the response keep the request's own tools, and a request that would not fit with them is rendered as sent. Changed from the PR as the plan asked: off by default, the mismatch documented, ported on top of the opt-in #923 and tested over HTTP with a tool call emitted in the compaction. (PR #924)
Codex sends a thread-title request beside each user turn (another session, no tools); running it on the engine replaces the one prefix cache. With "codex_thread_titles": true in the config it is answered at once with the user's line cut to 36 characters (usage 0). Changed from the PR as the plan asked: off by default (the title is not model-written), documented, tested both ways. (PR #923)
Rebased onto the 0.1.40.1 Vision (work_dir, #480 absolute paths). Added tests for the lazy encoder (nothing starts, restart starts it, a failed start is killed); the --lazy help no longer says text-only. A live smoke with a vision config and --lazy is still to run.
(PR #673)
Cut down to reporting only, as the plan asked: max_context is 0 until READY, and /v1/status, /v1/models, /props, /health, /slots and /metrics told a client the model had no context for the whole load. They now use the last known context (reported_ctx). Requests are still checked in prepare() as before.
Co-authored-by: updalla-apshir <[email protected]>
A reasoning-budget continuation (and the reasoning-loop recovery pass) is another native generation, not another API request: each pass keeps its own DONE, the prompt reuse is the first pass's (the generated reasoning is not this request's cached input), and times, tokens and draft counts (offloaded included) are summed. Rebased onto the 0.1.40.1 Service.run. (PR #615)
Cut down from the PR as the plan asked (recommend, never force): an empty --api-key or STRATA_API_KEY is already refused (#213) and a blank config key is refused with #956; an api_key that is present but empty now prints that the server has no key and starts. The setup-side refusal is left out (the final line already says no API key is set).
Co-authored-by: Guillaume PUTIER <[email protected]>
The 0.1.32 image encoder was configured without -DSTRATA_PORTABLE, so the old OFF default built it native
(MSVC + FindSIMD: /arch:AVX512 on the build machine) and it stopped with 0xC000001D on AVX2-only CPUs. A build
without flags is now the AVX2 baseline; the builds for the PC they run on (setup.py's two encoder builds, the
Dockerfile) pass -DSTRATA_PORTABLE=OFF and stay native.
- portable also forces GGML_AVX512*, GGML_AVX_VNNI and GGML_AMX_* OFF, so a -D cannot put AVX-512 back in
(with the old file, -DSTRATA_PORTABLE=ON -DGGML_AVX512=ON gave 10522 zmm instructions; now 0)
- native restores a fresh native configure's GGML_OPENMP / GGML_AVX* when a folder was portable before
- the configure log says which of the two it is
- test: build_engine's encoder build (gpu and cpu) passes -DSTRATA_PORTABLE=OFF
Rebased by hand onto the 0.1.40 setup.py (the Windows MSVC environment of #881 kept). The build script of the ready-made engine zips is not in this repository: it must pass -DSTRATA_PORTABLE=ON for the encoder (the default was OFF until now).
(PR #427)
Short forms with the required fields, a link to the troubleshooting table, and the pull request template. Taken without CODEOWNERS and without the template's checklist (the changes the plan asked for); blank issues stay allowed.
Prompt and output rates for the AMD RX 7900 XTX (gfx1100) beside the NVIDIA rows, with the measurement note. Only the DETAILS tables are taken: the PR's branch also deleted about 190 lines of current text, and the README tables are two fixed machines (RTX 5070 and RX 9070 XT).
tools/quantscope.py (from 1872183316/quantscope, MIT OR Apache-2.0) reads only the headers (a file, a folder, a URL, ms: or hf: repositories); tools/strata_inspect.py compares them with data/gguf_fingerprints.json, the files setup installs. Changed from the PR: the size menu is untouched (its bits per weight line would change the golden output; left out), and the untested verdict says that setup still refuses the GGUF names it lists as unsupported, so the two do not contradict each other. (PR #911)
Setup never switches hosts by itself: auto is Hugging Face, and a download that fails says --source modelscope exists (no probe at start). With it, each file is checked against the SHA-256 ModelScope publishes (a wrong one is deleted; a file with no published hash is warned about), the MTP tensors against the pinned checkpoint's own hashes, and setup says that ModelScope serves current files, not our pinned revision. Changed from the PR: the automatic switch and its 10 s probe are gone.
Co-authored-by: 1872183316 <[email protected]>
A new step 5 tries two tier settings (every window, 80 or 160 swaps, decay 0.97) against the engine's own, after the worker count, one restart each; a setting is kept only when it beats the default by 3%. The tier follows a text over some windows, so this step measures 512-token answers. The author measured +7.9% on a DDR3 Xeon with an RTX 4060 Ti. Applied by a 3-way merge onto the 0.1.40 calibrate.py; the check that it keeps the engine default on a fast-RAM PC is still to run on a GPU.
The generation card and board both run, the lanes now, and the copy rate that implies for a slow link. Changed from the PR: the fewer-lanes note is worded as a hint (a card or laptop can narrow the link while idle), not a verdict.
Co-authored-by: 1872183316 <[email protected]>
The model is the same base model and the same image encoder as UD-IQ4_XS; a user ran it by hand (#967, #971). Setup asks about images for it and warns that we have not run this file with images (no smoke test here: the 111 GB file is not on this PC). MCP's plan and size table follow.
Codex v0.160 sends an input item of type additional_tools, which was refused with a 400. The item is no message; a tools array in it joins the request's own tools, and one without a usable array adds nothing. The item's exact shape is not documented here: only the tools-array form is read.
Both tests upload and memset on the legacy default stream, then launch on
a cudaStreamNonBlocking stream, which does not wait for it. cudaMemset is
asynchronous to the host and a pageable cudaMemcpy may return before the
DMA lands, so the kernels can read buffers still being written. A device
sync before each launch closes it, the same fix 0.1.36 made in the fused
tests. Not reproduced on an RTX 5090: 0 failures in 210 runs of gr_parity
before and after.
Only the first commit of the PR (the stream syncs); the second one (a rewrite of ple_parity without the fixtures) is not taken. The same syncs for the HIP tests were not added (no AMD test machine). (PR #568)
With several GPUs in the config's "gpu" the server appended --layer-split auto, which the engine refuses beside
--peer-device (#531), so the peer tier could not be started through the server at all:
strata generate: --peer-device cannot be combined with --layer-split (use one or the other)
The split (and --split-skip-if-fits) is now added only when --peer-device is not among the args; the start line
names the peer tier instead of a layer split. Tests in serve/test_server.py (PeerDevice), docs/SECOND_GPU.md says
how to configure it.
Ported by hand onto the 0.1.40 engine_args (layer_split_value); the start banner now says the peer tier is in use when the split is not.
(PR #665)
--expert-cache N is N times the largest blob, compared only with the free VRAM
read before the slots are written; the check that runs once they are written is
auto-only. Under WDDM the free figure can be about a gigabyte high and an
over-committed allocation goes to system memory instead of failing, so only the
speed shows it. Measured at 1,048,576 on a 32 GB 4080 SUPER: 11,631 slots with
0 MiB free ran 13.7 tok/s, 11,178 slots (auto) with 217 MiB free ran 102-122,
and raising --vram-reserve-mib moves the free figure because it is deducted
before the cache is sized.
Also records that the GPU process Shared/Dedicated counters did not separate the
slow run from the fast ones on that machine, and adds a Troubleshooting row.
Read against generate.cpp: the second size check after the slots are written is auto-only (the loop breaks on !auto_cache), and the LOW line is printed at 256 MiB.
(PR #799)
The entry (serve/server.py with llama-swap's port, checkEndpoint /health, healthCheckTimeout), the DNS-rebinding
'Host ... is not allowed' trap and its three fixes, /upstream/<model>/ for Strata's own pages, and a note on the
Read against the 0.1.40 server: allowed_hosts and STRATA_ALLOWED_HOSTS, /health, the effort handling and the 403 text match.
(PR #829)
ggml's tiled CPU flash-attention kernel needs the head size (72 in this
encoder) to be a multiple of the vector width: 8 floats with AVX2, 16 with
AVX-512. With AVX-512, which a build from source gets on a CPU that has it,
ggml falls back to a kernel that is several times slower and accumulates in
FP16. The release builds are AVX2, where flash attention is the faster one,
so auto stays the default there.
1024x1024 picture, Ryzen 7 7700X, 8 threads, F16 mmproj: AVX2 8.3-9.9 s
with it, 14.0-15.2 s without; AVX-512 43.5 s with it (26% off the FP32
output), 13.3 s without. --gpu keeps auto, and an explicit --flash-attn
still wins.
Rebased onto the 0.1.40 strata_vision.cpp (the --min-tokens usage line). Only a source build on an AVX-512 CPU changes; the release builds (AVX2) do not. The Zen4 timing is not run here.
(PR #660)
- GGUF_DIR passes setup's --gguf-dir, so a container can use model files the
host already has instead of downloading them again (#252/#444 cover the
native setup only)
- RESIDENT_BUDGET_GIB and KV_STREAMING pass --resident-budget-gib and
--kv-streaming; unset keeps setup's own defaults
- INSTALL.md: a mounted GGUF folder must be writable on the first start
(the Unsloth SHA-256 result is kept as <shard>.done next to each shard)
- INSTALL.md: releasing tens of GB of page-locked RAM on SIGTERM takes longer
than Docker's 10 s default; without --stop-timeout the container is killed
(exit 137)
The INSTALL.md paragraph was added by hand (the PR's numbered list no longer exists); KV_STREAMING maps to setup's current --kv-streaming auto|on|off.
(PR #491)
The bare message cost a full debugging session: on Linux the failure was a
missing libcuda.so.1 (a host driver library the nix loader cannot see) in a
launch environment, not memory pressure. cudaGetErrorString names the real
cause, e.g. "CUDA driver version is insufficient for CUDA runtime version".
Ported by hand onto the 0.1.40 native_head.cpp (the PR's hunk context had moved).
(PR #476)
The PR guarded the mark of a whole shard; mark() itself now warns and goes on, so the SHA-256 result of a verified Unsloth shard and the shared-shard link on a --gguf-dir share do not stop setup either. The step is repeated on the next run.
A whole shard without its .done mark gets one written next to it. On a
read-only --gguf-dir (a network share) that write raised OSError and setup
stopped. It now warns that the mark cannot be written and goes on.
(PR #570)
A splitter may copy general.architecture into EVERY shard of a split while the
qwen4exp.* hyperparameters live only in shard 1 (seen with the Huihui
abliterated re-split of the UD-Q4_K_XL GGUF: general.architecture in shards
2-4, no qwen4exp.* there). NativeDense::load then ran check_architecture on
shard 2 and failed with 'missing qwen4exp.block_count'.
Run the full guard only when the shard is the first of the split (no split.no,
or split.no == 0); later shards take the existing split-metadata consistency
check whether or not they carry the architecture key. Verified end to end:
with this change the engine loads the re-split GGUF (previously: refused),
pack built with iq_pack.py --compat-bf16, coherent greedy output.
(PR #648)
read_expert_profile read the header's version field and then ignored it
((void) version), so a profile of a later format would have been read as a
version 1 layout: the fields after the version are only known to be
n_layers, n_expert, slots, n_ranked in a version 1 file. Refuse any other
version right after the magic, before the geometry check, with the version
found, the version this engine reads, and the file path in the message.
tests/core/expert_profile_version_test.cpp (registered like
expert_profile_save_test, CPU only): a version 1 file loads; the same bytes
with the version field at 2 or 0 are refused naming the version and the
file; a version 2 file of another geometry is refused by its version, not
its geometry; back at 1 it loads again.
(PR #632)
The inputs and the NaN sentinel go up with cudaMemcpy / cudaMemset on the legacy stream, and the kernels run on a
cudaStreamNonBlocking stream, which does not wait for it: a pageable copy or the memset can land after the kernels
started. On an RTX 4090 (sm_89, 128 SMs) that failed the screen ("unwritten or non-finite output") in 3 of 3 runs,
in a different product each time; with the sync before the kernels, 5 of 5 runs passed. The same fix as the fused
tests' (prefill_fused_moe_test.cpp, prefill_fused_iq_test.cpp).
(PR #532)
A request stopped on the solo path and continued in a slot is sent again as its prompt plus what it generated, and the engine holds all of that: the usage split counted the continuation. The first read's reuse is kept, and Anthropic's input_tokens is at least 1 (Claude Code took 0 as not given and counted the prompt twice: Prompt is too long at about half the context). Rebased onto the 0.1.40.1 server and the stop_sequence field.
strata_install refused every vision value but no with backend=hip, although setup's own --vision cpu reads images on the CPU beside the HIP engine. It now follows setup.hip_vision: cpu on Linux is planned, a GPU encoder (yes, gpu) and cpu on Windows get a message that says what works. The Windows AMD engine in MCP's hardware detection is left as it was (it still reports AMD cards on Linux only).
The configured key loses the spaces and line ends a header cannot carry; a key outside ASCII matches as UTF-8 and as Latin-1 (constant-time). Server side only: the web app part of the PR (the UTF-8 header and the key-rejected note) is left out.
reasoning_budget_tokens joins the shared Chat settings (0 means no budget, a whole number of tokens otherwise) and is read after the shared and config defaults are merged, as max_tokens is. Rebased onto the 0.1.40.1 Service.run (force argument).
req.get("defaults") is None both for an absent key and for an explicit null, and null means clients use their own settings again. A body the endpoint does not recognise cleared them all. {"defaults": null} still clears.
A context meter that divides n_prompt_tokens by n_ctx read 0 % against Strata. The key is the running request's prompt size, kept after it ends. Added to the PR: with --batch /slots lists every slot (the view /metrics has) instead of one entry.
Co-authored-by: Zerschranzer <[email protected]>
With "parallel": N the engine runs N requests at once, and /v1/status already says so. /props still said total_slots 1, so llama.cpp clients that read it refused or serialized concurrent runs. It now reports the same count, at least 1.
The caps (supports_tools, supports_tool_calls, supports_parallel_tool_calls, supports_system_role, supports_preserve_reasoning) are rendered once at template load. Changed from the PR: every probe error is swallowed (logged at debug) and turns that feature off, so a custom template that fails in an odd way cannot crash the start.
Co-authored-by: Ivan Tuzhilkin <[email protected]>
A POST behind a relay that sends Transfer-Encoding: chunked had no Content-Length and read as empty (400 No messages provided). _body(), the control-body read and the unread-body drain now decode it (it wins over a Content-Length, as RFC 9112 says). A chunked body is capped at 256 MiB (413; the announced size is checked before a chunk is read) and a malformed one is a 400. Content-Length requests are unchanged. Left out of the PR: nothing; added: the cap, the other body readers, tests.
Co-authored-by: Oscar Rojas <[email protected]>
The vision cache unlinked the oldest .sve on every insert past 64, including a file the same request had not read yet (FileNotFoundError). Vision.encode_all() protects the request's earlier files; the cache shrinks back on the next insert. A cache hit now counts as a recent use.
The solo and admission paths read the engine's ERR line, then sent STOP and drained for a DONE that never comes (it holds the control lines meanwhile). The ERR now ends the exchange.
Rows for #927 (2x RX 6900 XT), #917 (Radeon 8060S, gfx1151) and #955 (Arc Pro B65) in the new COMMUNITY.md index, with notes on what each
build was; the bullets I had added to docs/COMMUNITY_BENCHMARKS.md are removed so the index lives in one place. TRIMMED.md lists the
seven telemetry files left out of the 6900 XT folder.
The gfx1031 entry now lists the RX 6700 XT run (#1027) and the RX 6800M run on Windows (#915, #1078), and says the Windows zip
needs gfx1031 in the STRATA_HIP_ARCHS build list, which the default list has from 0.1.40.2 on.
NVIDIA_V100.md said sm_60 was not measured. It now points to the two-P40 layer split (#1028), the P100 alone and with a helper card
(#1069) and the V100 + P100 pair on 0.1.40 (#1079), each one reporter's run. OLDER_GPUS.md notes the extra host flag a GCC 12.3 +
nvcc build needed in #1074 (one report).
Three fresh engine launches, five fixed tasks at 512 and 7,000 prompt tokens, 640 output tokens: decode medians 40.4-41.3
and 39.8-40.9 tok/s, reference hashes recorded. The six local patches the report built with are kept as data; patches 3, 5 and 6
are ported as source changes in this release, the others were already in 0.1.40. Index line added to COMMUNITY_BENCHMARKS.md.
Co-authored-by: Tim Nevits <[email protected]>
Interactive Q2_0 and UD-IQ4_XS chats against llama.cpp Vulkan, then controlled A-E runs of the gfx1151 hipBLASLt tuning table
(250.9 to 528.9 prompt tok/s), dense MMQ (+0.2%) and the shared-expert decode stream (+2.5%, 8 requests). The build is
0.1.39 plus then-unmerged PRs (#895, #820, a tuning table adapted from #755), not the 0.1.40 release; the index line in
COMMUNITY_BENCHMARKS.md says so and notes that 0.1.40 turned the shared-expert stream off on HIP (#826).
One card, a layer split and the expert-helper mode, Strata 0.1.39 stock and with then-unmerged #835, #849 and #854, original
Flash-Next IQ3_S at 131,072 context; three runs at 4K, 32K and 128K prompt tokens and six recall checks per configuration.
Index line added to COMMUNITY_BENCHMARKS.md with a note that #835 is opt-in in 0.1.40, so the second set is not the
release default. The one-second telemetry.jsonl files (5 MB) are left out, and the README says so.
Co-authored-by: xjc10 <[email protected]>
The ready-made Windows HIP zip had no gfx1031 code, so an RX 6700 XT or 6800M user had to build from source with
STRATA_HIP_ARCHS=gfx1031 (#915, PR #1078; #1027 ran a 6700 XT as gfx1030). setup already knows gfx1031 (AMD_ARCHS, the
wheels index, the PCI id) and the arch gate builds it. This only adds it to the default list the zip is built from.
Prepared, not decided: building the release zip with this list makes it larger (one more arch of rocBLAS kernels and
engine code), and the owner decides whether the 0.1.40.2 zip carries it.
Two pieces of PR #895 that 0.1.40 did not have; the rest of it is in 0.1.40 (gfx1151 in the arch gate, sudot4, setup
and the Strix Halo defaults) or is not taken (the ROCm pin bump for every AMD arch).
- dq_q2_0: the compiler on gfx1151 (ROCm 7.13) folds the half path's negative scale times +0 to +0, as HIP 5.7 did on
gfx1012. The sign-preserving path now covers gfx1151 too. Changes bits on gfx1151 only (the same value the FP32 and CPU
paths give); nothing else is touched.
- Linux HIP, --expert-cache auto: an APU's "VRAM" is system RAM, and the device-free figure counts the whole GPU-addressable
pool, so a cache sized from it can ask the OOM killer for nearly all RAM. The PR capped the cache at the host's free
memory less 4 GiB; here it only prints a warning with both numbers and leaves the cache as asked
(recommend, never force). --expert-cache N sets it by hand.
Compile check only on HIP; the run needs a gfx1151 box.
Co-authored-by: mini <[email protected]>
A plain hipMemset runs on the null stream, which a non-blocking stream does not wait for; on a Radeon 8060S (gfx1151,
Windows) the memset landed after the MMQ kernel and left only the sentinel. hipMemsetAsync on the same stream orders it.
Taken from PR #919's Windows gfx1151 port; its other pieces are not taken here (the Windows carve-out accounting needs
a Windows gfx1151 run).
Co-authored-by: Stormace <[email protected]>
Merges two community reports of the same mechanism (GPU queues suspended while the kernel reclaims KFD userptr pages)
into one short section in AMD_HIP.md and one line in TROUBLESHOOTING.md: HSA_USERPTR_FOR_PAGED_MEM=0 (two R9700, ROCm
7.2) and GPU_PINNED_MIN_XFER_SIZE=1048576 with --mmap-experts (RX 6800), each labelled as one machine and not
confirmed, with the MemoryMax versus MemoryHigh note. Condensed from the two PRs.
Co-authored-by: zengyishou <[email protected]>
Co-authored-by: YamineRL <[email protected]>
setup listed the RX 7600 XT as "not supported" although the arch gate and the Windows zip already build it, and one
RX 7600 XT ran the engine (Swift IQ3_XXS, 146 requests up to 51K context, about 43 tok/s decode, 250-300 tok/s prompt,
unvalidated; docs/AMD_HIP.md has the report. The two setup tests that expected "not supported" now expect it listed.
- conversation_snapshot_test: the pinned host buffers are freed with sycl::free (they come from USM, not malloc).
- STRATA_VERIFY_NO_HOST with experts that are neither in VRAM nor mirrored now refuses to start, instead of a warning
followed by a generation with no safe host fallback.
- On QUIT the direct-access USM host mirror is released (gguf_src.close()) before the process leaves; a failure to
release is reported and exits 1.
From the Arc Pro B65 bench report (timnevits), where they are shipped as patch files; the other patches there were
already in 0.1.40. SYCL compile check only.
Co-authored-by: Tim Nevits <[email protected]>
Only subgroup row zero of the 32x8 block reaches the router's synchronization in route(); the other seven rows return
before it. A work-group barrier there is divergent, and on an Arc A770 the kernel hung. The surviving row now meets a
subgroup barrier. Also recorded in sycl/tools/fixups.py so a regenerated tree keeps it.
Salvaged from PR #797 (the rest of that draft is not taken: its build fixes were already in 0.1.40). An A770 on i915
reported decode only with this change (23.0 tok/s with MTP, 8.65 without). Needs an Arc owner to test on the current
tree; SYCL compile check only.
Co-authored-by: Txgom2 <[email protected]>
tune_hipblaslt on the gfx1201-100500 table's 32 shapes, on a Ryzen AI Max+ 395 (Radeon 8060S, gfx1151) with ROCm
10.2.0a20260930 (whl-next, hipBLASLt 1.5.0 efafc1c1). 3.9-15x faster than plain hipBLAS per shape; prefill with
STRATA_HIPBLASLT_VERBOSE=1: launches=3138 fallbacks=0.
Only the table is taken from PR #944. Its gfx11 WMMA attention and scorer duplicate the kernels already in 0.1.40, and
its "gfx11" prefix gate would enable gfx1103 and gfx1152, which 0.1.40 leaves out on purpose. To be re-measured on the
gfx1151 box.
Co-authored-by: Nathan Bradley <[email protected]>
On an Arc Pro B60 a system-scope atomic load of host USM is still served
from the GPU's cache, so a device spin never sees the host's store and
runs to kSpinMax every time. Read the flag through an annotated_ptr with
an uncached L1+L3 read_hint after an acquire fence instead. Bounded
ping-pong: 263 ms -> 2.4 us per round trip; per-layer waits on a B60
with experts in the host mirror: 8.7-10.5 -> 19.3 tok/s decode.
-DSTRATA_DOORBELL_ATOMIC_LOAD keeps the old load.
Based on konijiwa110's PR #742. Below sm_80 there is no TF32 tensor-core scorer, so the prompt path scores the QSA
blocks on the warp kernel: 5 shuffles per 4 products, ~1 TFLOPS on an RTX 2080 Ti, 24% of a 128K prompt. With
STRATA_SELECT_SIMT=1, qsa_block_scores_tc runs an FP32 tiled GEMM there (rows = query x indexer head, K = 128 through
shared memory, 2 queries x 4 heads x 4 blocks per thread, relu per head summed in registers), ~6.9 TFLOPS by the
author's measurement (128K prompt 1,016 to 1,242 tok/s, 248K 715 to 952 on one 2080 Ti).
The tiled kernel sums in another order than the warp kernel, so its scores are not bitwise equal. The PR made it the
default below sm_80; here it is opt-in, so default output is unchanged until an interleaved A/B of at least 10 pairs
supports a default. The tail block keeps the warp kernel's arithmetic. STRATA_SELECT_OLD=1 still forces the warp kernel.
qsa_select_bench is now also a CUDA test: at 32K, and at 131K with STRATA_QSA_WARP=1 and STRATA_SELECT_SIMT=1 so any
card runs the FP32 tiled scorer (FP64 gate, selections compared with the warp kernel's).
Co-authored-by: konijiwa110 <[email protected]>
On HIP the default attention kernel's score loop is bound by the LDS pipe on gfx1030: its 12 x 5 __shfl_xor_sync per
cell compile to ds_bpermute_b32 and share that pipe with the shared-memory q reads. attn_chunk_kernel_pre75 (PR #540,
bit for bit the default kernel) is now built for HIP wave32 (not the gfx906 backend) and run on gfx103x by default:
- NC_ template parameter, cells per step: CUDA 2 as before, HIP 8 (on gfx1030 every step up in NC was faster although
occupancy fell from NC 1: the q reads bound it, not occupancy)
- DPP_ template parameter: the butterfly's lane exchanges (reduce12, the softmax max / sum) through DPP row_xmask within
a row and v_permlanex16 across rows instead of ds_bpermute; same values, same pairing order, unchanged bit for bit
- pre75_attn() on HIP: gfx103x per device; STRATA_ATTN_PRE75=0 off, =1 also another wave32 card
CUDA is unchanged: the non-experimental build does not build the kernel, and the template defaults equal the old
constants with shfl_x<false> = __shfl_xor_sync.
RX 6900 XT, IQ3_S, prompt tok/s at 9.4K / 34.7K / 105.8K: 441 / 467 / 460 -> 459 / 494 / 488; with #835's FP16 GEMMs
746 / 917 / 928 -> 801 / 1,022 / 1,041. Teacher-forced log-probabilities against the default kernel: KL 0, argmax 100%.
bench, docs: the pre75 attention kernel on RX 6900 XT - measurements, exactness, decode A/B; OLDER_GPUS.md notes
bench/results/2026-10-04-rdna2-pre75-attention: the rig, the compiler output (registers, occupancy, ds_bpermute counts)
that motivated NC 8 and DPP, prompt speed on this branch (+4.2 / +5.7 / +6.1% at 9.4K / 34.7K / 105.8K tokens), the
step-by-step NC / DPP series on the FP16 prompt path, the teacher-forced exactness check (KL 0 over 1,578 positions),
a decode A/B (0.3-0.6 ms shorter windows, two rounds), the limits, and the configuration used.
docs/OLDER_GPUS.md: the RX 6800 / 6900 row says this kernel is the default on gfx1030 and names the switch; the A/B
switches line says what STRATA_ATTN_PRE75 does there.
Follow-up to #600 (fks's mma.m8n8k4 prompt attention for sm_70).
The kernel splits q (for q.k) and p (for p.v) into FP16 hi + lo halves
and runs both products into the same accumulator. Volta's tensor cores
don't keep full FP32 precision while they accumulate, so once the hi
products are in the accumulator most of the much smaller lo
contribution gets truncated away, and the split buys less than it
should. Giving the lo products their own chain and adding the two in
FP32 at the end fixes that.
qsa_prompt_attn_parity, one V100-SXM2-16GB, 2,048 queries, error vs the
FP64 reference (the FP32 kernel in brackets):
ctx 8,192 5.88e-6 -> 3.02e-6 (2.14e-6) 11.94 -> 12.20 ms
ctx 32,768 5.08e-6 -> 2.33e-6 (2.37e-6) 12.20 -> 12.38 ms
ctx 131,072 5.35e-6 -> 2.75e-6 (1.81e-6) 12.34 -> 12.51 ms
Registers unchanged (166 / 226 / 163 for modes 0 / 1 / 3), no spills.
docs/NVIDIA_V100.md updated for the new error level.
gfx1010 (Navi 10) is measured on an RX 5700 XT: 8 GB card, Coder IQ1_M, 32K
context, prompt 115 / 144 / 149 tok/s and decode 18.0 / 20.3 / 23.3 tok/s at 4K /
16K / 30K prompt tokens, 6 of 6 needle checks at 8K and 30K, 61 of 61 ctest on the
card. Nothing beyond the arch list is needed: the portable scalar dp4a that main
falls to measured 162.7 / 24.3 against 158.3 / 24.0 with the __gfx1012__ SDWA
control (#442) - inside the run-to-run spread - and 16 of 16 HIP tests pass on it.
gfx1011 (Navi 12) is untested on hardware: same RDNA1 ISA, builds with 0 errors on
ROCm 7.14's gfx101X-dgpu wheels.
Two box facts, for whoever picks this up: hipBLASLt 1.4.0 refuses all 26 of the
engine's GEMM shapes on gfx1010 (heuristic_count = 0), so there is no tuning table
and prompts run on plain hipBLAS; and in a PC that also has an AMD iGPU, ROCm's HSA
runtime segfaults in GpuAgent::InitDma while enumerating the pair - below HIP, so
HIP_VISIBLE_DEVICES cannot reach it - and the engine must start with
ROCR_VISIBLE_DEVICES naming the one KFD node (rocminfo for the index).
Co-authored-by: AcckiyGerman <[email protected]>
CMake caches CMAKE_HIP_FLAGS_<CONFIG> while it enables HIP, from the
values its platform files set once the compiler is identified. When
HIP cannot compile CMake's identification source, that configure
caches them empty and then stops; every later configure of the same
build folder keeps the cached values, so the build that finally works
compiles every HIP source without -O, i.e. at -O0, while C and C++ get
-O3 -DNDEBUG. setup configures build-hip every time, so one failed
attempt is enough.
That happened on an RX 6800 (Windows 11, HIP SDK 7.2, CMake 4.0.1):
the first attempt failed on Visual Studio 2026's <cmath>, the next one
passed with the include fix, and the engine (0.1.30 with the Windows
build of #247, --expert-cache 2048) decoded at 0.24-0.27 tok/s with
7.1-7.5 s per decode window. STRATA_VERIFY_PROFILE put the time in
the GPU's own stages (per window: q8+qkv GEMVs 1468 ms and out-proj
849 ms in the DeltaNet layers, the LM head 540 ms); the waits for flags
the CPU raises were 41 ms of its 8.4 s per window. With the flags back:
59-62 ms per window, 28.1-28.3 tok/s.
After enable_language(HIP), hip_backend.cmake gives each empty
per-configuration HIP flag the value CMake computed for the compiler
(CMAKE_HIP_FLAGS_<CONFIG>_INIT), which is what a fresh build folder
gets. Flags with a value, from CMake or the command line, are left
alone; one set empty on purpose is filled too.
Reproduced on Windows (HIP SDK 7.2, CMake 4.0.1) with a first
configure that failed on --rocm-path flags split at the space in
C:/Program Files, and on Linux (CMake 3.28, TheRock's ROCm 7.10 clang)
with one that failed on a --rocm-device-lib-path that does not exist.
After a working second configure in the same folder,
CMAKE_HIP_FLAGS_RELEASE stays empty and native_mmvq.cu compiles
without -O; with this change it gets -O3 -DNDEBUG. A fresh folder gets
-O3 -DNDEBUG either way.
setup.py's engine fingerprint now covers cmake/ as well, so a change
there alone (this one) recompiles a locally built engine on its next
start; that includes NVIDIA engines compiled here.
The batch slots' drafters share the draft-vocab head but never copied its ggml type; it stayed -1 and the first
admission of --batch-mtp exited with "unsupported native MMVQ GGML type".
cudaGetDriverEntryPointByVersion arrived in CUDA 12.5; the CUDA 12.2-12.4 engine build stopped in vmm.cpp. Same
fallback expert_cache.cpp already carries, and the sticky error of a failed lookup is cleared.
Co-authored-by: zhouxihong1 <[email protected]>
The STRATA_HIP_GFX906 build defines STRATA_HIP_GFX906, not STRATA_USE_HIP, so three
sites that gate on STRATA_USE_HIP still take the CUDA branch and fail to compile:
1. fused_gr.cu: a bare 'return;' in the bool fused_gr_read_multi -> 'return false;'.
2. mtp.cpp: the MTP stream-priority block (cudaDeviceGetStreamPriorityRange) is gated
on !defined(STRATA_HIP_GFX906) too.
3. vmm.cpp: the <cuda.h> include and the CUDA driver VMM code get the same gate.
All three are inside #if blocks that only the gfx906 build reaches.
vmm.hpp only includes <cstdint> and <vector>, so the unqualified size_t is not
declared on toolchains that do not expose ::size_t through those headers
(GCC 12.2, Debian 12). Qualify it as std::size_t, as expert_cache.hpp does.
Bench folder ported from PR #1078 by Hugua700, authorship kept.
Trimmed for 0.1.40.2: 0 raw dumps/logs/binaries left out (listed in TRIMMED.md of each folder).
Bench folder ported from PR #1062 by fioruccione, authorship kept.
Trimmed for 0.1.40.2: 7 raw dumps/logs/binaries left out (listed in TRIMMED.md of each folder).
Not ported: docs/COMMUNITY_BENCHMARKS.md.
Bench folder ported from PR #1028 by sarge18, authorship kept.
Trimmed for 0.1.40.2: 8 raw dumps/logs/binaries left out (listed in TRIMMED.md of each folder).
Bench folder ported from PR #1027 by GPF, authorship kept.
Trimmed for 0.1.40.2: 6 raw dumps/logs/binaries left out (listed in TRIMMED.md of each folder).
Bench folder ported from PR #1016 by avarakin, authorship kept.
Trimmed for 0.1.40.2: 21 raw dumps/logs/binaries left out (listed in TRIMMED.md of each folder).
Not ported: docs/COMMUNITY_BENCHMARKS.md.
Bench folder ported from PR #995 by lfontanez, authorship kept.
Trimmed for 0.1.40.2: 0 raw dumps/logs/binaries left out (listed in TRIMMED.md of each folder).
Not ported: bench/results/2026-09-29-speed-0126/README.md, bench/results/2026-09-29-speed-0126/matrix.json, docs/COMMUNITY_BENCHMARKS.md, tools/integrate_speed_0126_shin_blackmamba.py.
Bench folder ported from PR #832 by Zauberio, authorship kept.
Trimmed for 0.1.40.2: 0 raw dumps/logs/binaries left out (listed in TRIMMED.md of each folder).
Bench folder ported from PR #466 by gravitomagnetic, authorship kept.
Trimmed for 0.1.40.2: 1 raw dumps/logs/binaries left out (listed in TRIMMED.md of each folder).
Bench folder ported from PR #993 by obiscr, authorship kept.
Trimmed for 0.1.40.2: 1 raw dumps/logs/binaries left out (listed in TRIMMED.md of each folder).
Not ported: docs/COMMUNITY_BENCHMARKS.md.
Bench folder ported from PR #913 by 1872183316, authorship kept.
Trimmed for 0.1.40.2: 0 raw dumps/logs/binaries left out (listed in TRIMMED.md of each folder).
Not ported: docs/COMMUNITY_BENCHMARKS.md.
Bench folder ported from PR #902 by Mitsuasa513, authorship kept.
Trimmed for 0.1.40.2: 0 raw dumps/logs/binaries left out (listed in TRIMMED.md of each folder).
Not ported: docs/COMMUNITY_BENCHMARKS.md.
Bench folder ported from PR #882 by T-Crypt, authorship kept.
Trimmed for 0.1.40.2: 1 raw dumps/logs/binaries left out (listed in TRIMMED.md of each folder).
Bench folder ported from PR #850 by christopherrobertbrooks-tech, authorship kept.
Trimmed for 0.1.40.2: 0 raw dumps/logs/binaries left out (listed in TRIMMED.md of each folder).
Bench folder ported from PR #834 by T-Crypt, authorship kept.
Trimmed for 0.1.40.2: 4 raw dumps/logs/binaries left out (listed in TRIMMED.md of each folder).
Bench folder ported from PR #823 by christopherrobertbrooks-tech, authorship kept.
Trimmed for 0.1.40.2: 0 raw dumps/logs/binaries left out (listed in TRIMMED.md of each folder).
Bench folder ported from PR #815 by did-technomancer, authorship kept.
Trimmed for 0.1.40.2: 3 raw dumps/logs/binaries left out (listed in TRIMMED.md of each folder).
Not ported: docs/COMMUNITY_BENCHMARKS.md.
Bench folder ported from PR #780 by 1314521gjy, authorship kept.
Trimmed for 0.1.40.2: 2 raw dumps/logs/binaries left out (listed in TRIMMED.md of each folder).
Not ported: docs/COMMUNITY_BENCHMARKS.md.
Bench folder ported from PR #757 by 30crows, authorship kept.
Trimmed for 0.1.40.2: 4 raw dumps/logs/binaries left out (listed in TRIMMED.md of each folder).
Not ported: docs/COMMUNITY_BENCHMARKS.md.
Bench folder ported from PR #745 by xyzzing, authorship kept.
Trimmed for 0.1.40.2: 0 raw dumps/logs/binaries left out (listed in TRIMMED.md of each folder).
Bench folder ported from PR #740 by jase100k, authorship kept.
Trimmed for 0.1.40.2: 3 raw dumps/logs/binaries left out (listed in TRIMMED.md of each folder).
Bench folder ported from PR #721 by zimuhuan-code, authorship kept.
Trimmed for 0.1.40.2: 8 raw dumps/logs/binaries left out (listed in TRIMMED.md of each folder).
Bench folder ported from PR #674 by pertain99, authorship kept.
Trimmed for 0.1.40.2: 0 raw dumps/logs/binaries left out (listed in TRIMMED.md of each folder).
Not ported: docs/COMMUNITY_BENCHMARKS.md.
Bench folder ported from PR #624 by Dmitry-B, authorship kept.
Trimmed for 0.1.40.2: 0 raw dumps/logs/binaries left out (listed in TRIMMED.md of each folder).
Not ported: docs/COMMUNITY_BENCHMARKS.md.
Bench folder ported from PR #618 by HarukiOtaku, authorship kept.
Trimmed for 0.1.40.2: 2 raw dumps/logs/binaries left out (listed in TRIMMED.md of each folder).
Bench folder ported from PR #521 by lijieming, authorship kept.
Trimmed for 0.1.40.2: 2 raw dumps/logs/binaries left out (listed in TRIMMED.md of each folder).
Bench folder ported from PR #483 by Efs-O, authorship kept.
Trimmed for 0.1.40.2: 0 raw dumps/logs/binaries left out (listed in TRIMMED.md of each folder).
Bench folder ported from PR #440 by Ambolio, authorship kept.
Trimmed for 0.1.40.2: 4 raw dumps/logs/binaries left out (listed in TRIMMED.md of each folder).
Bench folder ported from PR #418 by qni-live, authorship kept.
Trimmed for 0.1.40.2: 0 raw dumps/logs/binaries left out (listed in TRIMMED.md of each folder).
Bench folder ported from PR #416 by pipeob0, authorship kept.
Trimmed for 0.1.40.2: 0 raw dumps/logs/binaries left out (listed in TRIMMED.md of each folder).
Bench folder ported from PR #404 by rwkeyes, authorship kept.
Trimmed for 0.1.40.2: 0 raw dumps/logs/binaries left out (listed in TRIMMED.md of each folder).
Bench folder ported from PR #811 by steve8697, authorship kept.
Trimmed for 0.1.40.2: 27 raw dumps/logs/binaries left out (listed in TRIMMED.md of each folder).
Bench folder ported from PR #777 by steve8697, authorship kept.
Trimmed for 0.1.40.2: 21 raw dumps/logs/binaries left out (listed in TRIMMED.md of each folder).
Bench folder ported from PR #758 by xxDoman, authorship kept.
Trimmed for 0.1.40.2: 21 raw dumps/logs/binaries left out (listed in TRIMMED.md of each folder).
Bench folder ported from PR #698 by CYoung83, authorship kept.
Trimmed for 0.1.40.2: 0 raw dumps/logs/binaries left out (listed in TRIMMED.md of each folder).
Bench folder ported from PR #628 by Yasei-no-otoko, authorship kept.
Trimmed for 0.1.40.2: 30 raw dumps/logs/binaries left out (listed in TRIMMED.md of each folder).
Not ported: docs/AMD_HIP.md.
Bench folder ported from PR #508 by R6DJO, authorship kept.
Trimmed for 0.1.40.2: 40 raw dumps/logs/binaries left out (listed in TRIMMED.md of each folder).
Bench folder ported from PR #433 by brenoperucchi, authorship kept.
Trimmed for 0.1.40.2: 2 raw dumps/logs/binaries left out (listed in TRIMMED.md of each folder).
Bench folder ported from PR #417 by mlfather, authorship kept.
Trimmed for 0.1.40.2: 162 raw dumps/logs/binaries left out (listed in TRIMMED.md of each folder).
Bench folder ported from PR #707 by noahark, authorship kept.
Trimmed for 0.1.40.2: 0 raw dumps/logs/binaries left out (listed in TRIMMED.md of each folder).
Not ported: docs/COMMUNITY_BENCHMARKS.md.
Bench folder ported from PR #499 by homeofe, authorship kept.
Trimmed for 0.1.40.2: 2 raw dumps/logs/binaries left out (listed in TRIMMED.md of each folder).
Not ported: docs/COMMUNITY_BENCHMARKS.md.
Bench folder ported from PR #469 by homeofe, authorship kept.
Trimmed for 0.1.40.2: 4 raw dumps/logs/binaries left out (listed in TRIMMED.md of each folder).
Not ported: docs/COMMUNITY_BENCHMARKS.md.
Bench folder ported from PR #389 by shrisha108, authorship kept.
Trimmed for 0.1.40.2: 1 raw dumps/logs/binaries left out (listed in TRIMMED.md of each folder).
Not ported: docs/COMMUNITY_BENCHMARKS.md.
The live smoke showed the same quoted-example problem in the answer: a model that wrote the call format in a
fenced example after </think> got a real tool call. The content parser now uses the fence / inline-code
tracking of the reasoning rescue: an opener inside a ``` or ~~~ fence (tracked across stream chunks) or inside
inline code stays text, the decision depending only on the text before it, so streamed and whole outputs agree.
Openers at the top level fire as before, including one after a sentence on the same line (none of the 37
corpus specimens needed that, but the old tests do and nothing argues against it). A closed call at the end of
an answer cut by max tokens stays a call.
Tests: quoted calls stay text and real ones fire at every chunk split; OpenAI (whole and streamed), Anthropic
and Responses over HTTP.
Found in the live smoke: an old request that learns its engine died while restart() has already taken the
process (self.proc is None) failed in death_note() with an AttributeError, and its client got no reply at
all. death_note(), alive() and _send() now read the process once and treat None as a dead engine.
Test: after close() the death note is a sentence, alive() is False and a send raises EngineDied.
Found in the live smoke of #1012: a long prompt being admitted when the engine is killed read its own
end-of-engine line (EngineDied), and its cleanup then drained the control lines for up to 300 s for an
end line that had been read already; the client got its 503 only after that. The cleanup now skips the
STOP and the drain when the engine process the request was sent to is gone (the server counts its engine
processes in gen, and a request notes the one it has the control lines of), and the drain polls once a
second and gives up as soon as that engine is gone.
Tests: a long read killed alone, and with short requests arriving meanwhile (it gives way and is admitted):
every request ends in a 503 within seconds, and the next request restarts the engine.
Versions are read as their first three numbers everywhere; the one place that was not (the version compiled
into an engine binary, `engine=0.1.40.1`) now accepts an optional fourth number. A test pins that a hotfix
tag keeps finding the engine zips: the checkout's v<project version> release first, the latest release second.
StrataEngine.restart() runs __init__ again, which made a new condition variable, control lock, wait counter
and wait list while requests were still waiting on the old ones: they hung for good, or failed with
`list.remove(x): x not in list`. The admission state is now made once and kept; restart() wakes the waiters
when it ends. A request that waits for the control lines now ends with a clean EngineDied (503, "not sent")
when the engine is gone and nobody is starting it, keeps waiting while a restart is under way, and goes on
with the new engine when it is up. The lock is checked again after it is taken, a slot that frees itself
after a restart keeps the lists of its own process, and the wait counters cannot go below zero.
Tests (fake batch engine behind the real StrataEngine and Service): waiters during a restart are served by
the new engine (2 and 4 slots, same condition variable and lock), get a clean 503 when the engine stays down,
and when the restart itself fails; the next request restarts and is answered; the solo engine is unchanged.
The #804 rescue turned any declared <tool_call> in the reasoning into a real call, so a call that was only
quoted (in a code fence, mid-sentence, in inline code, followed by more prose) ran, and so did one in a reply
cut by max tokens (47Hunter47's `rm -rf build` case). Now it must start a line (after optional whitespace),
be outside a code fence (``` or ~~~, tracked across chunks) and inline code, close with </tool_call>, and have
only whitespace or more calls after it before the end of the turn or </think>. Such calls wait until then,
and become reasoning text again when the turn ends by length, cancel or error. The decision is the same
streamed or whole. Declared calls beside an undeclared wrapper are still delivered, the wrapper stays
reasoning.
The server tells the parser how the turn ended, logs how many calls were read or kept as text, and counts
the read ones in the metrics totals (tool_calls_from_reasoning).
Tests: 37 recorded and constructed specimens (PR #525's corpus and 47Hunter47's 12 live strandings; 16 act,
21 do not) at chunk sizes 1, 2, 3, 7, random and whole; fence and opener split at every position; OpenAI
(whole and streamed), Anthropic and Responses over HTTP.
With "parallel": N, a batch window that fails prints its reason as an ERR
line on stdout and the engine exits with code 1. At that point every
request reads its own slot's lines, so nobody reads that ERR line, and the
server's note fell back to the log's last "strata" line (a stats line in
#997), or to "the usual cause is running out of RAM" without a log.
The pump now keeps an ERR line that was the engine's last stdout line, and
death_note gives it first. test_parallel's fake engine can fail its first
window over two slots: the note names the error.
(cherry picked from commit 9c5066303e)
2026-10-06 09:45:16 +02:00
2309 changed files with 466347 additions and 4424 deletions
description:Something in Strata doesn't work as documented
title:"[Bug]: "
labels:["bug"]
body:
- type:markdown
value:|
Thanks for the report.
Please search the [open issues](https://github.com/Niko1221/Strata/issues?q=is%3Aissue+state%3Aopen) first — this may already be tracked.
- type:textarea
id:what
attributes:
label:What happened
description:What you did, what you got, and what you expected instead. Paste the exact error text if there is one. If there's a set of steps or commands that reproduces it, put them here too.
validations:
required:true
- type:input
id:hardware
attributes:
label:Strata version, GPU, OS
description:The engine version is on the About tab, or in `GET /v1/status` under `engine`.
placeholder:"0.1.31 · RTX 5070 12GB · Windows 11"
validations:
required:true
- type:textarea
id:log
attributes:
label:Engine log
description:Drag in `strata-<model>.log` from your Strata folder. Drag and drop works directly into this box.
validations:
required:false
- type:markdown
value:|
A couple of extras that help a lot:
- Check the [troubleshooting table](https://github.com/Niko1221/Strata/blob/main/docs/DETAILS.md#troubleshooting) first, many reports are already solved there.
description:Suggest something new or an improvement
title:"[Feature]: "
labels:["enhancement"]
body:
- type:markdown
value:|
Explain what you want in plain language.
- type:textarea
id:idea
attributes:
label:What do you want to do?
description:Describe the goal rather than the implementation. If there's a workaround you use today, mention it.
validations:
required:true
- type:markdown
value:|
All of this counts: model and architecture support, the server and API, the web app, multi-GPU and layer split, images and vision, MCP servers, setup and installation, and performance.
<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>
<sub>Strata は無料で使えます。お使いの PC で快適に動いたら、コーヒー 1 杯分のご支援が開発を続ける励みになります。</sub></p>
<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>
@@ -109,6 +109,8 @@ download stops, run it again: it continues where it left off. Your browser opens
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).
**Is it fast on your PC?** Run `BENCH.bat` (`./bench.sh`): about 5-10 minutes, it measures chat, 4K/16K prompts and an agent-style session with your own settings and writes a report to share ([how](docs/COMMUNITY_BENCHMARKS.md#run-the-benchmark)).
## Which model should I pick?
The installer recommends one for your RAM. The same model comes in several sizes, compressed more or less. Smaller
{"d":"code","p":"Write a Python function that merges two sorted lists into one sorted list without using sort(). Include type hints and a short docstring."},
{"d":"code","p":"Implement an LRU cache in Python using OrderedDict with get and put methods, and write three unit tests with pytest."},
{"d":"code","p":"Write a JavaScript function debounce(fn, wait) and explain how it differs from throttle, with a small usage example."},
{"d":"code","p":"Write a C++ class template for a fixed-capacity ring buffer with push, pop and size, using std::array. Mention thread-safety."},
{"d":"code","p":"Show a Rust function that reads a file line by line, counts word frequencies in a HashMap and prints the top 5 words."},
{"d":"code","p":"Write a Go HTTP handler that returns JSON {\"status\":\"ok\"} and a middleware that logs the request duration."},
{"d":"code","p":"Write a SQL query that returns the top 3 customers by total order value per country from tables customers(id,country) and orders(id,customer_id,amount)."},
{"d":"code","p":"Write a bash script that finds all files larger than 100 MB under /var/log, sorted by size, and prints them in human readable form."},
{"d":"code","p":"Explain this Python error and how to fix it:\nTraceback (most recent call last):\n File \"app.py\", line 12, in <module>\n print(data['users'][0]['name'])\nKeyError: 'users'"},
{"d":"code","p":"Refactor this function to be more readable:\ndef f(a):\n r=[]\n for i in range(len(a)):\n if a[i]%2==0:\n r.append(a[i]*a[i])\n return r"},
{"d":"code","p":"Write a regular expression that matches ISO 8601 dates like 2026-10-11 and explain each part."},
{"d":"code","p":"Write a TypeScript interface for a paginated API response and a generic fetch helper that validates the shape at runtime."},
{"d":"math","p":"A train leaves a station at 9:15 and travels at 80 km/h. A second train leaves the same station at 10:00 on the same track at 120 km/h. At what time does the second catch the first? Show the steps."},
{"d":"math","p":"Prove that the sum of the first n odd numbers is n squared, using induction."},
{"d":"math","p":"Solve the system: 3x + 2y = 16 and x - y = 2. Then check the solution."},
{"d":"math","p":"What is the probability of getting exactly two heads in five fair coin flips? Explain the formula you use."},
{"d":"reason","p":"Three friends, Ann, Ben and Cara, each own exactly one of a cat, a dog and a parrot. Ann does not own the dog. Ben owns neither the cat nor the parrot. Who owns what? Explain your reasoning."},
{"d":"reason","p":"I have a 3 liter jug and a 5 liter jug and unlimited water. How can I measure exactly 4 liters?"},
{"d":"chat","p":"I have a job interview tomorrow for a junior data analyst role and I am nervous. Can you give me practical advice?"},
{"d":"chat","p":"What are some good ways to learn a new language as an adult with only 30 minutes a day?"},
{"d":"chat","p":"Recommend a weekly meal prep plan for someone who wants high protein, vegetarian meals on a budget."},
{"d":"chat","p":"My laptop gets very hot and the fan is loud when I open many browser tabs. What can I check?"},
{"d":"creative","p":"Write a short story (about 300 words) about a lighthouse keeper who discovers that the light attracts something other than ships."},
{"d":"creative","p":"Write a poem about autumn rain in the style of a haiku sequence, five stanzas."},
{"d":"creative","p":"Describe a bustling night market in a fantasy city, using all five senses."},
{"d":"science","p":"Explain how a transformer neural network processes a sentence, from tokens to the next-token prediction, for a software engineer who has not studied machine learning."},
{"d":"science","p":"Why is the sky blue and sunsets red? Explain Rayleigh scattering without heavy math."},
{"d":"science","p":"Explain how CRISPR-Cas9 edits a gene, and name two ethical concerns."},
{"d":"history","p":"Summarize the causes and consequences of the fall of the Western Roman Empire in a few paragraphs."},
{"d":"history","p":"Compare the economic policies of the New Deal with those of the Reagan era."},
{"d":"law","p":"Explain the difference between a trademark, a copyright and a patent, with one everyday example of each."},
{"d":"business","p":"Draft a polite email to a customer whose order is delayed by two weeks, offering a 10 percent discount and a new delivery date."},
{"d":"business","p":"Write a one-page project proposal outline for migrating a small company's file server to a cloud storage service."},
{"d":"data","p":"Create a markdown table comparing PostgreSQL, MySQL and SQLite on concurrency, typical use cases, licensing and scaling."},
{"d":"data","p":"Return a JSON object describing a fictional book with fields title, author, year, genres (array) and a nested publisher object. Only output JSON."},
{"d":"tool","p":"You are an assistant with a tool get_weather(city, unit). The user asks: 'Do I need an umbrella in Hamburg tomorrow?' Write the tool call you would make as JSON and then explain what you would tell the user once you get the result."},
{"d":"summ","p":"Summarize in three bullet points: Photosynthesis converts light energy into chemical energy. In the light-dependent reactions, chlorophyll absorbs photons and splits water, releasing oxygen and producing ATP and NADPH. In the Calvin cycle, these carriers drive the fixation of carbon dioxide into sugars. The rate of photosynthesis depends on light intensity, carbon dioxide concentration and temperature."},
{"d":"fr","p":"Explique en français comment fonctionne un moteur à combustion à quatre temps, avec des phrases simples."},
{"d":"de","p":"Schreibe eine kurze, freundliche E-Mail auf Deutsch, in der du einen Termin beim Zahnarzt von Dienstag auf Donnerstag verschiebst."},
{"d":"es","p":"Dame una receta sencilla de tortilla española y explica los errores más comunes al hacerla."},
"content":"The source code defines a series of 140 exported functions, named `rule0` through `rule139`, each of which accepts a single numeric argument `x` and applies a conditional transformation based on a specific integer constant associated with the function's name. For any given function `ruleN`, if the input `x` is strictly equal to the integer `N`, the function returns `x + (N + 1)`; otherwise, it returns `x - N`. This pattern creates a set of specialized handlers where the \"true\" branch increments the input by one more than the rule's index, while the"
}
},
{
"label":"0.1.30",
"kind":"followup",
"trial":1,
"wall_s":9.036391863017343,
"metrics":{
"prompt_tokens":4451.0,
"reused":4338.0,
"fresh":113.0,
"prefill_ms":1734.0,
"prefill_tps":65.2,
"generated":128.0,
"decode_ms":7293.0,
"decode_tps":17.6
},
"usage":{
"prompt_tokens":4451,
"completion_tokens":128,
"total_tokens":4579,
"prompt_tokens_details":{
"cached_tokens":4338
}
},
"finish_reason":"length",
"message":{
"role":"assistant",
"content":"The most relevant boundary case is the strict integer equality check (`x === N`) within each function, which acts as a precise trigger for the \"increment\" branch while defaulting to a \"decrement\" branch for all other values. This creates a highly sensitive behavior where the output depends entirely on whether the input matches the function's specific index; for example, `rule0(0)` returns `1` (the increment path), whereas `rule0(1)` returns `1` (the decrement path), and `rule0(-1)` returns `-1` (the decrement path). The smallest useful regression test should verify this critical"
}
},
{
"label":"0.1.30",
"kind":"fresh",
"trial":2,
"wall_s":23.5992965750047,
"metrics":{
"prompt_tokens":8830.0,
"reused":0.0,
"fresh":8830.0,
"prefill_ms":16290.0,
"prefill_tps":542.0,
"generated":128.0,
"decode_ms":7296.0,
"decode_tps":17.5
},
"usage":{
"prompt_tokens":8830,
"completion_tokens":128,
"total_tokens":8958,
"prompt_tokens_details":{
"cached_tokens":0
}
},
"finish_reason":"length",
"message":{
"role":"assistant",
"content":"The source code defines a series of 280 exported functions, named `rule0` through `rule279`, each accepting a single numeric argument `x`. Every function follows an identical logical structure: it checks if the input `x` is strictly equal to the integer value embedded in its function name (denoted as `n`); if this condition is true, the function returns `x + n + 1`, and if the condition is false, it returns `x - n`. Consequently, each function acts as a conditional modifier that either increments the input by one more than its index or decrements it by its"
}
},
{
"label":"0.1.30",
"kind":"followup",
"trial":2,
"wall_s":9.576413626025897,
"metrics":{
"prompt_tokens":9071.0,
"reused":8957.0,
"fresh":114.0,
"prefill_ms":2196.0,
"prefill_tps":51.9,
"generated":128.0,
"decode_ms":7367.0,
"decode_tps":17.4
},
"usage":{
"prompt_tokens":9071,
"completion_tokens":128,
"total_tokens":9199,
"prompt_tokens_details":{
"cached_tokens":8957
}
},
"finish_reason":"length",
"message":{
"role":"assistant",
"content":"The most relevant boundary case is the **exact integer equality check** (`x === n`), which acts as a strict gatekeeper for the \"increment\" branch. For any function `ruleN`, the input `x` must be the specific integer `N` to trigger the `x + N + 1` return path; any other value, including `N + 1`, `N - 1`, or non-integers, falls through to the `x - N` return path. This creates a highly sensitive behavior where a single unit change in input flips the output from a large positive jump to a large negative subtraction."
}
},
{
"label":"0.1.30",
"kind":"fresh",
"trial":3,
"wall_s":11.473167807038408,
"metrics":{
"prompt_tokens":4210.0,
"reused":0.0,
"fresh":4210.0,
"prefill_ms":7721.0,
"prefill_tps":545.2,
"generated":128.0,
"decode_ms":3744.0,
"decode_tps":34.2
},
"usage":{
"prompt_tokens":4210,
"completion_tokens":128,
"total_tokens":4338,
"prompt_tokens_details":{
"cached_tokens":0
}
},
"finish_reason":"length",
"message":{
"role":"assistant",
"content":"The source code defines a series of 140 exported functions, named `rule0` through `rule139`, each of which accepts a single numeric argument `x` and applies a conditional logic based on a specific integer constant associated with the function's name. For any given function `ruleN`, if the input `x` is strictly equal to the integer `N`, the function returns `x + (N + 1)`; otherwise, it returns `x - N`. This means that for every function, there is exactly one input value that causes an increment by `N + 1`, while all other inputs"
}
},
{
"label":"0.1.30",
"kind":"followup",
"trial":3,
"wall_s":6.953374623961281,
"metrics":{
"prompt_tokens":4451.0,
"reused":4338.0,
"fresh":113.0,
"prefill_ms":867.0,
"prefill_tps":130.3,
"generated":128.0,
"decode_ms":6078.0,
"decode_tps":21.1
},
"usage":{
"prompt_tokens":4451,
"completion_tokens":128,
"total_tokens":4579,
"prompt_tokens_details":{
"cached_tokens":4338
}
},
"finish_reason":"length",
"message":{
"role":"assistant",
"content":"The most relevant boundary case is the **exact integer equality check** (`x === N`), which acts as a strict gate for the \"special\" branch (`x + N + 1`) versus the \"default\" branch (`x - N`). This behavior is highly sensitive to type coercion and precision: if `x` is a floating-point number that is mathematically equal to `N` but not strictly identical (e.g., due to floating-point errors), or if `x` is a string like `\"5\"` for `rule5`, the strict equality `===` will fail, causing the function to fall into the default"
}
},
{
"label":"0.1.30",
"kind":"fresh",
"trial":4,
"wall_s":16.981091229012236,
"metrics":{
"prompt_tokens":8830.0,
"reused":0.0,
"fresh":8830.0,
"prefill_ms":12624.0,
"prefill_tps":699.5,
"generated":128.0,
"decode_ms":4343.0,
"decode_tps":29.5
},
"usage":{
"prompt_tokens":8830,
"completion_tokens":128,
"total_tokens":8958,
"prompt_tokens_details":{
"cached_tokens":0
}
},
"finish_reason":"length",
"message":{
"role":"assistant",
"content":"The source code defines a series of exported functions named `rule0` through `rule279`, each accepting a single numeric argument `x` and returning a numeric result based on a specific conditional logic. For any given function `ruleN`, the behavior is identical: if the input `x` is strictly equal to the integer `N` (the number embedded in the function name), the function returns `x + N + 1`; otherwise, it returns `x - N`. This pattern creates a set of 280 distinct functions where each function acts as a specialized transformation that increments the input by `N+1"
}
},
{
"label":"0.1.30",
"kind":"followup",
"trial":4,
"wall_s":6.207711412978824,
"metrics":{
"prompt_tokens":9071.0,
"reused":8823.0,
"fresh":248.0,
"prefill_ms":1062.0,
"prefill_tps":233.6,
"generated":128.0,
"decode_ms":5132.0,
"decode_tps":24.9
},
"usage":{
"prompt_tokens":9071,
"completion_tokens":128,
"total_tokens":9199,
"prompt_tokens_details":{
"cached_tokens":8823
}
},
"finish_reason":"length",
"message":{
"role":"assistant",
"content":"The most relevant boundary case is the **exact integer equality check** (`x === N`), which acts as the primary switch between the two arithmetic branches. For any function `ruleN`, the behavior changes drastically at the single point where `x` equals `N`. Specifically, if `x === N`, the result is `2N + 1` (since `x + N + 1` = `N + N + 1`), whereas for any other value, the result is `x - N`. This creates a discontinuity in the function's output curve.\n\nThe **smallest useful regression test** should"
"content":"The source code defines a series of 140 exported functions, named `rule0` through `rule139`, each of which accepts a single numeric argument `x` and applies a conditional transformation based on a specific integer constant associated with the function's name. For any given function `ruleN`, if the input `x` is strictly equal to the integer `N`, the function returns `x + (N + 1)`; otherwise, it returns `x - N`. This pattern creates a set of specialized arithmetic operations where the \"true\" branch adds an incrementally larger value (starting from 1 for `"
}
},
{
"label":"0.1.31",
"kind":"followup",
"trial":1,
"wall_s":8.811686859990004,
"metrics":{
"prompt_tokens":4451.0,
"reused":4203.0,
"fresh":248.0,
"prefill_ms":2051.0,
"prefill_tps":120.9,
"generated":128.0,
"decode_ms":6752.0,
"decode_tps":19.0
},
"usage":{
"prompt_tokens":4451,
"completion_tokens":128,
"total_tokens":4579,
"prompt_tokens_details":{
"cached_tokens":4203
}
},
"finish_reason":"length",
"message":{
"role":"assistant",
"content":"The most relevant boundary case is the **exact integer equality check** (`x === N`), which acts as a strict gate for the \"special\" branch. For any function `ruleN`, the behavior changes drastically only when `x` is exactly `N`. For example, in `rule0`, if `x` is `0`, it returns `1` (`0 + 1`); if `x` is `1`, it returns `1` (`1 - 0`). This creates a collision where different inputs can produce the same output, but more importantly, it highlights that the function is **not** a simple"
}
},
{
"label":"0.1.31",
"kind":"fresh",
"trial":2,
"wall_s":15.694618315959815,
"metrics":{
"prompt_tokens":8830.0,
"reused":0.0,
"fresh":8830.0,
"prefill_ms":11265.0,
"prefill_tps":783.8,
"generated":128.0,
"decode_ms":4417.0,
"decode_tps":29.0
},
"usage":{
"prompt_tokens":8830,
"completion_tokens":128,
"total_tokens":8958,
"prompt_tokens_details":{
"cached_tokens":0
}
},
"finish_reason":"length",
"message":{
"role":"assistant",
"content":"This source code defines a collection of 280 exported functions, named `rule0` through `rule279`, each of which accepts a single numeric argument `x` and implements a specific conditional arithmetic transformation based on a unique integer constant associated with its name. For any given function `ruleN`, the logic checks if the input `x` is strictly equal to the integer `N`; if this condition is true, the function returns `x + (N + 1)`, effectively adding the next consecutive integer to the input, whereas if the condition is false, it returns `x - N`, subtracting the function"
}
},
{
"label":"0.1.31",
"kind":"followup",
"trial":2,
"wall_s":6.075737391016446,
"metrics":{
"prompt_tokens":9071.0,
"reused":8957.0,
"fresh":114.0,
"prefill_ms":835.0,
"prefill_tps":136.5,
"generated":128.0,
"decode_ms":5225.0,
"decode_tps":24.5
},
"usage":{
"prompt_tokens":9071,
"completion_tokens":128,
"total_tokens":9199,
"prompt_tokens_details":{
"cached_tokens":8957
}
},
"finish_reason":"length",
"message":{
"role":"assistant",
"content":"The most relevant boundary case is the **exact integer equality check** (`x === N`), which acts as a strict gatekeeper for the \"special\" branch (`x + N + 1`). This is critical because it distinguishes the single \"trigger\" value from all other inputs, which fall into the \"default\" branch (`x - N`).\n\n**Key Boundary & Edge Cases:**\n\n1. **Integer Equality (`x === N`):**\n * **Trigger:** When `x` exactly equals the rule's index `N`, the function returns `N + (N + 1) = 2N"
}
},
{
"label":"0.1.31",
"kind":"fresh",
"trial":3,
"wall_s":6.66396439098753,
"metrics":{
"prompt_tokens":4210.0,
"reused":0.0,
"fresh":4210.0,
"prefill_ms":4679.0,
"prefill_tps":899.7,
"generated":128.0,
"decode_ms":1978.0,
"decode_tps":64.7
},
"usage":{
"prompt_tokens":4210,
"completion_tokens":128,
"total_tokens":4338,
"prompt_tokens_details":{
"cached_tokens":0
}
},
"finish_reason":"length",
"message":{
"role":"assistant",
"content":"The source code defines a series of 140 exported functions, named `rule0` through `rule139`, each of which accepts a single numeric argument `x` and applies a conditional transformation based on a specific integer constant associated with the function's name. For any given function `ruleN`, if the input `x` is strictly equal to the integer `N`, the function returns `x + (N + 1)`; otherwise, it returns `x - N`. This pattern creates a set of specialized arithmetic operations where the \"true\" branch adds an incrementally larger value (starting from 1 for `"
}
},
{
"label":"0.1.31",
"kind":"followup",
"trial":3,
"wall_s":7.167138791992329,
"metrics":{
"prompt_tokens":4451.0,
"reused":4203.0,
"fresh":248.0,
"prefill_ms":900.0,
"prefill_tps":275.6,
"generated":128.0,
"decode_ms":6259.0,
"decode_tps":20.5
},
"usage":{
"prompt_tokens":4451,
"completion_tokens":128,
"total_tokens":4579,
"prompt_tokens_details":{
"cached_tokens":4203
}
},
"finish_reason":"length",
"message":{
"role":"assistant",
"content":"The most relevant boundary case is the strict integer equality check (`x === N`) within each function, which acts as a precise trigger for the \"true\" branch; for example, in `rule0`, the input `0` is the critical boundary where the behavior shifts from the default subtraction (`x - 0`, which is identity) to the special addition (`x + 1`), making `rule0(0)` return `1` while `rule0(1)` returns `1` (via `1 - 0`), creating a collision that highlights the fragility of this design. The smallest useful regression test would"
}
},
{
"label":"0.1.31",
"kind":"fresh",
"trial":4,
"wall_s":11.244427776022349,
"metrics":{
"prompt_tokens":8830.0,
"reused":0.0,
"fresh":8830.0,
"prefill_ms":8888.0,
"prefill_tps":993.5,
"generated":128.0,
"decode_ms":2345.0,
"decode_tps":54.6
},
"usage":{
"prompt_tokens":8830,
"completion_tokens":128,
"total_tokens":8958,
"prompt_tokens_details":{
"cached_tokens":0
}
},
"finish_reason":"length",
"message":{
"role":"assistant",
"content":"The source code defines a series of 280 exported functions, named `rule0` through `rule279`, each of which accepts a single numeric argument `x` and implements a specific conditional arithmetic operation based on a unique integer constant associated with the function's name. For any given function `ruleN`, the logic checks if the input `x` is strictly equal to the integer `N`; if this condition is true, the function returns `x + (N + 1)`, effectively adding the successor of the rule index to the input, whereas if the condition is false, it returns `x - N`,"
}
},
{
"label":"0.1.31",
"kind":"followup",
"trial":4,
"wall_s":4.393163571017794,
"metrics":{
"prompt_tokens":9071.0,
"reused":8958.0,
"fresh":113.0,
"prefill_ms":721.0,
"prefill_tps":156.8,
"generated":128.0,
"decode_ms":3659.0,
"decode_tps":35.0
},
"usage":{
"prompt_tokens":9071,
"completion_tokens":128,
"total_tokens":9199,
"prompt_tokens_details":{
"cached_tokens":8958
}
},
"finish_reason":"length",
"message":{
"role":"assistant",
"content":"The most relevant boundary case is the **exact integer equality check** (`x === N`), which acts as a strict gatekeeper for the \"special\" branch (`x + N + 1`) versus the \"default\" branch (`x - N`). This creates a discontinuity in the function's behavior: for `ruleN`, the output jumps from `N - N = 0` (when `x = N - 1`) to `N + N + 1 = 2N + 1` (when `x = N`), and then back down to `N + 1 - N = 1"
| Storage | Model on a 3.7 TB NVMe at PCIe 4.0 x4 (`/mnt/models`). Disk only matters at startup: during generation the server read 23 MiB for 400 tokens (experts are resident in RAM/VRAM, 100% expert cache hits). Cold start was not measured. |
## Software
- Ubuntu 26.04.1 LTS, kernel 7.0.0-31-generic
- NVIDIA driver 595.84, CUDA 13.3 toolkit libraries (`/usr/local/cuda-13.3`)
- Strata engine 0.1.30 (release download, not a git checkout; no commit hash)
Review the following C++ source file from the Strata engine (v0.1.32, src/core/expert_source.cpp, first 9,000 bytes). List the three most important correctness risks you see, with line references.
// src/core/expert_source.cpp - the adapter. See the header for the three clauses of the contract.
Loaded 100 of 2309 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.