Five conflicts, resolved keeping every f-024-rel behaviour (slots pin, long-prompt skip, HIPK, ZC fix) and maya2's:
- ds4_main.cpp, request struct: both fields kept (pin=N from f-024-slots and dspark_cum from maya2).
- ds4_main.cpp, request parser: both keys kept (pin, dspark_cum/spec_cum).
- ds4_main.cpp, prompt feed loop: slots' pin_at (the read stops at N for the shared-start checkpoint 3) and maya2's Stop handling (checkpoint 1 before the prompt, set_cancel, the stop check at the top of each feed) combined: the loop checks Stop first, then sizes the feed with the pin cut (n is no longer const).
- ds4_model.cu, forward chunk loop (two places): the skip's 'run.runs(k, il)' (an early chunk skipped past the yoco layer) kept, and maya2's halt_now() / halt_end(di, err) check added after it, so a skipped layer does no halt work.
win_submit (ds4_kernels.cu) is compiled to a no-op outside _WIN32 (#if defined(_WIN32) ... else (void) s), so it is already out of the Linux CUDA and Linux HIP builds.
Owner PC (RTX 5070 12 GB, Ryzen 5 7600, official 2.00 pack, shipped profile-2.00.bin, 3 alternating pairs, medians, off = every switch off):
4K prompt, off 92.64 tok/s: win_submit 99.12 (+7.0%, 3/3 pairs), flow pump 97.37 (+5.1%, 3/3), 64 Engram readers 96.64 (+4.3%, 3/3),
unbuffered Engram 85.56 (-7.6%, 0/3), all on 86.84 (-6.3%).
code decode, off 4.51: timer 4.51 (1/3), 64 readers 4.41 (2/3), unbuffered 4.67 (2/3, one pair 3.83), all 4.57.
mmap decode, off 1.95: cold-page prefetch 1.71 (-12%, 0/3).
Cold (no profile) agreed on win_submit and flow pump (+4.5%, +2.7%, 2/2) and showed the timer +12%, readers + unbuffered +22% on decode (2/2, off arm noisy).
Identity: --vram-experts 0, off vs all IDENTICAL (4K prompt, code decode); text equal to off in every warm arm.
Stop during a long prompt (H100, 8K and 16K): STOP answered in 0.12 / 0.16 s, the conversation before the prompt kept, the next request identical.
DSpark cum cut: H100 code 20.13 tok/s off vs 19.60 at 0.5, no gain.
ON: DS4_WIN_SUBMIT, DS4_FLOW_PUMP_WAIT, Stop, STRATA_DS4_LANE_FREE (a no-op on the physical-core lane).
OFF, switch turns it on: DS4_TIMER_MS=1, DS4_ENGRAM_THREADS=64, DS4_EG_DIRECT=1, DS4_IQ_PF_COLD=1, STRATA_DS4_DSPARK_CUM=0.5.
One conflict, docs/DEEPSEEK41.md (both sides edited the three-set paragraph and the file table). Resolution: f-024-rel's text kept (IQ1_M first, the model-card sentence, 533 GB for all three, the 'not a model to pick' Engram row), with the setup worker's sentence that setup recommends 1.75 where 2.00 is tight in memory or disk added to the 1.75 entry.
One conflict, src/dsv41/ds4_model.cu (forward_chunks): f-024-rel has the long-prompt skip (kv_only layers), f-024-hipk added DS4_PROF stage marks around the same lines.
Resolution: the f-024-rel code kept as it is, and hipk's three prof_mark(d, 5/6/7) calls added at the same places (before the q/kv gemms, before the snapshot, after the kv_only return). They are no-ops with DS4_PROF unset.
Same default as Maya #363. For the deepseek41 family only: Service.reasoning_budget() answers 32768 when the request has
no reasoning_budget_tokens / thinking_budget_tokens and the config has no reasoning_budget_tokens at all (a config value,
0 included, wins; a request's 0 turns it off). The existing wrap-up (#123) closes the thinking at the budget, and the
answer-room clamp still cuts it to leave the answer its share of max_tokens. Thinking off and other families unchanged.
Tests in serve/test_dsv41.py: the default, a request's 0 and own value, the config's value and 0, another family, and a
5000-token cap that is wrapped up and answered. serve suite: 1027 tests OK (15 skipped).
/v1/messages and /v1/chat/completions with max_tokens 200-600 and the thinking on spent all of max_tokens on the
thinking: the 0.1.45 answer room starts at max_tokens 1024 and a client that gave no budget of its own got no text block.
For the deepseek41 family only, with no budget from the client or the config, the thinking is now capped at max_tokens / 2
for max_tokens from 48 up to 1023 (the existing wrap-up, </think> and the answer follow in the other half). A client's
own reasoning_budget_tokens / budget_tokens wins, "answer_room": false turns it off, thinking off is unchanged, other
families (Qwen) are untouched. Maya #363 closes the thinking in the engine instead; the server's #123 budget already does
that for every engine, so only the short-cap gap needed closing.
Tests (serve/test_dsv41.py SmallCap, fake engine that thinks without end): max_tokens 200, 400 and 600 on both APIs return a
non-empty text block with the thinking wrapped up at half; thinking off answers directly; a client budget wins; a tool
call under a short cap still parses; another family keeps the old behaviour. serve suite: 1025 tests OK (15 skipped).
Layers 20-39 of DeepSeek-V4.1-Flash read a prompt's earlier tokens only through layer 20's compressed KV and index
keys, and each looks back one 128-token window more than the layer before. So for the answer and for the rings the
decode reads they need their own work only on the last (40 - 20) x 127 + 128 = 2,668 rows. A prompt of ~3,700 or more
new tokens on one GPU now runs its earlier chunks through layers 0-19 plus layer 20's kv_only part (the hyper-connection
pre-norm, the compressor latent, the index keys, the compressed KV) and nothing of the later layers' attention or
experts; the chunk the cut falls in runs layers 20-39 from the cut row on; later chunks run every layer. The expert flow
plans nothing for the skipped (chunk, layer)s. STRATA_DS4_PROMPT_SKIP: 1 (default) the skip, 2 only the last 128 rows
(DeepSeek's own serving; answers shift a little), 0 off. Same approach as Maya #318.
H100 emulation (64 GB cgroup, 12 GiB VRAM, store 41 GB, 3.6 GB/s SSD cap): 16K prompt 69.3 s -> 51.5 s (236 -> 318
tok/s, two alternating pairs), 4K 32.5 s -> 30.7 s; 800 teacher-forced positions after a 4,200-token skipped prompt: KL
0.00029 against the full work, top-1 against the text 88.11% against 87.98%, ppl 1.5110 against 1.5111.
--score gets DS4_SCORE_PREFIX=N (the segments joined, the first N tokens read as one prompt with the last row's logits,
the rest scored) so the skip can be scored; tools/dsv41/test_prompt_skip.py runs it against a pack when one is named.
The Maya notice file now says later Maya changes are covered by the same notice.
A prompt that does not continue the conversation in the model first sets that one aside (position, window rings,
compressor partial groups, each KV source's compressed KV and index keys, its checkpoints), then takes back the kept
conversation it does continue, in RAM or from the slot folder, also across restarts. The latest checkpoint the prompt
still agrees with is used (end of the last prompt, before it, the anchor of the first prompt of a run, and the shared
start the server marks with pin=N). The .meta format and the prefix matching are in ds4_slotmeta.hpp with a GPU-free
test (ds4_slot_test). STRATA_DS4_REUSE=0 reads every request from token 0 (reference for tests). Off unless
STRATA_DS4_SLOT_KEEP=1.
f-dsv41-rel was based on the 0.2.2-era tree (merge-base 1f727f4b5), so the merge brings it onto 7ddc17a38 without dropping 0.2.3 / 0.2.3.1.
Conflict resolutions (0.2.3.1's behaviour kept everywhere that is not DeepSeek):
- packaging/package_engine.py: build() keeps 0.2.3.1's check_reader / allow_stale_vision / allow_no_reader and adds require_dsv41; both the
picture-reader check and the strata-dsv41 presence check run; the CLI call passes both sets of arguments.
- packaging/ENGINE-COMPONENT.md: 0.2.3.1's cuda13 / cuda12 rows (vision built per variant), with strata-dsv41.exe added to the cuda13 contents.
- serve/server.py: the parser is self.parser_cls (OutputParser, or the DeepSeek DSML parser) and keeps 0.2.3.1's declared_only argument.
- strata_cli/launch/cmd.py: the --plan output prints 0.2.3.1's drafting line and then the DeepSeek placement lines.
- strata_cli/launch/runner.py: plan() returns 0.2.3.1's dict with "drafting", plus "placement" for multi-file (DeepSeek) models;
drafting_plan_line() kept; dsv41_engine() and placement() added.
T tokens in tiles of 16 and rows in groups of 8 (activation slice and accumulators in L1): 7.7-9 ns per block and token on IQ2/IQ3 rows against
28-60 for ggml's dot (x3.6-6.6), IQ1_M 16 ns (x1.8); bit-identical to ggml (ds4_gg_bench --exact at 7, 8, 16, 33, 64 tokens, all six types).
gg_cpu_gate_up takes up to 64 tokens a call (and any number in pieces); ds4_gg_bench --expert times the whole expert pipeline.
The layer-major chunked prompt path only ran with --threads on / --tp > 1. --prefill-chunk N (N > 0) now enables it on one card too (kChunkedPromptDefault stays false until the decode gate is signed off). A chunk's routing bumps the layers' heat and CPU-lane hits, and cache_settle() runs the swap rule in rounds after the prompt so the decode that follows finds the hot experts in VRAM (8K prompt: 1699 swaps, 5.6 s). If the chunk buffers do not fit, the chunk is halved down to 256 rows, then windows of 16 with one line saying so. ds4_main: --window 0 (default) feeds a whole prompt when the chunk path is on.
The rows are ggml-cpu's vec_dot bit for bit at any token count, so the default cannot change an answer; a one-time per-type check against the
ggml-cpu build in use turns them off if they ever differ (DS4_GG_IQ256=0 never, =1 always). EX rows: 5 and 6 tokens in one decode pass
(were 4+rest), independent integer chains for 1-2 tokens, IQ1_M: delta dot from per-call pair sums of the activation, group signs from one
shifted load. The main engine's iq256_rows hash is unchlanged (phaseA-tests/cpul/hash_iq256.cpp).
The main engine's iq256_* rows keep their float order (EX=false, the default; hash of their outputs on all types and 1..8 tokens is
identical before/after, phaseA-tests/cpul/hash_iq256.cpp). The new iq256x_rows (EX=true) sums as ggml-cpu does - block scale f16(d)*y.d, the
format constant after the sum, ggml's hsum - so a token's dot no longer depends on how many tokens share the pass (the greedy ids of a
DSpark run changed with DS4_GG_IQ256=1 because the old order differed in the last bits). IQ1_M (29), single-token in ggml, gets a
multi-token AVX2 row. ds4_gg_bench --exact: 0 differing bits for IQ1_M, IQ2_XXS/XS/S, IQ3_XXS/S at 1..8 tokens.
Conflict decisions (win side = f-dsv41-win, port side = f-dsv41-port):
- ds4_iq.cu: kept port. Its IQ1_S/IQ1_M/IQ4_NL kernels are verified bit-exact on HIP gfx1201, sm_86 and sm_120; win's
IQ1_M table load and supported() list are a subset (IQ1_S, IQ4_NL added).
- ds4_kernels.cu/.cuh: kept port's engram_dequant fmt int (0 MXFP8, 1 Q6_K, 2 IQ4_NL) and its iq4nl_value, replacing
win's q6k flag and iq4nl_value; same arithmetic.
- ds4_model.cu: kept port's eg_fmt vector and its Engram type check (unknown type exits 2 instead of segfaulting);
win's eg_q6k is gone, the duplicate IQ4_NL row branch from the textual merge is removed.
- ds4_packinfo.cpp: kept port (device types derived from iq::supported()).
- iq_expert_test.cpp: kept port (hip_or_cuda.h); win's tests/dsv41/hip_as_cuda.h removed (nothing else used it).
The IQ1_M test-d placement is the same bytes in both versions.
- CMakeLists.txt: kept port's test-language block (HIP, CUDA or CXX for the CPU-only build); win's
_strata_dsv41_test_lang variable dropped.
- pack_gguf.py: kept port's type table (IQ1_S/IQ1_M comment) and combined both missing-Engram checks
(has_part from win's split Engram layout AND port's dense-override names).
Deriving their engram.gate from their k*q and transposing their wkv scored 0.409 then 2.996 KL - both my arithmetic, not
their data (their k/q/wkv are stored transposed and the gate is not k*q in the engine's layout). --gate-kv-from <pack>
copies those two tensors from a pack that is known good, so a pack can carry their ple table and nothing else of theirs
differ. That is how the comparison the plan asks for was finally made (numbers in EACH-PC-PLAN/R9700-BOX.md, step 2):
their 110.6 GB IQ4_NL table scores KL 0.16787 / ppl 3.627 against 0.16502 / 3.588 for our 203 GB MXFP8 one - +0.003 KL,
inside this engine's run-to-run placement noise - while saving 92 GB of disk.
Also fixes the fold's reads: Gguf's tensor offsets are relative to the shard's data section (pack_gguf adds data_start),
so the fold must add it too; without that every tensor came out as NaN.
tests/dsv41/{iq,tq}_expert_test.cpp included <hip/hip_runtime.h> unconditionally, which no CUDA toolkit ships - every CUDA
box had to pass a hand-written shim on -I (found on l620 and z440, 2026-10-23, and reported as the one thing worth
upstreaming). include/strata/dsv41/hip_or_cuda.h now provides the handful of HIP names those tests use on a HIP build and
maps them onto the CUDA runtime otherwise (hipMalloc/Free/Memcpy, device sync, the error type and the events).
Verified on r9700 that the HIP build is untouched: incremental build rc=0, ds4_iq_test --type 29 ALL OK, ds4_tq_test ALL OK.
Their engram_k/q/wkv are transposed against the engine's layout (wkv [6144, 25600] against [25600, 6144], k/q [5120, 4]
against a [4, 5120] gate), so the fold now transposes and writes the engine's shape. The pack builds (their IQ4_NL table,
all files present) and runs without crashing, but it scores KL 0.40915 - and that is this tool's bug, not their data: a
direct read of their BF16 engram_k through Gguf's offsets already comes out as NaN, while the engine reads the same tensors
through a pack fine, so the fault is in how this code turns an offset into bytes (the Q5_K dequant inherits it).
Do not quote 0.409 as the IQ4_NL-vs-MXFP8 comparison; that measurement is still open. Next step is to read the tensors the
way pack_gguf writes them into tensors.txt and check one row against the engine's own dump.
Their sets keep the Engram as three per-layer tensors (blk.N.engram_k/q/wkv) while the engine wants engram.gate (k*q
folded) and engram.wkv; their engram-IQ4_NL.gguf carries only the ple table, so a pack built around their Engram stopped at
'blk.1.engram_kv.weight: missing'. engram_fold.py folds the gate and renames the kv into a dense_requant-style folder, and
pack_gguf now treats a tensor a --dense-override supplies as present (it checked the quant's shards and the Engram set only).
Result: packs/ista_2.00_native - 48784 tensors, their IQ4_NL table, Engram layers 1,14, 2896 Engram reads for a 32-token
prompt, no crash.
NOT yet a measure of their table: it scored KL 0.40915 / ppl 4.769 against 0.16502 / 3.588 for the same set with our MXFP8
table, because their engram_k/q/wkv are stored TRANSPOSED relative to the engine's layout (their engram_wkv is [6144, 25600]
against [25600, 6144]; their k/q [5120, 4] against the [4, 5120] gate). Copying the bytes as they are yields a valid pack
with the wrong data. engram_fold.py still needs the transpose (gate in float; the Q5_K kv dequantised, transposed, written
BF16) - Phase 1.1's 'load their layout as published'.
It was a hand-kept copy of the supported() switch and fell behind when IQ1_S, IQ1_M and IQ4_NL got kernels: the 2.00 set's
60 IQ1_M expert tensors (59.5 GB) were reported as 'no GPU kernel ... the CPU lane takes them' while the kernels were in the
same binary and ds4_iq_test passed them. That is exactly the line an operator trusts before a download, so it now walks
ids 0..63, asks iq::name()/iq::supported(), and adds MXFP4 and the trellis types by hand (they are not in that switch).
Verified: packs/ista_2.00bpw_9e2528f and packs/h225 both report 'no GPU kernel: none'. Also fixes the intake's eval call
(session16.sh takes the pack directory name, not a path - the 2.00 score had failed with an empty runs/s16_<path>.tsv).
Found and fixed on p620, the only box with their engram-IQ4_NL.gguf (110.6 GB). Before this, a pack built around their
file loaded for 38 s and then died in Model::Impl::eg_issue at the first token (3 of 3 runs, addr2line 0x6d3450).
- Model::Impl::load: one guard line and exit(2) for an Engram tensor whose type the reader does not handle, naming the
layer and the type (verified with a pack retyped to IQ4_XS, rc 2; a missing embed tensor also exits 2). The guard stays
for the next unknown type.
- eg_q6k becomes eg_fmt per layer: 0 MXFP8 ([scale, 32 codes] blocks, 264 B a row), 1 ggml Q6_K (210 B a 256), 2 ggml
IQ4_NL (18 B a 32: an fp16 d and 16 nibble bytes), 3 an old pack's split FP8. The load loop sniffs the type from the
pack's tensor table, eg_issue builds the row descriptor from it (144 B a row, off + id*144, no scale tensor), and
EgReader::read() needed no new branch: its contiguous, scale-free path already served Q6_K.
- ds4_kernels.cu: engram_dequant_kernel and engram_dequant_tok_kernel take the format and add the IQ4_NL branch, a literal
port of ggml's dequantize_row_iq4_nl (fp16 d per 18-byte block, low nibbles 0-15, high 16-31, kvalues_iq4nl).
- The Engram row slot stays cols*(hd+hd/32) for every format, so existing packs are byte-identical: --pack-info on the
MXFP8 h225 pack and on a Q6_K zero pack is unchanged, and a Q6_K pack's 32-token ids are identical old against new.
Verified with their real table: the pack that segfaulted runs 32 tokens in 1692-1711 ms (18.1-18.3 tok/s) and produces
coherent Python (GGML_TYPE_Q2_0 = 42, GGML_TYPE_NAMES: dict[int, str]); 64 tokens on the code prompt, DSpark off 17.16
and on 23.36 tok/s (44 of 53 drafts accepted). IQ4_NL decode is bit-exact against ggml on 8 of 8 sampled rows, and the row
addresses match the pack's tensor table (data_start 320, 144 B a row).
Not covered: no non-zero MXFP8 table existed on the box to re-check that path in-engine (the Q6_K and synthetic-zero MXFP8
packs were), there is no in-engine float dump against the Python reference (the proof is formula- and address-level), and
the legacy split-FP8 format (3) is kept but untested.
1. CMakeLists: -DSTRATA_BUILD_DSV41=ON was silently dropped when no GPU backend was on, because the whole DSV41 block
sits inside if(STRATA_ENABLE_CUDA OR STRATA_ENABLE_HIP). Found on p1004 (2026-10-23), whose P100 is not a DeepSeek
target but which is exactly the box the CPU-lane A/B belongs on. A new branch builds ds4_gg_bench with no GPU
runtime at all. Verified on r9700 with both backends off: configure rc=0, build rc=0, the benchmark runs IQ3_XXS at
60.5 GB/s (ggml-cpu) against 64.1 (AVX2 rows, x1.06) and 8.02e-09 against a double reference.
2. tools/dsv41/ista_intake.py: the pack step now uses the recipe that actually builds a loadable pack for these sets -
--engram-meta (the UD set's first shard carries the Engram hash parameters, without it pack_gguf sees no Engram layers
and refuses per_layer_token_embd), --mmproj, the combined --skip-tensors regex, and the DSpark noise token and markov
rank (pack_gguf fills the rest of the geometry from the drafter but not those two, so a pack came out with
dspark_noise_token_id 0). Verified end to end on the 2.25 set: the intake rebuilds packs/h225's content exactly -
same 48794 tensor lines, --pack-info 'files: 10, all present', 'no GPU kernel: none', dspark geometry 5/128799/256/128/3.
Found and implemented on z440's CUDA bring-up, applied here and re-verified on HIP.
ds4_iq.cu: decode<T>/deq8<T> for kIQ1_S, kIQ4_NL, kIQ1_M, plus iq1_codes() (iq1s_grid_gpu's 4-bit nibbles -> the int8 codes
8*(n-1)+-1 through two __vsub4) and iq1m_d() (IQ1_M carries its F16 d in the top nibbles of its four u16 scales - there is no d
field). kQK<T> is 32 for IQ4_NL as well as MXFP4; load_tab uses iq1s_grid_gpu / kvalues_iq4nl; DS4_IQ_SWITCH and
supported() cover the three types, so they leave the CPU lane and join the device path.
tests/dsv41/iq_expert_test.cpp: make_mat now writes IQ1_M's scale where the format keeps it (bytes 49/51/53/55, high nibbles).
Without that the derived d is random and roughly 1/32 of the blocks come out NaN, which std::max then hides.
Parity (z440, one A4000, ds4_iq_test --type 19,20,29): token gate_up 6.70e-08 / 2.04e-08 / 5.97e-08, token down 3.62e-08 /
4.71e-08 / 3.71e-08 against ggml's to_float and a double reference; CPU lane 3.4e-8..8.2e-8; F16 prompt tiles inside their
own tolerances. Bandwidth: IQ4_NL 376 GB/s (84% of the A4000's 448, bandwidth-bound), IQ1_S 214, IQ1_M 202 (both
compute-bound at 0.92-1.10e12 weight-values/s against IQ2_XXS's 1.22e12).
Not covered: no end-to-end run with a pack that uses these expert types (none on z440); a combined-seed run put IQ1_M's
light-path gate_up at 1.10e-2 against its 1e-2 tolerance, which is the int8 activation rounding of that path (it does not
move with tensor cores off), not the dequant.
1. ds4_tq.cu:262 used the two-argument __shfl_xor, which is HIP-only; the rest of the tree already uses
__shfl_xor_sync(0xffffffffu, v, lane), which both backends have.
2. tests/dsv41/{tq,iq}_expert_test.cpp were forced to LANGUAGE HIP, so a CUDA configure failed at generate time
('The language HIP was requested for compilation but was not enabled'). The language now follows the enabled
backend: HIP, CUDA, else CXX.
Verified on r9700 that the HIP build is untouched: incremental rebuild rc=0, ds4_iq_test --type 17 ALL OK, the engine
--pack-info clean. Verified on z440 (CUDA 12.9, sm_86) by the bring-up agent: HEAD builds, ds4_iq_test ALL OK for
IQ2_XXS/XXS/IQ2_XS/IQ2_S/IQ3_XXS/IQ3_S/IQ4_XS at 5e-8 against ggml, 272-369 GB/s on one A4000. IQ1_M, IQ1_S and
IQ4_NL still have no CUDA kernel (CPU lane only) - that is the Phase 5 kernel list.
X01: TestStartFailed has one text builder (TestStartFailed.plain): what did not work in plain words, that nothing was installed and the current engine is unchanged, and one next step (`strata update --force`, or a plain retry when the card was busy). The engine's own words stay in .reason, the state file and the JSON. The app's message matcher also accepts the new wording. Hints that a caller passes, such as the one for a busy launch, are used as they are, so the launch-holds-the-card line of f-023-tui1 composes with it.
#18: `components rollback` when an earlier rollback is still in force says the engine is already the previous one and what to run, instead of "0.1.45.3 -> 0.1.45.3".
#15 (K05): max_tokens, max_completion_tokens and max_output_tokens below 1 are a 400 naming the field (they used to mean "no cap"); an unknown model id is still answered by the loaded model, now documented in DETAILS.md.
#11: the refusal of a feed older than the one already accepted now says both sequence numbers, the folder that holds the newer one (.updates) and one next step (use the newest release folder, or delete state.json to take the older one). install.ps1 and install.sh print the whole check output, not its first 6 lines, and no longer blame a missing signature when the check gave a reason.
#12: tar.exe got the path through the ANSI/OEM code page and failed on accented letters. The installer now runs Windows' own bsdtar (System32 first, so a GNU tar from Git for Windows cannot be picked up for a zip) with ASCII relative arguments from inside the data folder and renames the result, so no non-ASCII path ever crosses that boundary. Tested on C:\...\<U-umlaut>n<i-umlaut> test with a stub release folder; the old script fails there with "could not chdir".
plan_import now returns missing_shards (the file names of the split set that are not in the folder). problem_words and plain_issues say "<name>-00002-of-00002.gguf is missing" instead of "some parts of the model are missing"; more than two are counted. The three call sites (models import, strata launch --model-path, the Model screen's own-files field) pass the list.
#6 (A07/B03): a model that is not installed now counts the draft layer (5.2 GB) and the picture reader (0.9 GB) in the one place (modelstate.assess), so the Folder step, the confirm and --plan agree (Gyro-S 58.5 + 5.2 + 0.9 = 64.6 GB). Gyro joins the families that get the helpers. --mtp off and --drafting off drop the draft layer from the count and from what setup is told. setup.py's disk check counts the draft layer it is about to fetch.
B03: --plan keeps "engine_missing" for no engine at all; an installed engine that fails the check says so in plain words and adds engine_usable.
B10/E04: with --drafting off (or an installed config that says off) the written run config carries "mtp": false and no --mtp / --spec-min-p arguments.
The Terminals before 0.2.1.3 (0.2.0-beta, 0.2.1, 0.2.1.1, 0.2.1.2) install a newer Control and report "updated", but their launcher has no hand-over, so the old Control keeps running. Their updater test-starts the staged Control with --version (STRATA_NO_ACQUIRE=1, no STRATA_HANDOVER, from the .s staging folder) and shows the first 120 characters of the answer when that fails. Control now answers that start with one plain sentence and exit 7, so the old updater keeps the old Control and prints the sentence instead of "updated". Every updater that can hand over sets STRATA_HANDOVER and is not touched. Release note added.
Cargo.toml and Cargo.lock, desktop and host package.json and lock, PRODUCT_VERSION, CMake project and Metal STRATA_VERSION, build_hip_linux PKGVER default, About comments, tests that pin the number, Docker image tags and docs (0.2.3-<flavor>), download names in the READMEs, AMD_HIP.md and UPDATES.md, AGENTS.md, the bug report placeholder. The engine moves too: it shares the CMake number and its source changed since 0.2.2 (DFlash2, verify and prefill paths). A new test checks every place agrees and that a 0.2.2 install (Control 0.1.47) is offered Control 0.1.48.
The release runbook promises the pack/requant builder checks free space before it writes (DEEPSEEK-RELEASE.MD 9.6 item 5);
it did not. --dry-run now prints 'space: N GB planned, M GB free on <fs>' and returns 3 when 98% of the free space does
not cover the plan, so 'requant_dry && requant' cannot start a run that will fill the disk; a real run raises instead.
With --resume only the bytes still missing are counted against the free space.
Verified on the box with two recipes that do not fit /data today: rq3 (224.3 GB planned against 128.6 GB free) and rq_gu3
(251.2 GB against 128.6 GB) both refuse with the numbers printed, rc 3 in dry-run. pack_gguf's own output is a few MB
(tensors.txt, engram.bin, tokenizer) and needs no guard.
The rule from DEEPSEEK-RELEASE.MD 6.5.2 - every ggml type works, a tensor whose type has no GPU kernel stays on the CPU
lane with one warning instead of stopping the load - is now implemented for IQ1_S (19), IQ1_M (29) and IQ4_NL (20):
- iq::known(t) (a layout exists: block_values/block_bytes/name) is separated from iq::supported(t) (a device kernel
exists). type_row_bytes/type_block use known(), so a pack lays out such a tensor instead of refusing it;
gg::type_on_device(t) is what the VRAM tier tests.
- the CPU lane takes them: gg_cpu_type() covers 10-14, 16-23 and 29 (ggml-cpu's row dots).
- a layer whose experts are such a type is flagged cpu_only at load, printed once ('no GPU kernel for it, its experts
stay on the CPU lane') and excluded from the VRAM budget in place_experts().
- iq::block_values(IQ4_NL) = 32 (QK4_NL): the only block here that is not 256 values. Without it the CPU lane read
IQ4_NL at the wrong row stride (2.4e-02 and 1.4e+05 error against the reference).
Verified: ds4_iq_test --type 29,19,20 runs the CPU lane against ggml's own decode and a double reference - IQ1_M
1.6e-08 / 4.6e-08, IQ1_S 5.4e-08 / 4.0e-08, IQ4_NL 3.1e-08 / 3.2e-08, ALL OK; the device types still pass; --pack-info
reports the layer and role for each such tensor. A full model with real IQ1_M weights has not been run (none on the
box): the arithmetic of the lane is what is verified.
Owner decision 2026-10-22. setup_intel no longer switches pictures off for a B-series card; it prints the line instead (known risk: the first-picture xe reset on the B70, 2026-10-19). strata launch gets the same line as a warn-level note (Computer.new_card_note) and its picture default for an Intel engine now follows the CPU-reader rule at any VRAM size (it used the GPU reader for a 32 GB B70). Gyro adoption on Arc was already allowed in this tree (adopt.FAMILIES). Tests that pinned the opt-in are updated; docs/INTEL.md, packaging/README.md and docs/GYRO.md say B-series is supported and untested by us.
Same failure on v0.2.2 (011c9e600): the failed new start is refused for its port as intended, the old config is relaunched, but it was still loading when the 5 s startup wait ended, so rolled_back was False and the left-over process held the temp folder at teardown.
Today a pack built by another tool had dspark_block_size 0: DSpark ran one token a round and was worth +0.8% instead of
+28-77%. The report now shows block_size / noise_token_id / markov_rank / the drafter's expert counts, the mtp tensor
count, and the Engram layer ids, with a <- marker when the block size is 0 or there is no drafter. Verified on h225,
c0q3hQ6_K, c0q2d5 and mxfp4 (all: block 5, noise 128799, rank 256, 1242 mtp tensors, engram layers 1,14).
The pack is pointers: ds4.txt + tensors.txt + the weights where model_dir says. Today /home/niko/dsv41-c0/t3 was empty
and the c0q3 pack (all IQ3_XXS) described itself perfectly while being unable to load. --pack-info now stats every file,
compares the length with the largest tensor end, prints MISSING/SHORT lines and exits 2 when any is bad; a healthy pack
prints 'files: N, all present and long enough'. Verified: h225 10/10, c0q3hQ6_K 19/19, c0q2d5 12/12, c0q3 fails with
six MISSING lines.
The IST-DAS 2.25bpw GGUF differs from ours in five ways that stopped pack_gguf, each found by building its pack:
- geometry(): ALIASES for the keys they spell differently (kv_source_layer_ids, index_source_layer_ids, the indexer's
candidate_*, dspark_target_layer_ids, engram_layer_ids). Without the source ids the engine refused the pack
('layer 2: compress ratio 2 without a KV source of that ratio before it').
- --engram-meta FILE: their GGUF carries no Engram hash parameters (no ple.*/engram.layers, no token map, no
multipliers/primes/offsets - those are tensors of integer type in their file); the parameters come from a GGUF of the
same model that still has them, the tables from --engram.
- integer tensors (I8/I16/I32/I64/F64) are left out with a line instead of stopping the build (engram_token_map.weight,
engram_primes.weight, ...).
- --skip-tensors REGEX: their attn_output_a is one interleaved 3-D tensor ([8,1024,4096] ne) that this engine's
o_groups slices cannot take; our Q5_K requant of the same BF16 weights supplies it through --dense-override, which
now also *adds* a tensor the quant leaves out (it could only replace before).
- the DSpark geometry (dspark_block_size / n_routed_experts / num_experts_per_tok / target_layer_ids / markov_rank) is
filled from the drafter's dflash.* keys and the markov tensor's shape, and --dspark-noise-token sets the model's noise
token (128799). Without it d_block stayed 0 and DSpark ran one token a round: h225 measured 16.77 tok/s off vs
25.93 on (3.16 tokens a round) after the fix, 1.01 before.
- The Models screen gets one more column in the context row, the existing Select, with the four choices; DFlash2 is offered for Qwen3.8-Flash-Next
(the qwen and gyro families) only. The choice is saved with the model (a setting the app owns, like Draft layer) and shown again next time.
- Before a load the host asks Control (strata drafting plan / fetch --if-needed): when the choice would use the DFlash2 drafter and its file is not
here, it is downloaded pinned and checked and packed, with plain progress in the load. A drafter that cannot be had never stops the load: the MTP
starts and the job says so. The download plan dialog lists the drafter's size when it is chosen.
- What ran is told in the engine's words under Details and below the row: "Drafting: MTP (DFlash2 didn't fit)" and the other three reasons, all in
the eight languages. Off is the draft layer's own switch.
- The app never reads the suggestion table: Control decides (strata_control/drafting.py), so the Terminal and the app cannot disagree.
Logic and tests ported from #3578 by medking82. The alert stays red and in place; only its words change for the restart interruption of an update that a later update of the same component finished. Sentence-case text in all eight languages; the PR's screenshots and validation files are left out.
Co-authored-by: medking82 <[email protected]>
The overflow was found and reported in #3601 by G0-0000. Its 75% column made the inputs narrower at every width, so this keeps the current layout when it fits: the label column may shrink, long names wrap, the input keeps 360 px.
Co-authored-by: G0-0000 <[email protected]>
- strata_control/drafting.py is the one place: the choice (saved in the model's run config as "drafting"), the table card class -> suggested
drafter with the measured evidence (Auto picks DFlash2 only where it was faster on every tested prompt; today no class qualifies, so Auto is
the MTP everywhere), the pinned drafter (incoai/Qwen3.8-Flash-Next-DFlash2 rev a22a1744, sizes and SHA-256 of the repository), what each choice
puts on the engine arguments, and the plain "Drafting: MTP (DFlash2 didn't fit)" words read from the engine's own log.
- serve/server.py applies the choice at every start and reports "drafting" / "drafting_note" in /metrics.
- strata launch --drafting writes it to the launch config and presets, shows it in --plan, adds the drafter to the download plan when it would
be used, fetches it before the start (a failure starts the MTP and says so), and prints the engine's fallback line.
- strata drafting plan|fetch (JSON/JSONL for the app). tools/dflash2_pack.py now reads safetensors with numpy alone (no torch on the person's
PC), byte for byte the same file as before (checked against the earlier version), and ships in the Terminal bundle.
- DFlash2 is for Qwen3.8-Flash-Next (qwen, gyro) only. The engine in this release runs it only in its CLI run, not with --serve, so while
drafting.ENGINE_SERVES_DFLASH2 is False the choice starts the MTP, says "DFlash2 isn't available in this mode yet" and downloads nothing.
strata-dsv41 --pack-info --pack <dir> reads only ds4.txt and tensors.txt: no device, no weights, no lock. It prints the
layer x role type map with an expert's bytes and a token's MB (top-k picks), the type totals, all-experts GB at the mean
bpw, the dense side split into embed / head / mtp / vision / engram, and every tensor whose type has no device kernel
(IQ1_S, IQ1_M, IQ4_NL today) with the layer and role - the H1 rule's report, and the first line of every benchmark log.
Verified on c0q3 (208.1 GB at 3.06 bpw, 3.251 GB a token - the document's figures) and c0q3d5.
- ds4_iq.cu: IQ2_XS (type 17) through the same kernels as IQ2_XXS: Ty<17> (74 B, 1024-word grid),
decode<17> and deq8<17>, block_bytes/name/dispatch. ds4_iq_test compares the token path, the window
path, the prompt sets and the CPU lane against ggml's own to_float + a double reference: worst
4.6e-08 (token gate_up), 4.6e-08 (down), 8.9e-03 (F16 tiles), CPU lane 3.5e-08. IQ2_XXS / IQ2_S /
IQ3_XXS / IQ3_S / IQ4_XS pass the same test unchanged.
- ds4_model.cu: gg_type_of takes 17; embed.weight may be any ggml block type (Q5_K / Q4_K / Q6_K ...)
through gg_row_to_bf16 (ggml's to_float, the same BF16 rounding as the Q8_0 path) - the IST-DAS sets'
Q5_K token_embd needs no dense override now.
- gg_dot_bench: the A/B's pass criterion was |x-y|/|x| on single outputs, which fails on cancellation
while the two paths agree to a few ulps; it now also compares 64 sampled rows against a double
reference scaled by the L1 norm (worst 1.1e-08) and requires |x-y| <= 1e-6 of the output scale.
A/B at 32 threads: single token x1.03 (IQ2_XXS) / x1.13 (IQ2_S); 4 rows x2.4 / x2.3.
- CMake: ds4_iq_test target; ds4_tq_test and ds4_tq_test_portable link ds4_gg_cpu.cpp (the ggml CPU lane
ds4_iq_cpu.cpp refers to since 49e82263a, missing from those targets).
Python's OpenSSL checks against certifi's roots and does not fetch a missing intermediate; Chrome and curl.exe use the Windows store
(schannel), which also holds the roots antivirus HTTPS scanning, a company proxy or a preview build adds. strata_control/tls.py
install() puts truststore (pinned in requirements.txt and the three runtime locks with hashes, CPython 3.12 in every archive) under
every HTTPS connection of setup, Control's transfer and component download, serve and the fetch tools, with certifi as the fallback.
A certificate failure is no longer the raw urlopen text and is not retried: it names the likely causes (antivirus web scanning, a
company or school proxy), says what to try, and never offers to skip the check.
The host: server/system-ca.mjs adds the system roots to Node's bundled list at start (Node 24.5+), and the agent process gets
NODE_USE_SYSTEM_CA=1.
- /compact was typed into the chat and sent to the model as text (it answered "nothing to compact, this conversation is still short" at 90%
full). The host now runs the Harness's /compact and says how full the chat was and is, measured.
- Before a message goes out the model server counts it (its own tokenizer). Fits: sent. Fits once the older messages are a summary: summarised
first, with a line in the chat. Can never fit (a 103 KB paste is 70K tokens in a 64K window): not sent, and the card offers "Attach as a file"
(saved in the chat folder, the assistant reads it in parts) or "Send a shortened preview".
- A refusal that remains after the Harness's own compact-and-retry is a plain sentence with the engine's text under Details, and the card
offers "Summarize and continue" or "Start a new chat" instead of "Try sending the message again".
- Compaction shows: a status line while it runs and one plain line afterwards. The context meter follows the running turn (the host reports
what the context holds after every model request) so it no longer reads 1% between two tool steps.
- A Harness error "lost host child is still present" reads as "the last Strata session is still closing down".
- All new sentences are in the catalog in the eight languages. The real-engine run is scripts/context-e2e.mjs.
The child marker without a creation time could not tell the recorded engine from another Python or Strata program that had reused the
number, so every load said "lost host child is still present" until the marker was deleted by hand. The child is always started before
its marker is written, so a live process created after the marker's file time (with a two-second slack) is not the child. Unreadable
times still keep refusing; a recorded creation time still decides by itself. New procs.started_epoch reads a creation time on Windows,
Linux and macOS.
The read-only check refused any command containing & ; ( ) { } before it understood quoting, so listing "C:\...\GUI Q&A" showed an
approval card. Quoted text is now set aside first (maskQuotes): an expansion inside double quotes ($ or a backtick), an unterminated
quote, a quote glued to other characters and a quoted text that starts with a dash are still not understood, so those and everything
outside quotes ask as before.
The Harness prices a conversation at 4 characters a token; code, logs and JSON are about twice as dense. A new Harness plugin
(plugins/strata-context.mjs, arithmetic in context-budget.mjs) wraps the token meter so pressure is the last measured prompt plus what
came after, priced at the density that measurement showed. It also wraps the compaction summary request: when it would not fit the
window it is asked for in pieces (a message bigger than a piece is cut for the request, each piece builds on the summary before it, a piece
the server still refuses is halved). A chat the engine refused once (a 70K-token message in a 64K window) is no longer stuck: the next
message compacts and goes through. Tests run the real Harness against a scripted server that counts and refuses like the engine.
recommend() (Terminal, app and shared vectors) now turns a card with 8 GB or less Off only when a bigger usable card is there or it has under 4 GB. The hardware report (tools/strata_mcp.py) applies setup's own opt-in rule for a PC with only Pascal/Volta NVIDIA cards and no usable AMD card, so those cards are no longer flagged 'older than the RTX 20 series' (engine_variant cuda12); the feed already picks the cuda12 package for sm_61 and refuses cuda13. Tests: shared gpu-roles vectors, strata_mcp hardware, feed selection for sm_61, and Tune keeping a two-card role map.
The load used to stop with 'Some model files are missing' and nothing else, so Retry looked the same every time. Control's model list now carries the required parts it cannot find (part, file, status); the host joins them with the installer's missing GGUF files and shards, puts the names in the error (four shown, all in Details and in the load log), and the app shows 'These model files are missing: ...' with 'Press Download & load again to fetch them. Files that are already there are kept.' in all eight languages.
While the callback is pending the browser holds rendering, so the frame callback never ran and every click waited for the browser's own guard: about 4 s in Chromium (and so the WebView2 app window, reproduced in Edge 155: callback 4023 ms) and about 5 s in Firefox. The callback now resolves on the first DOM change after the hash flipped, or after 150 ms. Measured in Edge 155: callback 9-19 ms, full transition about 350 ms, with and without a 3.6 MB chat history.
On an AMD R9700 PC a leftover nvidia-smi made detection say cuda, so the update staged cuda13 and its test start failed. detect_backend (Control) and gpuBackendHere (app) now let the Windows display adapters decide first and ignore a tool for a make with no adapter. The update decision keeps the installed engine's make when the PC has a card of it, and otherwise stages nothing and says so in plain words (status wrong-make, shown by the app). update_engine refuses a package of another make before any download. --backend and STRATA_ENGINE_BACKEND are the explicit way to change make.
The reader was clamped only when the config carried vision.max_tokens; an old, hand-edited or migrated config left it unclamped and
one picture became 93,700 image tokens. Vision.token_limit gives every reader a limit (GPU 1024, CPU 300, the numbers setup writes)
unless the config names one (a value the person set is kept, 0 still means none). strata-vision refuses a picture that comes back
over the limit plus an eighth with a plain ERR instead of passing 90 times the budget on, and prints a WARNING when its own warm-up
picture is over it. The server logs a picture that is more than 4 times the limit with the numbers to send with a report.
shell: true meant cmd.exe on Windows (Get-Content failed), the host's own folder (python tools/foo.py found nothing) and a fixed two
minutes with silently cut output. custom-command-run.mjs: a Windows command that reads as PowerShell runs in pwsh or powershell.exe,
any other in cmd.exe as before, sh elsewhere (or the tool's named shell); the folder is the call's optional cwd, then the tool's cwd,
then the home folder, and a missing folder is a plain refusal; the time is the tool's timeoutSec (1 to 3600, default 120) and ends the
programs the command started; a cut output says so. Settings stores cwd, shell and timeoutSec for a command tool and an edit that
does not send them keeps them. No new field in the Settings screen: that needs a design decision.
A launch command that printed nothing was reported as 'The program, folder or file was opened.', and the system prompt told the model
to say it is open. A browser given a file:// address also returns without an error, and the model then reported that it had previewed
the page. The card now says what is known (the command finished without an error, so the program was started; Strata cannot confirm
that a window or page opened). The prompt (and the plan view step) send local images, SVG, HTML and PDF to the present tool and
forbid claiming a window opened or a page rendered unless a tool confirmed it.
T5: the agent's tool servers came from local[0], the first model loaded, not the model the chat runs on. buildProfile takes
defaultLocalId (the native default instance, agent-bridge option defaultLocalId); the agent's default route follows it too.
T6: the editor and the save check accepted 64 characters while the profile dropped everything over 32 or with a dot, silently.
server/mcp-names.mjs holds the one rule (32, letters, digits, hyphen, underscore; src/lib/mcp-names.ts is its copy, a test
keeps them equal). A name saved before the rule stays saveable. mcpRows reports what it leaves out (name, no program or address,
a name that collapses to another's id); the agent status carries skippedServers and the chat shows one notice per reason before
the turn. The sentences are in all eight languages (src/locales/tools-<lang>.json).
A session whose preset lookup threw ran the call unasked (return next()). It is treated as Manual now: a call that is not provably
read-only asks, a read goes through, and the reason is logged.
Ported from #3556 (Amit Tsur): a declared namespaced tool written with its flat alias (mcp__server__tool for mcp__server.tool) keeps
its schema while streaming and is reported under the declared name; an ambiguous alias is no alias; a quoted incomplete wrapper
(`Format: <tool_call>... </function> and then I explain it.`) stays text; a following call opener separates concatenated calls;
streamed JSON is closed only once the wrapper or the next call confirms the boundary; a call cut by the output limit stays
incomplete. The Responses-only alias map is gone (the parser resolves aliases now).
Reconciled with the 0.2.2 envelope rule: with tools declared, a call for an undeclared name stays text on every route (the PR's
tests that expected a native call for an undeclared name now expect text, or a native call only where no tools were declared and
declared_only is off: the Responses/Codex compaction route keeps that). The aliases attribute replaces the envelope work's _alias
set, so the lenient opener (#3441) and the declared-only gate use the PR's unambiguous alias table.
New rail: a call that named a tool the request did not declare is counted (parser.unread) with the names the model wrote
(parser.unread_names), and the server's existing log line for unread calls now prints them.
Co-authored-by: Amit Tsur <[email protected]>
 used to come out as a link with a stray '!', and a data: URI as literal text. Allowlist (src/lib/markdown-images.ts):
data: PNG/JPEG/WebP/GIF (never SVG), the host's /api/files/raw address, and a scheme-less path read through that address for the
active chat. http(s) pictures are not fetched: they show as a link with the alt text. file:, javascript:, blob:, share paths and
every other scheme show as text. One .md-image rule keeps the picture inside the bubble.
build_cuda_linux.sh builds strata-vision for the same architectures as the engine (cuda12 includes sm_60/61/70) and records
vision_archs in BUILD.json; the Dockerfile records it too. packaging/build_vision_windows.bat does the same for the Windows
cuda12 and cuda13 zips (they shared one reader built before Pascal support, #3669). package_engine.py refuses a CUDA package whose
reader's vision_archs do not cover its archs and a macOS package without strata-vision (#3697); --allow-stale-vision and
--no-picture-reader are the explicit exits; --stamp-vision records a hand-built reader. ENGINE-COMPONENT.md describes the release steps.
A local engine reading a long prompt sends the Harness nothing for minutes, so the 4 minute stall limit ended the turn mid-read. The host now hands the bridge the engine's /metrics progress (live.prompt_read, generated, slots); a moving mark keeps the turn alive for at most 30 minutes after the Harness last spoke. An engine that stops reporting still ends at the usual limit. API routes are unchanged.
Every streamed token stringified every chat and wrote the lot to localStorage, which grew the WebView2 renderer to gigabytes. Writes from the streaming path are now coalesced (1 s), flushed when a turn ends, is cancelled or fails, and on pagehide/hidden. Unchanged chats reuse their JSON by object identity; a field over 200k characters, a chat over 1.5M and a list over 3.5M are shortened in the saved copy only. The stored format is unchanged.
A picture a tool returned wrote to an elastic encoder that was not running (AttributeError, then a 400 that every later
turn repeated). _note_unreadable_tool_images now calls _vision_up and turns a start failure into the usual
[image omitted: why] note; Vision.encode raises a clear ValueError when no reader runs.
vision_start_error no longer returns a blank sentence: last stderr lines, the exit status in words, the seconds of a
timeout, and a kernel-image error (sm_60 on the Windows cuda12 reader, #3669) names the card/architecture and the CPU way out.
No release feed ever carried a linux-x86_64-sycl engine package, because no recipe built one (the 0.1.45 package of stream split-s1 was made by
hand). build_sycl_linux.sh builds the JIT engine with oneAPI, bundles the oneAPI runtime and the Unified Runtime adapters with collect_libs.py,
sets RUNPATH $ORIGIN on every ELF file (Intel's libraries have none or a wrong one), packages it with archs intel-arc and checks the package
with --verify, binary_deps.py and an env -i strata --version. _drm_vendor_backend takes the sysfs root so the Arc detection can be tested with a
fake /sys/class/drm; tests/terminal/test_sycl_package.py covers the choice (an Arc PC with no oneAPI tools gets the package, nobody else does,
Windows gets a reason) and the recipe; the release-matrix test has the sycl package.
The migrated generate.cpp had no --version (the CUDA and HIP engines answer it first thing), and three things in dpct's helper headers
built a SYCL device at program start: blas::descriptor's inline _saved_queue_ptr, the dpct::device_memory constructor (pointer table,
mem_mgr, dev_mgr) and its default-built sycl::queue member. On a PC whose user cannot open the GPU the engine therefore aborted with
'No device of requested type available' before it could print anything, which also failed the engine smoke test of strata update.
The headers are edited and sycl/tools/fixups.py reproduces each edit after a re-migration. A frozen first-token-logits run on an Arc A750
(4095-token prompt, IQ3_XXS) is byte-identical before and after.
F5: a start that did not reach ready names the stage, the cause line from the engine log, a next step (render group on Linux, smaller
context, strata update) and the log path; --no-tui and runs without a keyboard never ask 'Keep or move?' and print what was kept.
F3: a context or picture change forwards the installed cards and Main/Helper roles to setup.
N3: --mtp on|off reaches setup, and setup no longer fetches the draft layer for --mtp off.
N4/N2: --plan counts a picture reader unless one sits beside the files, names the --vision off and --mtp off switches, and counts
the ready-made pack (NVFP4) setup would fetch; setup's disk hint is --data-dir under launch.
N5/N6: STRATA_DATA_DIR/install is looked at first, --plan says when an earlier install is the one in use, and an installed engine
older than the model needs is updated or named.
N8: --plan warns when the file headers say another family or model than the one named.
N9: update --check names the package it selected.
N11: the Arc adopt path takes Gyro and the sentence has no double full stop.
setup.py imports tools/calibrate.py after a tune, but INSTALLER_TOOLS did not list it, so every launch after a tune died with
ModuleNotFoundError and only 'The model could not be installed'. The list now names it, and add_installer also reads the
tools/ modules setup.py imports so the list cannot go stale again (stage-control.py uses the same list).
A new test stages the bundle the way package_terminal.py does and imports setup.py and its tools with tools/ limited to the bundle.
strata launch keeps the last 400 lines of setup's output in launch/<name>.setup.log, shows the Python error line in plain words
when setup crashed without an [X] line, and names the file.
A source checkout's .venv\Scripts\python.exe starts the real interpreter as its child, so the pid in the server's ready file
was one below the pid the runtime host started, the ready file was never matched, no probe was sent and the update ended
with startup_wait_expired. The host now accepts a ready pid within three parents of its child (Windows only).
The test start also shows progress (loading, loaded and waiting for one token) and a wait that runs out says what the engine was
doing, with the kept log, instead of blaming a busy graphics card because the log mentions VRAM.
+8.4% on json_extract at fixed windows (53.55 -> 58.03 tok/s), 94 of 250
output tokens differ from the BF16 read, cold HC read 93.8 -> 20.3 us
(142 -> 352 GB/s). Experimental, opt-in only, never a default.
gr_read launched six kernels per hyper-connection read: norm, down,
gate, mean, inject, write. The inject projection is now the down grid's
last block (one warp per stream, the same lane stride and warp_sumf as
gr_inject_kernel), and gate + mean are one kernel (one block owns WARPS
columns for every stream, the per-stream gated values meet in shared and
are added in the old c order). Both keep the old per-row arithmetic and
reduction order, so lo/lq, mixed and inject are bitwise unchanged; gated
is no longer written to global because nothing outside gr_read reads it.
Six launches per read become four.
This is a no-op for the default IQ3_S path on the Strix Halo: that engine
already takes the fused (staged) hyper-connection read, not gr_read. The
change is kept for the unfused gr_read path (CUDA, other HIP
architectures and --no-fused-gr).
Verified on gfx1151: json_extract at fixed windows (--spec 4
--spec-min-p 0 --suffix-draft 0) gives an identical output line before
and after.
Cargo.toml and Cargo.lock, desktop and host package.json, PRODUCT_VERSION, CMake project and Metal STRATA_VERSION, build_hip_linux PKGVER default, About comment, tests that pin the number, Docker image tags and docs, download names in the READMEs, AMD_HIP.md and UPDATES.md.
iq_kernels.cu is f-022-int's (staged codebook, row chunks and staged activations on cc 6.0; STRATA_TQ_STAGE). On top of it, tq_sub8_on()
(CUDA, not cc 6.0, STRATA_TQ_STAGE / STRATA_TQ_SUB8 not 0) turns on the staged codebook and the staged 8-lane down rows for the other cards
and adds the 8-lane 160-dot gate/up rows (row_dot_160_sub8). RTX 3060, Gyro-M, 24 groups, 1 token: gate/up 142 us (the cc 6.0 path forced:
158, off: 225), down 77 (off: 184); the hashes of the outputs are equal in all three modes.
The reporter's prompts ended in non-finite logits in the window that reads the prompt's last few tokens (after the last turn boundary), every time and on every retry, and passed with --short-read 0. The cause on their machine (a Windows VM, sm_89, a Broadwell AVX2 CPU, IQ3_S) was not reproduced on a P100 with a Broadwell Xeon, an RTX 3060 or a Core Ultra.
The engine now reads the request that follows such a window with --short-read 0 for that one request, so the server's transparent retry succeeds instead of failing the same way. Nothing changes until a prompt window reports non-finite logits. STRATA_NAN_INJECT=1 exercises it.
With the runtime capped to two hardware queues the two primed queues are all of them, and the high-priority draft stream and the draft side stream share them: a 4K prompt on an RX 9060 XT read 681-753 tok/s primed and 1188-1238 unprimed. Primed only when the cap is 3 or more or unset; STRATA_HIP_QUEUE_PRIME=nn still forces it. HIP code only (gfx12, Linux).
GYRO.md gets the measured RTX 3060 numbers of the shared-memory codebook / 8-lane row kernels (Gyro-S 53.6 -> 62.8 tok/s, Gyro-M 50.6 -> 56.1, 5 interleaved pairs, identity gate identical), the release notes one line, and the places that said Gyro-M decodes about 20% slower than Q2_0 now say 16-21% (RTX 3060 12 GB after the kernels: 16%, R9700: 17-21%). The translated READMEs keep the old figure.
packaging/build_cuda_linux.sh is the recipe for the Linux cuda13 and cuda12 packages (cuda12: CUDA 12.9, 60;61;70;75;80;86;89, STRATA_EXPERIMENTAL_SM60=ON, driver 575.51). package_engine.py refuses a cuda12 package without sm_60, sm_61 or sm_70, so the 0.2.1 mistake (sm_75/86/89 only, a P100 container in a restart loop) cannot ship again.
The root Dockerfile takes the CUDA image from CUDA_IMAGE: the CUDA 12 case turns on the Pascal/Volta build when an architecture below 75 is listed, names the package cuda12 with the matching driver, and the terminal stage has git (package_terminal.py calls it).
A <tool_call> whose opener drops the word function (<=NAME>, <function:NAME>, <NAME>) is read as <function=NAME> when NAME is a declared tool.
The answer follows the rule the reasoning already had (#804): a call counts only for a declared tool. Chat Completions and /v1/messages requests that declared no tools get the text back; the Responses route keeps its Codex compaction behaviour.
Blocks that open like a call but do not read as one are counted in the server window.
After #3484 the process-wide STRATA_PF_PAD can be on for a gfx12 stage, which then dequantized every weight padded, found no product and dequantized it
again unpadded before the copy path. Ask strata_pf_padded_gemm_available() for the current device first; same products, same bits.
A 39.8 GB shard came out with the wrong SHA-256 on 0.2.1 and was deleted, 40 GB of download lost.
- every ranged answer must be a 206 with exactly the Content-Range asked for (start, end and total), a fitting
Content-Length and the same ETag as the first answer; a changed file part-way stops the download and drops the .part
- the raw unbuffered write is looped until every byte is down (its return value was ignored)
- a .part records the address and ETag it came from (.part.src); one from another mirror or revision is dropped, not mixed
- a parallel download always has a piece list before the first byte is written; if none can be kept it uses one stream,
so holes in the sparse file are never read as a finished prefix
- the one-stream resume accepts a 206 only at the byte asked for; another range restarts from zero
- a finished file with a pinned hash but no finish mark is checked, not marked; the rename waits for a virus scanner
- a SHA-256 mismatch re-reads the file from the server piece by piece, rewrites only the blocks that differ and checks
again ("Some parts of the file were damaged; downloading only those parts again"); deleted only if still wrong
- update archives (strata_control/transfer.py): a resumed answer is appended only when its Content-Range starts at the byte asked for
- tests: tests/terminal/test_download_integrity.py (a local server that answers 200 for ranges, wrong ranges and totals,
cut bodies, another ETag, 503s, flipped bytes)
Python only (setup.py, strata_control/transfer.py, tests).
A Strix Halo's free VRAM is the GPU pool (carve-out + GTT, nearly all RAM with ttm.pages_limit raised) while the cache copies experts into that
same RAM on top of the arena, so a cache sized from the pool alone could ask for more than the OS had left (OOM killer). The auto size is now
min(GPU free, MemAvailable - max(8 GiB, 8% of RAM)); --expert-cache N is kept as asked, STRATA_UMA_RESERVE_GIB=N sets the reserve (0 = old sizing).
STRATA_PF_PAD=1 STRATA_PF_GEMM=1 lets the two K 6144 producers write row stride 6,208 into the prompt GEMM, but the only
product that reads a padded stride is strata_pf_gemm_f16_ld, which is built into wmma_gemm.cu's gfx11 matrix-core block
and returns false everywhere else. On an RDNA4 card Gemm::native then reached its "a padded X (ldx 6208, K 6144) needs
STRATA_PF_PAD's path" branch and called std::exit(1): under serve/ that is one engine restart per request. Reproduced
on an RX 9070 (gfx1201, ROCm 7.2.4, kernel 7.2.11) at --vram-reserve-mib 700 and 1,536, so it was not the VRAM guard.
- wmma_gemm.cu/.h: strata_pf_padded_gemm_available() answers whether the padded product exists on the current device
(the same gfx11 list the kernels are compiled for; false in a build with no gfx11 target).
- prefill.cpp: pf_pad() asks it before padding, and prints one line when the env asks for a product this card has not.
- gemm.cu/.hpp: a padded X that no kernel took is copied to a contiguous FP16 buffer (one cudaMemcpy2DAsync, one
grow-only buffer per Gemm) and the ordinary paths run - the same products, the same bits. exit(1) remains only for a
failed allocation, with a message that says so.
Checked on the RX 9070, same 1,199-token prompt and 8 greedy tokens in every arm:
- before (0.1.45, pre-fix binary): `prefill gemm: a padded X (ldx 6208, K 6144) needs STRATA_PF_PAD's path`, exit 1.
- after, STRATA_PF_PAD=1 STRATA_PF_GEMM=1: `prefill gemm: STRATA_PF_PAD needs the padded gfx11 product, which this card
does not run; not padding`, prompt and decode complete, exit 0, output identical to the unpadded run.
- after, with the gate forced open so that the producers really pad and Gemm::native takes the copy path: no exit, no
"needs STRATA_PF_PAD's path", output identical to the unpadded run (13 271 248046 198 248045 74455 198 248068).
With the gate in place STRATA_PF_GEMM=1 alone is inert on this card (4K 1,247 against 1,243 tok/s, 8K 1,330 against
1,321, medians of 3 starts).
docs/AMD_HIP.md carries the RDNA4 bullet for it. Built for gfx1201 (RX 9070, ROCm 7.2.4, Release): 338 of 338; ctest
77 of 79, the two failures (ple_parity without the Q2_0 GGUF here, hip_prefill_mmq_parity's APR block reading
ggml_type_name(145) with GGML_TYPE_COUNT 43) fail identically on origin/main. The changed files are shared with the
CUDA build and were not compiled for CUDA here (no nvcc on this machine); SYCL is untouched.
(cherry picked from commit 341625013d)
The #1576 memory guard compared the iGPU's BIOS carve-out with the dGPU's VRAM and could keep the iGPU as the last stage (head, draft layer, verify).
The picture reader inherited the engine's first visible card, the iGPU. setup --gpus all took every usable card including the APU.
All three are recommendations: a number list, gpu_order as_given, a manual layer split and vision.cuda_device are kept as asked.
The int8 codebook lookups of tq_load went through the global table: a warp's 32 random 2-byte reads touched ~20 L1 sectors each
(native_expert_bench, ncu: 51M sectors for 2.8M requests, L1 64% busy). The grouped gate/up and down kernels now stage the 4 KB
codebook per block (stage_iq_grid, STRATA_IQ_STAGE_GRID=0 reads it in place) and take 4 rows per warp: gate/up K=160 (new
row_dot_160_sub8) and the K=40 down (row_dot_40_sub8, as IQ4_NL), 32 rows per block so the staging is amortised and no lane idles.
The sums keep row_dot_multi's tree (lane sums of t, t+8, t+16, t+24, then the same xor steps), so the outputs are bit for bit the
old ones: native_expert_bench prints an FNV hash of the outputs now, equal for base and new on Gyro-S, Gyro-M and Q2_0 blobs.
RTX 3060, 24 groups, 1 token: Gyro-M gate/up 221 -> 140 us, down 182 -> 86 us; Gyro-S 228 -> 157 and 173 -> 89.
strata_control/vram_fit.py estimates the VRAM left for the expert cache (calibrated on 12 A750 starts) and walks the ladder 32K -> 16K -> 8K, prompt chunk 1024, reserve down to 100 MiB. Used by sycl/setup_intel.py (setup), strata launch's recommended context and the adopt path. A config that already fits is returned unchanged; values the user chose are kept; it never refuses.
The start-up line and the --pool-cores check used the layout as seen by whichever thread asked, and on Linux a thread that was
already pinned (the host, after the session pins it) sees only its own CPU. The layout is now read in the constructor.
- setup.py: the nine pinned shards of unsloth/DeepSeek-V4.1-Flash-1017-GGUF UD-Q8_K_XL (revision f924b94f, sizes and SHA-256 from the reference run; the DSpark drafter and picture reader pinned but not fetched), MULTI_FILE_FAMILIES, install_dsv41: plain warnings for the 508 GB download, RAM and disk (a refusal only where the disk cannot hold the files), the strata-dsv41 check before any download, --gguf-dir for files you already have, pack_gguf.py, config with --pack --max-context 8192 --eos 1 --tp 2 and the cards; menu entry and a --check line.
- strata_control/dsv41_fit.py: where the model goes on the cards and in RAM, from the measured run (6,240 experts in graphics memory predicted on 4 x 32 GB, 6,197 measured), the n-gram table's SSD note.
- tools/strata_mcp.py and strata_cli/launch: the model is in the list; strata launch --plan prints the placement (layer blocks per card, experts on the cards and on the CPU, RAM, disk, the SSD note); --model-path takes the nine shards where they are; parallel, pictures and tuning are said, not passed to an engine that does not take them.
- tests: tools/test_setup_dsv41.py (22), tests/terminal/test_launch_dsv41.py (13).
thread_name.hpp pulled <windows.h> (and WIN32_LEAN_AND_MEAN) into every file that includes aux_cpus.hpp; on Windows the function is now
defined in pool.cpp, which already includes it. The PLE reader and the direct file readers are not named (they do not link the pool).
Both pass --pool-cores to the engine (experimental, off by default). A list that cannot be read is said and left out.
strata launch also shows the notices that the flags themselves produce (they were dropped before).
The start-up now has one plain line: CPU workers on logical processors 2,4,...,30 (15 workers, one per physical core; host on 0).
It says so when two workers share a core, when the host's core is taken, and when the OS did not say which processors are
siblings. --pool-cores LIST / STRATA_POOL_CORES pins the workers to exactly the listed logical processors (off by default;
siblings, the host's processor and processors the process may not use are warned about, never refused). The default placement
is unchanged. The pool workers, the host thread and the long-lived helper threads get names (strata-pool-N, strata-host,
strata-stage, strata-adapt, ...), so ps -L, /proc and Windows thread tools show which is which. Windows without core
information no longer counts every logical processor as a core when it picks the default worker count.
- tools/strata_tokenizer.py: the deepseek-v3 pre-tokenizer (llama.cpp's DEEPSEEK3_LLM list as a sequence of splits) chosen by the pack's tokenizer.json 'pre'; special tokens decode to their literal text (the DSML token is not in the byte alphabet). 20 of 20 reference prompts tokenize to the ids llama.cpp made.
- serve/dsml.py: DSML calls (the GGUF template's spelling) translated as they stream into the form OutputParser reads, so tool streaming, tool_choice and calls in the thinking work as for the other families; string=true values stay strings.
- serve/server.py: Service.set_family('deepseek41'): end token from the pack, DsmlOutputParser, forced-call opening in DSML, effort words low/medium/high/max/1..100, model name and card sampling.
- serve/test_dsv41.py (39 tests, fake engine) and fixtures from the GGUF's own template and the llama.cpp reference prompts.
A 39.8 GB shard came out with the wrong SHA-256 on 0.2.1 and was deleted, 40 GB of download lost.
- every ranged answer must be a 206 with exactly the Content-Range asked for (start, end and total), a fitting
Content-Length and the same ETag as the first answer; a changed file part-way stops the download and drops the .part
- the raw unbuffered write is looped until every byte is down (its return value was ignored)
- a .part records the address and ETag it came from (.part.src); one from another mirror or revision is dropped, not mixed
- a parallel download always has a piece list before the first byte is written; if none can be kept it uses one stream,
so holes in the sparse file are never read as a finished prefix
- the one-stream resume accepts a 206 only at the byte asked for; another range restarts from zero
- a finished file with a pinned hash but no finish mark is checked, not marked; the rename waits for a virus scanner
- a SHA-256 mismatch re-reads the file from the server piece by piece, rewrites only the blocks that differ and checks
again ("Some parts of the file were damaged; downloading only those parts again"); deleted only if still wrong
- update archives (strata_control/transfer.py): a resumed answer is appended only when its Content-Range starts at the byte asked for
- tests: tests/terminal/test_download_integrity.py (a local server that answers 200 for ranges, wrong ranges and totals,
cut bodies, another ETag, 503s, flipped bytes)
Python only (setup.py, strata_control/transfer.py, tests).
- strata launch with no --gpu replaced the installed config's own card choice (setup --gpu 1, the app's pick, "gpus_asked") by the
fit rule's pick, which is card 0 on equal cards. The installed choice and its order are kept; the Cards step preselects it.
- The server prints one line tying the engine's own numbering ("GPU 0 / CUDA0" in its log) to nvidia-smi's, when they differ.
- A card named by the app or by strata launch drops an old AMD config's hip_ordinal, which would have sent the engine to the old card.
- A list "gpu" in an image config is a device list ("1,0"), not "[1, 0]".
stream.cpp: load_dense skipped nothing of the n-gram table because it was always another shard; in a Gyro
file it is the first 28.8 GB of the one file (the 32 GB allocation that failed on the Mac mini).
mac_fit / setup_mac / setup.py (app path) / strata_mcp: Gyro-S and Gyro-M as the 'gyro' family of the Mac
(one file, --compat-bf16 pack, --native and --ple-gguf the same file).
expert_parity --decode-check compares ref_quant's trellis decoder with the format's definition written out bit by bit.
The decode (moe_gu / moe_down) and prompt (mm_gu2 / mm_down2) expert kernels read the three trellis types
through a unit decoder in quant.metal (the codebook from threadgroup or constant memory); had128 kernels
rotate the gate/up and down inputs of Hadamard-folded layers in the decode window and both prompt paths.
ref_quant decodes the types with the CPU engine's own state extraction; expert_parity takes real experts of a
GGUF (--gguf --layer), the pack's hadamard.txt (--had-pack) and has a bitwise rotation test (--had-test).
The server and strata serve start on 0.0.0.0 without a key and print one plain
warning. strata launch gets --no-api-key; the Access screen makes a key by
default and t turns it off. api_key "none" in a config means no key on
purpose. Cloudflare without a key gets a stronger warning. Docs updated.
The Terminal on PATH ignored components/control/active.json, so after
strata update the old Control kept running. strata.py now checks the
pointer first (stdlib only) and runs the newer, whole Control with its
own interpreter; never an older one, never outside the control folder,
no loop (STRATA_HANDOVER), one warning and a fallback if it is broken.
strata update says the next command already runs the new Control.
Maya's tools/ds4/chat_template.jinja lost tool associations and later system instructions; the GGUF's embedded
template matched DeepSeek's encoding.py byte for byte on 9 cases (notes-dsv41-ref.md).
Co-authored-by: mw00 <[email protected]>
CMake option STRATA_BUILD_DSV41 (default OFF, adds the target strata-dsv41 and a private static kernel lib; no
existing target changes). The vendored exl3 / sampler / window-grouping kernels live in namespace strata::dsv41k so
no symbol is shared with strata_kernels. Missing CUDA names for HIP come from a force-included private header
(include/strata/dsv41/hip_extra.h), the IQ CPU lane stays the stub branch, MAYA_* env aliases are gone.
New --score IDS.json --score-out RUN.tsv: teacher-forced top-64 log-probabilities in tools/quant_quality.py's
row format (the gate against the llama.cpp reference). MIT notice added to every ported file.
Co-authored-by: mw00 <[email protected]>
Co-authored-by: needmorevram <[email protected]>
Verbatim copy of mw00/project-maya src/ds4, tests/ds4_*, tools/ds4 and the exl3 / sampler
kernel sources it needs, MIT, with their LICENSE and a NOTICE. Not built yet; the next
commits adapt it to Strata's tree.
Co-authored-by: needmorevram <[email protected]>
tools/trellis/tqenc.{hpp,cpp}: shift-trellis Viterbi (group-min ACS), tail-biting by a rotated free pass or the exact
search over all start groups, fp16 scale refinement, importance weights. tqenc_test.cpp checks the streams, the
Viterbi against a naive DP, planted solutions, bit-exact decode against tq_states.hpp and the CPU kernels.
The CPU decoder gets K = 5 and 4 (ids 224, 225, experimental v1); existing types and layouts are unchanged.
The agent footer reads the local engine's /metrics for the turn (no empty number on short replies, prompt reading speed kept apart), model APIs are timed from the first token, the monitor average and the request list use answer speeds, the footer shows the last reply's own speed, and the server's done line no longer prints a million tok/s for a one-token reply.
The server and strata serve start on 0.0.0.0 without a key and print one plain
warning. strata launch gets --no-api-key; the Access screen makes a key by
default and t turns it off. api_key "none" in a config means no key on
purpose. Cloudflare without a key gets a stronger warning. Docs updated.
The Terminal on PATH ignored components/control/active.json, so after
strata update the old Control kept running. strata.py now checks the
pointer first (stdlib only) and runs the newer, whole Control with its
own interpreter; never an older one, never outside the control folder,
no loop (STRATA_HANDOVER), one warning and a fallback if it is broken.
strata update says the next command already runs the new Control.
tools/idle_gpu_ab.py is the 0.2.1 split A/B harness with a per-card nvidia-smi sampler. Result on the 0.2.1 engine: the two-card
split is 47-49 % idle in decode (cards take turns), the single card 5 %; STRATA_SPLIT_OVERLAP, --pipeline-windows 1/2 (with and
without the draft layer on card 0) and --batch-groups 2 give no gain worth a default. docs/MULTI_GPU.md: the overlap flag is now
measured on NVIDIA.
- the plain summary and the README get a Capped column (requests that reached the 512-token cap) and leave out the
short case's prompt rate (an 80-token prompt measures the fixed cost of a request); results.json keeps it
- BENCHMARK_PROTOCOL.md, COMMUNITY_BENCHMARKS.md, BENCH.bat and bench.sh: the standard suite took 2 to 4 minutes after
loading on an RTX 3060 and on 2x RTX A4000, not 20 to 30
On a fresh Windows PC (long paths off) the AMD engine packages could not be unpacked: staging under .updates/staging/<12 hex> pushed the
longest hipBLASLt file past 259 characters, and the failure surfaced as 'local file operation failed (FileNotFoundError)'.
- staging is now <components>/.s/<4 hex>/engine/<folder>; Control stages the same way
- unpack, delete and verify go through extended-length paths (strata_control/longpath.py)
- OSError messages name the file and say so when the path is too long
- a plain warning (shown as a warn line by launch) when the install folder is too deep for the engine's own file loading
- tests: real 0.2.1 package path lengths for a 20 character user name
The first runs of strata bench run on the 0.1.46 release engine (RTX 3060, 2x RTX A4000) showed:
- the release terminal archive carried no COMMIT file, so every run was nonconforming; package_terminal.py now takes
--commit / STRATA_COMMIT and refuses to build a distributable archive when the commit is unknown
- the model drive on an LVM volume was not found (lsblk -s nests lvm -> part -> disk)
- nvidia-smi 615 prints CUDA UMD Version, so no runtime was recorded; the runtime is now the engine's build
runtime (engine-component.json) plus the driver's supported CUDA version
- engine.log was empty (serve --quiet); it now holds the run config's log from the start of the run
- a fresh server (0 prior requests) was reported as 'could not tell whether freshly loaded'
package_terminal.py reads STRATA_BUILD_COMMIT first and only then asks git, and no longer crashes when git is missing. build.sh passes the commit as a build arg, the Dockerfile turns it into that env.
Cargo.toml and Cargo.lock, desktop and host package.json, PRODUCT_VERSION, CMake project and Metal STRATA_VERSION, build_hip_linux PKGVER default, About comment, tests that pin the number. tauri.conf.json has no version key (Tauri takes it from Cargo.toml). Download names in the READMEs, AMD_HIP.md and UPDATES.md follow.
When the room rule picks the file cache but the cache holds under half of the read set, the engine now
times O_DIRECT preads (8 threads) against first touches of a mapping (3 threads) on random 1 MiB
pieces of the model file, once, and reads unbuffered when the direct path runs at 4 GB/s or more and
is 1.3x (under 30% held) / 1.5x (under 50%) faster than the cached path. The reporter's PCIe 5 NVMe
(8.18 vs 4.93 GB/s, 13% held) lands on unbuffered (2x prompts, 3-4x decode in their A/B). A 1.7-2.5
GB/s NVMe (P1004) stays on the cache, where it decodes 1.1-1.6x faster; a ratio alone could not
separate the two, hence the absolute floor. STRATA_FILE_TIER_PROBE=0 keeps the room rule alone.
Default change: proposed, not validated on a fast drive here. Decision in platform::file_tier_prefers_direct
(platform_memory_test).
Zero-copy does not apply to experts.bin (one contiguous blob the kernels already read from the mapping), but the io_fill_ prefetch path does. RTX 3060, IQ3_XXS 43 GB pack, 32 GB cgroup: decode 1.38/1.21/1.33x (5/5 pairs), prompts 1.04/1.06x. At 16 GB the cold 4K prompt was 4-6% slower, so it stays off below the room test and the log says how to force it. Identity gates 10/10, exact_pp 4K/20K identical.
On gfx1200/gfx1201 under Linux the compute stream and the expert staging stream
landed on the same compute pipe in about half of the starts, which made 4K prompts
13% slower (p620, 1,980 vs 2,290 tok/s). Five queues (four normal, one highest) are
now opened one after the other on the main thread before any other thread starts.
STRATA_HIP_QUEUE_PRIME=0 turns it off. Answers are unchanged.
The app and setup rewrote the run config's "args" on every Load: the resource preference forced
--vram-reserve-mib to 1536 (2048 with pictures, a fifth of the card on "work") whatever the file said, and
a setup run (started by the Load when the context or pictures differ) dropped every option it did not
choose itself, the control vector options included.
- run config: "user_args" holds the options set by hand; "args_written" is the args as setup/the app last
wrote them, so a hand edit of "args" is noticed and moved into user_args (strata_control/user_args.py,
apps/host/server/native-user-args.mjs: the same rule)
- every start (serve/server.py engine_args), every setup run (setup.carry_user_args), every host write and load
and a saved tuning put the person's options over the generated ones; the same option: theirs wins
- old files without args_written: setup keeps the options the old args had, the new ones lack and setup.py never names
(plus the control vector group); a reserve that is not setup's own 700
- --vram-reserve-mib: any number the person set (Expert setting, user_args, the key, a value in args) is used as it
is, 0 included; the safety default (1536/1024, 2048 with pictures, 20% on "work") stays for a card that drives a
display or is not known to be headless; a card the probe says drives no display keeps 512 (700 with pictures);
a 0.2.0-beta file where the app wrote the number itself is told from a changed one
- hardware: drives_display per card (nvidia-smi display_active; the Windows adapter with a desktop resolution for AMD)
- setup --vram-reserve-mib, the web settings page and `start --vram-reserve-mib` record the value as the person's
- docs/TROUBLESHOOTING.md: what is kept and what the app still chooses
f3be9743 warms the first BF16 and FP16 products of every device at start. On a layer split across a
gfx1151 and a gfx1201 card it slowed prompts about 25% (hc read of the gfx1151 stage 2.7 -> 9.0 s per
32K block; STRATA_PREFILL_WARM=0 restores it). Two cards of one architecture lose nothing (2x R9700,
interleaved pairs). The warm-up now runs only when every visible card has the same gcnArchName;
STRATA_PREFILL_WARM=1 asks for it anyway. Policy in warm_policy.hpp with warm_policy_test.
On Meteor Lake and Arrow Lake-H the LP-E cores are in neither the cpu_core nor the cpu_atom
list, so the pool and setup counted them as P-cores: a Core Ultra 7 255H read as 8P+8E, the
more-E-than-P default plan did not apply and 15 workers ran (-35%). A CPU in no list now falls
back to its capacity. Tests: pool_plan_test (helper), tools/test_setup_hybrid.py (fake /sys).
Each AMD engine package gets tuning/<arch>-hipblaslt-<version>.txt for the architectures it covers and the
hipBLASLt version it bundles, and records hipblaslt_version in BUILD.json (packaging/stage_tuning.py for the
Linux build scripts, tools/hip/package_windows.py for the Windows zip). package_engine.py lists the tables in
the manifest and refuses an AMD stage without a version or with a table for another version or architecture.
serve/server.py with_component_engine sets STRATA_HIPBLASLT_TUNING from the package (strata_control/
hipblaslt_tuning.py): a table only for the exact arch and version, a log line for a mismatch, an unreadable
card or a missing version, STRATA_HIPBLASLT_TABLE=on|off (or "hipblaslt_table" in the run config) as the one
switch, and a STRATA_HIPBLASLT_TUNING the person set is kept. setup.py looks in the engine folder's tuning/
first.
A table changes the rounding of the prompt's GEMMs (R9700: residual differs, first-token KL 0.016), so the
automatic choice is off (AUTO_USES_TABLE = False) until the owner decides; "on" uses the table. No gfx1201
table exists for hipBLASLt 1.2.0 and one calibrated for it was not worth shipping (+1% prompt, -2% decode).
sign-and-notarize.sh starts strata-vision --version in the signed engine folders (it already signs every Mach-O).
MAC.md carries the M4 numbers (0.45 s on the GPU, 50.7 s on the CPU for a 260-token picture) and the rule.
- The EXIT trap of build.sh (--ctx-only or --ctx given) and publish.sh (--work given) ended with a failing test, so dash reported exit 1 after a
successful run. Both traps now use if/fi. Test: build.sh --ctx-only exits 0 and its context holds docs/TERMINAL_LAUNCH.md.
- docs/DOCKER.md: image sizes, the 2026-10-19 test of the images built from the 0.2.0-beta packages (cuda13 and cuda12 on an RTX A4000, ROCm images
built and checked, wrong-card message).
tools/vision builds one program for the Mac: ggml's Metal backend inside, --gpu reads on the GPU, without it on the CPU. The reader
choice (strata_control/vision_reader.py) gives a Mac the GPU reader whatever its memory (the CPU reader needs ~50 s per picture:
ggml has no BF16 vector kernel on ARM). setup.py's Mac path downloads the mmproj and writes the vision section; strata launch,
the MCP server and metal/setup_mac.py default pictures on. packaging/build_vision_mac.sh builds the program for the engine package.
- .github/workflows/docker-images.yml: manual only (workflow_dispatch) with the release tag as input; a plan job lists the flavors the
release has an engine package for; one job per flavor builds the image and, only with push on, pushes ghcr.io/niko1221/strata:<version>-<flavor>
and latest-<flavor> with GITHUB_TOKEN (packages: write).
- docker/fetch-engines.py: downloads the engine package from the release and checks it against the signed update feed (feed signature, archive
size and SHA-256, manifest hash, package signature, every file), the same code as `strata update`. Flavors cuda13, cuda12, rocm-gfx120x,
rocm-gfx110x, rocm-gfx1151 (the halo package), sycl (only if the feed lists one).
- docker/publish.sh: the same steps by hand (fetch, build, start check, push only with --push).
- Fix: docker/build.sh did not copy docs/TERMINAL_LAUNCH.md, which package_terminal.py needs; no image could be built from a clean tree.
- Dockerfile: OCI source/url/documentation labels (ghcr links the package to the repository).
- Compose files and docs/DOCKER.md name the 0.2.1 tags on ghcr.io; DOCKER.md has a maintainers section. Tests for the scripts, the workflow and the fetch.
Not a hang: 42 tests against a fake engine that paces real time took 127 s in one module and passed. The yield/failure tests move to serve/test_parallel_yield.py; the shared fake-engine helpers are FakeEngineCase.
get_prebuilt has probed the release with a HEAD request since 0.2.0; the refusal tests reached GitHub, got a 404 and compiled instead of refusing. The code is right, the test was stale.
A --data-dir that names a new folder used to move Models, packs and mtp into it and rewrite the saved data_dir. Now the terminal asks (keep or move), --yes and a missing keyboard keep the old library in place (still found as a root), --move-data moves, and the saved default changes only on a move, --set-default-data-dir, or a PC with no models. Tests use temp folders.
A single connection to Hugging Face's CDN stalls to 1-3 MB/s for 10-30 s again and again on the
owner's route; eight connections hide it, but a request per 32 MiB piece paid TCP slow start every
time. A request now covers up to four neighbouring pieces (the piece list and resumes are unchanged),
a try that moved bytes forward does not count as a failed one, and the default is 16 connections.
A server whose HEAD gives no length (ModelScope) is asked for bytes 0-0 so it gets the connections
too. mtp_fetch reuses the CDN address, reads the 28 shard headers side by side and lets the
connections run ahead of a slow range.
The installed app listed every model's pictures as "off", so each Load of a picture-capable model ran
setup again, which writes the arguments (--mtp, --spec-min-p, --vision, --max-context) and the vision
section from its own choices. Hand edits and the Read pictures tick came back each time.
- host: images comes from the run config's vision section, so a prepared model loads straight away
- host: Read pictures off no longer downloads/keeps the reader; --vision is left out of the engine arguments
- host: Draft layer (MTP) setting; "MTP per request" and it are the app's own and no longer frozen
- host: a context size set by hand shows in the Context list
- "mtp": false in the run config (strata_control/draft_layer.py) is applied at every start by
serve/server.py, kept by setup, written by `setup --mtp off` and `strata launch --mtp off`
- strata_install accepts any context size, not only the listed ones
- docs: draft layer off, and what the app owns in the run config
Reported crash (0.2.0-beta, Windows 11 26300): strata-desktop.exe exits with 0xc0000409 at 0x31a315 and no log. That offset is the
fast-fail in Rust's abort after a panic (panic = "abort"); a Tauri setup error is a panic ("Failed to setup app"), and the shell had no
log on Windows, no console (windows_subsystem), so the reason was lost.
- the run log opens first in main (%LOCALAPPDATA%\Strata\logs, the folder Save logs already reads) and a panic hook writes the
message and place to it and to desktop-crash-<time>.log, and shows a plain message box before the abort
- setup no longer returns an error to Tauri: a host that does not start or a window that cannot be made is reported the same way
and ends the app with exit code 1; the build error is no longer an expect
- the host's output goes to the run log (it was lost without a console)
- plain next step in the box for a WebView2 problem and for a blocked host
- STRATA_NO_DIALOG (scripted runs) and STRATA_DESKTOP_TEST_PANIC (tests) for proving the net
download() reads a file of 256 MiB or more in 32 MiB ranged pieces over 8 connections
(STRATA_DL_CONNECTIONS=1 restores one stream), writes each piece at its own offset,
and keeps the finished pieces in <name>.part.chunks so a stopped download continues
without fetching them again. The redirect to the signed CDN address is resolved once
and again before it expires or after a 401/403/410. A server without Range support
or without a length is read in one stream as before; an older single-stream .part
continues after its last whole piece. The progress line and the .part size the app
reads are unchanged, the SHA-256 pin is still checked on the whole file, and the
.done mark is the same. tools/mtp_fetch.py reads its ranges in parallel too.
tauri build makes Strata.app.tar.gz and its .sig before sign-and-notarize.sh re-signs and notarises the app, so the published Mac update would be the unsigned app. The script now writes Strata.app.tar.gz from the stapled app on request; the Tauri key stays on the PC (npx tauri signer sign), and set-app-platform checks the .sig against tauri.conf.json's pubkey over the exact bytes and records url, signature, sha256 and size.
Qwen3.8-Flash-Next (ISTA), Swift 1.5, the Coder and the picture reader get fixed
hashes (REPO_PINS, checked in download()). A 404 on the pinned commit stops instead
of taking the current file; STRATA_ALLOW_UNPINNED=1 is the announced tester opt-out
(setup.py and tools/mtp_fetch.py). Swift moves to the commit that has its IQ3_S.
Tests for pins, mismatch, 404, opt-out and offline use; SAFETY.md updated.
Header uses the existing brand lockup. Performance tables get bars and plain labels, with the engine versions left to DETAILS. What you need is built around 32 GB of RAM plus VRAM and links the platform docs. Troubleshooting moves below How does it work.
DETAILS: pictures on by default with the 24 GB reader rule, the engine label of the prompt-chunk planner (before 0.1.45.3, not 0.1.46), the per-architecture sets measured on 3060, A4000 and P100, the N7 and ROUTE_PRIOR decisions, a pointer to the app and Terminal troubleshooting. Links to private phaseA-tests notes removed from DETAILS and BATCHING. Tester readmes: AMD pictures are on (CPU under 24 GB, GPU on a 24 GB 7900 XTX), the RX 7900 text reconciled with the community runs, Mac needs macOS 15 and has no picture reader yet.
MAC: the .dmg and the Terminal bring the Metal engine; pictures only with the source build. AMD_HIP: the 24 GB
picture rule and the measured R9700 GPU reader. INTEL: the grouped XMX label is engine 0.1.45.3 (not 0.1.48), plus
the A750 prompt-hang fix (fca3c5958; STRATA_PF_DMA_PAGEABLE=1 restores the old copy). INTEL_ARC: maintainers now test
on a B70 and an A750; where the Intel engine comes from. OLDER_GPUS: the P100 sm_60 defaults (STRATA_SM60_PERF).
MULTI_GPU: two models on two cards, one make per engine. MODELS and TROUBLESHOOTING: the Mac and Intel Arc have no
picture reader yet.
Agents install the Terminal with the one line and run strata launch --model NAME --yes --no-tui (flags checked against
strata launch --help). Pictures follow the 24 GB rule on every make; the Mac has no picture reader yet. INSTALL gains
The app and The Terminal sections, update paths for both, and a Mac source section. AGENTS points to CONTRIBUTING.md.
Pictures are on by default (CPU reader under 24 GB, GPU reader from 24 GB, every make; no Mac reader yet).
Gyro and NVFP4-QAD read pictures; Gyro needs engine 0.1.45.1+ and is correct on RTX 50 since the SwiGLU guard.
The three NVFP4 versions get their app names. A Qwen3.6-35B-A3B section, the app and Terminal ways to add a model,
and engine versions on the older speed tables. Troubleshooting: no-engine warning, needs a newer engine / Update
engine, Mac xattr steps, strata doctor, Save logs. Private test-notes references removed.
Pictures on by default (CPU reader under 24 GB, GPU reader from 24 GB, every make; none on the Mac yet), the Mac app,
the Terminal in a short section, the Anthropic base URL without /v1, two models on two cards, one make per engine.
The speed tables stay and say which engine measured them. A Contributing line links CONTRIBUTING.md.
setup.py control_engine_step stops only for strata launch (STRATA_NEED_ENGINE=1); the app warns and continues. The Models card and the first-run
download card show the owner's plain warning before and during the download; the load refusal says the same with Settings > Engine.
A /plan on a small model searched again after every refused search and rewrote its plan after each one until the context reached 100%,
then the turn ended with no answer and no message. The bridge now counts what the turn shows (turn-guard.mjs): 3 failed searches in a
row, 10 searches in a plan turn (25 otherwise), a plan written from 1. again 4 times, a local model's context at 93%. The turn is stopped the
way Stop stops it but ends as a normal turn: the plan it had is still proposed (the plan card says why it stopped early), any other turn
ends with one plain translated sentence with a next step (frames carry the English sentence as `key`, the page translates it, 8 languages),
and the Harness's own /compact is asked for so the next message has room. A turn that ends with no words at all always gets a message:
"I ran out of room in this chat..." at 85% of the window or more, otherwise "I stopped without writing an answer...". Settings -> Tools says
results may come from Bing when DuckDuckGo is busy. Tests: counting (pure) and three end-to-end runs through the real Harness with the scripted
model (searchloop / bloat) and the real search code on a pretend web that refuses (bridge option searchMock), no network.
- tools/strata_mcp.py: disk usage from the files a model really uses (every part of a multi-part GGUF, the picture reader, packs, draft layer),
a file named by several flags or shared by several models counted once; Delete's plan stays separate (removable_gb)
- Settings > Storage: one list and one total (optimized models and other local models), shared files as a line of their own
- Check folder on the same folder no longer unloads the loaded models (O01); an actual change still asks first
- Settings shows the app version (0.2.0-beta) as Strata with the engine beside it; Control's 0.1.45 is no longer shown as Strata
and no longer goes into the update check as the app version
launch_gu_swiglu (STRATA_GU_SWIGLU, on by default on compute capability 12.0 cards since 147-verify) runs
16-lane gate/up rows that assume 80 dots a row, but its guard rejected only the K-quants. The APR trellis
types (TQ2_T / TQK6 gate/up: qk 128, ipb 8, 160 dots at n_embd 2560) went through it: half of each row,
Gyro-S answered with loops, garbage or nothing on every RTX 50 / RTX PRO Blackwell card. They now take
launch_gu's full-warp kernel, as launch_gu itself always did for them; 80-dot formats are unchanged.
l620 (RTX PRO 5000, Gyro-S through serve/server.py, the owner's QA prompts, greedy and the app's sampling):
before 0/8 correct, after 8/8 ("STRATA_OK 42", "4", the follow-up sentence, "PING"), the same as
STRATA_SM120_PERF=0 / STRATA_GU_SWIGLU=0; Gyro-S fix vs STRATA_SM120_PERF=0: identity gate 10/10, exact_pp
4K / 20K IDENTICAL (KL 0).
The default DuckDuckGo search failed with "limiting searches from this computer" after two or three quick searches (reproduced on a
Linux box: two searches 1.2 s apart answered, the third got HTTP 202 and every DuckDuckGo page, html and lite, refused for many minutes
while it was polled). Now: browser-shaped requests (Referer, Origin, Sec-Fetch-*), one request at a time per source 2.5 to 4 s apart with
random jitter and at most 8 a minute, a 30 minute session cache per question, a per-source cooldown after a refusal that doubles from
1 to 10 minutes (Retry-After honoured) during which that source is not asked, no lite-page follow-up after a refusal, one retry for a
timeout or 5xx. When DuckDuckGo refuses, Bing's result page is used (links decoded from its redirects). When every source refuses the
tool says, in one plain sentence, that web search is limited, to try again in a minute or add a key (Exa, Perplexity or DeepSeek) in
Settings -> Tools -> Web search, and tells the model not to retry. Tests use a mocked rate-limited search and a real Bing page fixture.
- native state: the host's 'native' events are applied at once; the light poll (1 s while a job runs, none while hidden) is no longer
held by a slow forced read; a read that never answers is dropped after 20 s
- install progress: every file of the model is counted (parts, picture reader beside the model folders, NVFP4 packs by their pinned sizes);
the bar no longer sits at 99 % while the picture reader downloads; the screen names the part (part 2 of 4) and lists the parts
- a config is 'ready' only when every part of a multi-part GGUF is whole
- Models page keeps version, context, computer use, picture reading and expert choices across tabs; Read pictures is on for a version that
is not downloaded yet; the running download is named
- version list: smaller rows, a Downloaded tag, NVFP4 / NVFP4-NVIDIA / NVFP4-QAD named by who made them with one line each
- first-run page: the four recommended versions, then a More versions list (the Models page list)
"/loop 1m say hi, stop after 3 answers" kept firing and the model answered "stopped" every minute until /loop stop. The count
("stop after 3 answers", "three replies", "5 times"), the time ("for the next 2 hours", "stop after 30 minutes") and a stop
condition ("stop when it passes", "until the file exists") are now parsed out of the line, left out of the message the model gets,
and enforced by a new scheduler (lib/loop-runner.ts): after the last allowed answer nothing is sent again, and the person is told in
one plain sentence why it ended. The scheduler also ends the loop when an answer says it is done or stopped (a bare "Stopped."
always; "Done ..." and similar when a limit was stated; DONE as the first word when a stop condition was given in words), and after
three failed answers in a row. A busy chat waits without using up the count. Tests drive the scheduler with a scripted model and a
fake clock. Strings in 8 languages.
Messages-API clients (coding CLIs, the SDKs) add /v1/messages to their base URL themselves. The Connect dialog showed the OpenAI-style
base that ends in /v1, so those apps posted /v1/v1/messages and got a 404. The third tab now shows the root address with a
one-line note (the base-URL variable for coding CLIs), the other tabs keep /v1. strata connect cuts a pasted /v1 back to the root for
every app and says so for coding CLIs, and strata launch and the monitor's Connect view list the Messages-API address. The server (serve
and the app's host, plus the shared listener) reads /v1/v1/messages and /v1/v1/messages/count_tokens as the real paths; nothing else
is aliased. Tested with the real Python SDK client against the mock-engine server.
The catalog only walked setup.FAMILIES; Qwen3.6 lives in OWN_PATH_FAMILIES, so the app's Qwen3.6 page had an empty list.
strata_models now lists its sizes (download, memory, minimum engine 0.1.45, pictures) and strata_install plans them.
strata update exits 0 when the feed has no engine for this PC (or only one for another card family), so launch went on and downloaded the model before failing with the generic engine line. ensure_engine now re-checks the engine after the update step and refuses at once with the update's own plain sentence plus one next step; setup.py in Control mode stops at the engine step for a build of another card family, an old driver, or when launch asks for an engine (STRATA_NEED_ENGINE=1), before the download step. The app without any engine keeps its model-first order.
An experts.bin pack (the NVFP4 packs) keeps STRATA_COLD_OVERLAP, STRATA_IO_PREFETCH and STRATA_IO_PF_GATE
off unless they are set: the defaults were measured on the GGUF read in place only. The startup line and
docs/DETAILS.md say so.
- STRATA_COLD_OVERLAP defaults to 4 on Linux (0 elsewhere), STRATA_IO_PREFETCH to 1 on Linux for the
file tier (mapped GGUF in place or experts.bin), STRATA_IO_PF_GATE to 1 with io prefetch. Off switches:
STRATA_COLD_OVERLAP=0, STRATA_IO_PREFETCH=0, STRATA_IO_PF_GATE=0. One startup line names them.
- cgroup_available_bytes: every clean file page of the group (memory.stat file, active + inactive; else
inactive_file) is reclaimable, less min(half of it, limit / 64), as MemAvailable counts page cache on a
bare host. A container whose own reads left active cache was judged short of RAM and its file tier
switched to unbuffered reads (an 18 GiB RAM tier in a 32 GiB group: 1.2 GiB available instead of ~11).
Unit tests with mock memory.stat / memory.max values.
- The per-request cold-reads line only with STRATA_COLD_OVERLAP or STRATA_IO_STATS set.
- docs/DETAILS.md: the 0.2.0 Linux file-tier defaults.
With STRATA_IO_PREFETCH=1, the decode's read-ahead (pread on the I/O threads, the look-ahead two layers
deep) and its pread demand reads run only while the gate is open; the prompt path is never gated.
Checked every 16 decode windows:
- room: (the tightest cgroup v2 limit - the group's non-file memory, else MemAvailable) / the experts'
bytes; closes below STRATA_IO_PF_GATE_LO 0.55, opens above STRATA_IO_PF_GATE_HI 0.65;
- pays: closes when fewer than 128 MB were read ahead in 16 windows, or under STRATA_IO_PF_GATE_USEFUL
(0.10) of those bytes were used; then a probe every 256 windows.
Closed, the look-ahead predicts one layer and warms with the madvise hint, as without io prefetch. The
decode thread (and the fetch threads it starts) is marked by FileExpertSource::DecodeScope. The serve
log's file-tier line says the room, the share used, the state and the flips. A request's wait for the
adaptive round in flight at its start is logged when >= 1 ms.
5 pairs (Q2_0 GGUF in place, 12 GB card emulated, cold SSD, cgroup RAM caps), mode 4 + IO_PREFETCH +
gate vs mode 4 alone: 32 GB 1.16 / 1.21 / 1.70 decode, 1.86 / 1.98 prompts; 16 GB (closed) 1.00 / 1.01 /
1.10, 1.54 / 1.78 prompts; an 18 GiB RAM tier 0.91 / 0.84 / 0.86 (its IO_PREFETCH start re-checks the
RAM inside the cgroup and switches the file tier to unbuffered - not the gate's doing).
Identity gates vs b829a3189 (fixed flags, Q2_0 and IQ3_S, default and mode 4 + IO_PREFETCH + gate with
--mmap-experts): 10/10 and exact_pp 4K / 20K IDENTICAL in all four.
Owner-approved for 0.2.0. The card list says (GPU n) with the same numbers as Monitor and the picker; a loaded
model is named from the catalog (Qwen3.8-Flash-Next Q2_0) in the picker, the Loaded list, the sidebar card and the
Monitor, the raw file id only as a fallback; with Run multiple models on, This computer and the card rows show what is
left after the models that stay loaded (a short muted line says so), in 8 languages. The picker rows no longer
print the raw id beside the name.
ready_needed() drops the checkpoint's tensors/* once the model is prepared (prepared_done): the engine reads the prepared files only. have_model, ready_fetch, the already-downloaded search in other Strata folders and the disk need now use it, so a prepared model whose 95 GB of safetensors were deleted is ready instead of failing with 'not enough free disk' and a 102 GB download.
NVFP4-QAD (FP4-Local) reads pictures: vision True in setup.py and the MCP catalog; the config's vision section uses prepared/dense.gguf as the text model with the shared mmproj, like the GPTQ pack.
Tests for both; the launch test for a model without pictures now stands one in.
The coordinator's rule: the owner PC and a tester's system service run connectors for unrelated tunnels, and
quick tunnels must keep working. Running connectors are listed (pid; the command line is only matched,
never shown). A refusal only when one is clearly Strata's own: --url at Strata's port, or the same token.
Terminal (strata launch) follows the same rule. Checked with a real cloudflared on r9700.
Found running the AMD AppImage on r9700 (Q2_0 on card 1, IQ3_XXS on card 2, Run multiple models):
- the card list ignored loaded models (same text, same pre-selected card); a second model on a used card died
after two slow engine starts with 'server_exited'. Now the row says 'Already used by <model>', a free card is
pre-selected while Run multiple models is on, and a failed start there is one plain sentence (no useless retry).
- Unload refused because a chat still used the model (409) showed nothing near the button; it now shows the
sentence and the next step in the Loaded models card.
- a model that failed to start was listed in the chat picker (disabled); it is hidden.
Inside the AppImage LD_LIBRARY_PATH holds the bundled libraries, and on Ubuntu 26.04 (uutils coreutils)
dirname fails there: 'libsystemd.so.0: version LIBSYSTEMD_254 not found'. The python/python3 launchers then
computed an empty folder and the app said Control stopped / no engine. Found with the AMD AppImage on r9700.
Gyro's expert files are native_experts.txt v5 (Hadamard-folded). The engine reads v5 since the APR port
(f47b0385f); the released 0.1.45 stops at v4 (its binary and source have no v5 reader, every 0.1.45.1+ package
has). GYRO_ENGINE in setup.py and the catalog the app reads (tools/strata_mcp.py: engine in the fallback table,
engine_min on every size) now say 0.1.45.1. The host marks sizes the installed engine cannot read
(needsNewerEngine; an engine built from this checkout or with no readable version is never called too old),
refuses the download and the load with a plain sentence, and Models / first-run setup show the existing
'This model needs a newer engine' notice with Update engine and hold the buttons back.
The per-model settings already live in one text file (strata-<model>.json: args, env, sampling and the
named keys). Expert settings gets an Open settings file button (Notepad, open -t, or the system's
plain-text program) with the note that changes apply on the next start. The file is checked when the
panel opens, when the window comes back into focus, and before a load: bad JSON, a wrong value, an
unknown key (the nearest real name is offered) are said in plain words with the line, in 8 languages.
A mistake holds the start back instead of an engine that dies on it; an unknown key only warns.
The app only looked in Strata's own connector folders, so a cloudflared installed by the system
(/usr/bin, brew, winget, a .deb) was reported as not installed. Now: own folder, component folder,
PATH, then /usr/bin, /usr/local/bin, ~/.local/bin, Homebrew, Program Files. The first copy that runs
--version and is 2022.1.0 or newer is used; the Connector step names it and its place. A cloudflared
already running (system service or started by the person) is said so plainly and not doubled.
Terminal (strata launch) gets the same search and check. Plain messages in 8 languages.
SYCL has no cudaHostRegister, so the expert arena is pageable memory even where the engine calls it pinned. A copy->memcpy of a 2.3 MB blob
from it goes through the Level Zero driver's staging path on the calling thread, which never returned on some prompts (A750, default
flags, a 2,744-token docs excerpt: the thread spun in sched_yield inside libze_intel_gpu with every engine idle, 6 of 6 runs; also seen
in a 5.3K-token needle prompt of the task suite). Only USM host allocations (the --stream-experts pinned mirror) are DMA'd directly now
(answer cached per 2 MiB bucket); every other expert goes through the stager's pinned buffers like an unpinned one.
STRATA_PF_DMA_PAGEABLE=1 restores the old direct copy.
Checks: the repro completes 3 of 3 (0 of 6 before); identical first-token logits and tokens, old vs new: A750 (frozen share, 1.4K, 2.5K and
4K prompts), B70 stock config and B70 without --stream-experts (there the new path is also faster: prefill 680-740 vs 77-456 tok/s); A750 4K
prefill 79.4 vs 79.3 tok/s, B70 stock 1246 vs 1240.
Adds sycl/tools/test_prompt_deadlock.sh with the 2,744-token prompt (passes on the fix, times out on the old build).
llama.cpp refuses a GGUF whose tensor directory has a type it does not know, so the picture reader could not open a Gyro model (TQK6 / TQK7, ids 145 and 146) and Gyro was left without pictures. strata-vision now copies the key/value header into a small tensor-less GGUF and loads that vocab-only; the weights are never read or type-checked. Falls back to the file itself when the header holds a value type the copier does not handle.
Gyro-S and Gyro-M read pictures (setup.py, MCP catalog). Adds a C++ test (strata-vision-gguf-test) with a GGUF that has unknown tensor types.
The NVIDIA worker's non-bitwise result came from a cherry-pick onto a tree without the dot-count guard in launch_gu (K-quant rows have 160 dots, the sub-16 rows assume 80); the merged tree has it and the Split path is bitwise on every route.
core/gfx12_platform_defaults.hpp holds the per-platform rule (checked in arch_defaults_test); the engine log names it at the first prompt; docs/AMD_HIP.md.
Conflicts with 148-amd's gfx12 WMMA default resolved: STRATA_HIP_WMMA unset = gfx12 on every call (148-amd), 0 = off, 1 = forced (also gfx11), auto = gfx12 with n_q >= STRATA_HIP_WMMA_MIN_Q; failed launch falls back to the FP32 kernel; the 96-slot ring follows the same state; the opt-in hint is gone.
The zero-copy pre-pass ran mincore for every CPU expert of a layer, the RAM copy's too: -2..-9% decode
with an 18 GiB RAM tier. It now skips the RAM-resident and override experts (b32: 0.98/0.97/0.91 ->
1.05/1.10/1.08 vs mode 0, 3 pairs). A file without a residency query (not Linux) never goes zero-copy.
- STRATA_IO_PF_SCHED=1 (with STRATA_IO_PREFETCH=1): confidence-scored read-ahead from the router
lookahead, per-window speculative byte budget, deadlines (late jobs dropped), useful-share feedback;
new per-request counters (read ahead MB, used before demand, late, not admitted).
- STRATA_RAM_ADAPT2=1 (with STRATA_RAM_ADAPT=n): RAM-tier promotions ranked by expected stall saved per
byte (drive vs page cache), read on the source's I/O thread, installed at window boundaries; the
prompt's routing seeds it with a limited weight (Prefill::on_routes).
- STRATA_COLD_OVERLAP: 1 reads a layer's staged blobs in the background while the pool computes the
ready experts, each landed group computed as it lands; 2 measures R / C_r / C_c per layer; 3 adds
zero-copy and 4 is zero-copy alone: an expert the page cache holds whole (GGUF in place, Linux mincore)
is computed from the mapping through a per-job NativeFmt (ExpertJobMulti::fmt) instead of being
assembled into a stage buffer. Same bytes, same rows, same bits.
Default (all unset) is the old code path: identity gates vs b829a3189 IDENTICAL (Q2_0 and IQ3_S,
fixed flags, 10/10 + exact_pp 4K/20K); mode 3 with --mmap-experts also 10/10 + 4K/20K IDENTICAL.
Teacher-forced KL against the old kernel on an R9700 (Q2_0, int8 KV, fixed cache so the floor is 0.0000): 0.0003 over 6,024
positions (p99 0.006, top-1 99.8%), 0.0014 over 1,760 positions of 8 long documents (top-1 98.9%); HumanEval, reasoning, agent,
needles, CJK and tool calls equal. gfx11 stays opt-in. The 96-slot prompt ring follows the kernel (qsa_hip_wmma_active()).
The start-up hint about the switch is gone.
The auto chunk scan only tested the GPU cache. A lent slot's expert comes back from the RAM copy or the model file for the length of a
prompt, and every chunk streams the whole file set; past the page cache that set is re-read per chunk (28-45 GB per 29K prompt on
2x RTX 3060 / 32 GB / IQ2_XS at 8192-token chunks, ~1 GB at 4096). prefill_plan.hpp is the pure model (file set, miss share fitted to
#2824, additive stream + compute + drive time); the serve scan and the generate path refine the VRAM pick with it, after the RAM copy
exists. A set that fits the page cache keeps today's pick. STRATA_PREFILL_HOST_PLAN=0 turns it off; --prefill N is untouched.
The catalog no longer forces images:false in the app; a model reads pictures when it is picture-capable and the engine component carries strata-vision.
API models: a model reads pictures when the person ticks 'This model reads pictures' or the server says so (model list modalities, llama-server /props); the chat sends image_url to any OpenAI-style API that does, and says in plain words how to fix every case where it cannot.
Importing a model folder recognises the picture reader next to it and says plainly when a model type is not ours (use a Custom model API).
native_w11_kernel's gate/up path walked 256-value super-blocks only; a Q2_0 row is 64-value blocks, so gate/up of that
format now takes the same one-block-a-stage walk its down path has. R9700 Q2_0: prompt +16.7% (4K) / +19.0% (20K),
5/5 pairs; hip_prefill_fused_iq Q2_0 / Q2_0: fused vs MMQ rel RMS 1.6e-5.
The RTX 5070 check (48 SMs, IQ2_XS pack, experts partly in RAM): the
fused int8 prompt experts were 4.7% slower on 20K prompts (0/6 pairs) with
no decode gain, and one token a block row could not be told apart from
token pairs in the hyper-connection down projection (an earlier run leaned
to pairs). So on a 12.0 card with fewer than 80 SMs the prompt experts
keep MMQ (STRATA_PF_FUSED=1 still takes the fused kernels) and the down
projection takes token pairs at every even launch (STRATA_GR_DOWN_TG=2);
12.0 cards with 80+ SMs (the RTX PRO 5000 measured) are unchanged.
A new prompt's first chunk waits for its n-gram rows (65,536 random SSD
reads at 4K tokens) unless the table is in RAM. RTX PRO 5000 (Linux, NVMe,
251 GB, a fresh engine per prompt, 3 rounds, the same bits): prompt 4K
1298 -> 1059 ms (-18%), 20K 4644 -> 4223 (-9%), 98K 20447 -> 20027 (-2%),
decode +0.8%. On an R9700 (slow prompt) the read hides behind layer 0 and
nothing changes; under 64-200 GB RAM caps (cgroup, page cache included)
ram mode equalled direct there and cost ~6 s of a cold start.
The rule: not low-RAM mode, Linux (not WSL), and RAM >= the model's own
need + the table + 16 GB (Q2_0: 93 GB, IQ3_S: 107 GB); a smaller PC keeps
the SSD reads as before. Setup says what it did and how to undo it.
Tests: RotationalDisk gets three SSD cases; the golden configs keep the
Windows behaviour (ple_ram_platform_ok mocked).
Owner decision, the AMD gfx12 rule: on a compute capability 12.0 card the
native packs' prompt experts run on the fused int8 kernels (moe_fused_iq)
for the gate/up formats measured there - the IQ3_S pack's IQ2_S, IQ3_XXS,
IQ3_S and IQ4_XS layers with IQ4_NL / Q2_0 down. IQ2_XXS / IQ2_XS gate/up
keep MMQ until measured. This rounds differently from MMQ:
RTX PRO 5000, IQ3_S: prompt 4K +7.0%, 20K +5.2%, 98K +3.4% (3/3 pairs);
teacher-forced KL against MMQ 0.0002 (5998 positions, noise floor 0.0000)
and 0.0010 (1760 positions after ~6K-token documents); first-token KL
against the FP16 prompt path 0.01181 vs MMQ's 0.01236 (48 prompts of
1.5K-24K tokens); HumanEval 156/164, reasoning 26/30, agent 6/6, needles
6/6, CJK 8/8, tool calls 4/5 - the same as MMQ.
STRATA_PF_FUSED=0 keeps MMQ, =1 takes every covered format (as before),
STRATA_SM120_PERF=0 drops the 12.0 default. Other cards: unchanged.
native_gu_swiglu_kernel: a 512-thread block owns 32 hidden columns of a
group; each 16-lane sub-warp computes gate row r and up row r together
(row_dot2_80_sub16: both rows' loads before the applies, each row's sums in
row_dot_80_sub16's order), the block forms h = silu(gate) * up in shared
memory, and warp k writes entry k's q8_1 block with q8_1_store. The values
are the separate kernels' bit for bit (gate/up are the same floats, the
product is rounded on its own as in swiglu_q8_1_entries_kernel); it saves
the gate/up round trip and one launch per layer.
RTX PRO 5000 (IQ3_S, all experts resident): a layer's gate/up + SwiGLU
35.5 + 2.4 us -> 36.3 us; fixed 4-token rounds 11.92 -> 11.80 ms; default
flags, 6 interleaved pairs: decode +0.6% story, +0.9% code, +0.5% 8K
document, 6/6 pairs each, the same tokens in every case.
On by default for compute capability 12.0 (sm120_series.hpp); elsewhere
opt-in with STRATA_GU_SWIGLU=1; STRATA_GU_SWIGLU=0 turns it off.
Measured on the RTX PRO 5000 against the token pairs of the previous commit,
fixed verify rounds, same text: 2 tokens 9.44 -> 9.24 ms, 3 tokens 10.81 ->
10.56, 4 tokens 11.94 -> 11.90, 5 tokens 14.04 -> 13.70. Each (row, token)
is still summed by one warp in the same order. STRATA_GR_DOWN_TG=2 / 0 picks
pairs / no split as before.
The shared table was measured on an RTX 5070 (48 SMs) together with an RTX
3060, so a small 12.0 card keeps it; the RTX PRO 5000 table (110 SMs) now
applies to 12.0 cards with 80 or more SMs (il_cc answers 1201 for them).
The 5-8 column table stays for every 12.0 card. docs/DETAILS.md: the
measured decode gain of the sm_120 defaults, docs/MMVQ_IL_TABLE.md: the
new tables and switches.
Measured on an RTX PRO 5000 Blackwell 72 GB (IQ3_S, every expert in VRAM),
fixed 4-token verify rounds, story / code, the same text as before:
STRATA_SIDE_MIXER=2 13.67 -> 13.15 ms a round
STRATA_ATTN_DAILY=1 -> 13.31
STRATA_GR_DOWN_TG=2 -> 13.34
STRATA_GR_UP75=1 -> 13.44
all four 13.70 -> 12.23 ms (2-token rounds 10.37 -> 9.59)
Each is bitwise equal to the kernels it replaces (their parity tests; the
identity gate). include/strata/core/sm120_series.hpp holds the rule: on for
compute capability 12.0 unless STRATA_SM120_PERF=0; each switch's own
variable still wins in both directions on any card. The down projection's
token pairs run at every even launch there (Turing keeps 4 and 6 only).
STRATA_ATTN_DAILY / STRATA_ATTN_MERGE_V2 and STRATA_SIDE_MIXER are now
decided per device (a layer split can mix cards). Other cards: unchanged.
A verify window of 5+ rows fell from the interleaved small-batch kernels (1-4
columns) back to native_mmvq_multi: on an RTX PRO 5000 Blackwell the dense
projections of a round jumped +2.0 ms from T=4 to T=5 (the main Q5_K head
521 -> 1011 us). native_mmvq_il_kernel now also runs 5-8 columns from the
cp = 8 interleaved copy (1-2 rows a warp), every value bitwise native_mmvq's
(mmvq_il_parity, T 1-8, every rows choice).
Tables for compute capability 12.0, read off mmvq_il_parity --bench
--emit-table on the RTX PRO 5000 (3% rule): kIlRows_120 for 2-4 columns (it
takes the interleaved kernels in more cells than the shared table, e.g. the
head at 2 columns 507 -> 367 us) and kIlRows58_120 for 5-8 columns (the head
at 5 / 8 columns 899 -> 491 / 1178 -> 667 us). Other cards keep their
tables and native_mmvq at 5+ columns.
STRATA_MMVQ_IL_WIDE=0 turns the 5-8 column path off; STRATA_MMVQ_IL_120=0
gives a 12.0 card the shared 2-4 column table again.
Conflict in src/prefill/prefill.cpp (step 2b): the split rotates mixed into Xr (APR, had_gu)
before 0.1.45's quantize_scatter (NVFP4 activations keep the gather); 0.1.46 adds the pf_wmma
FP16 gather branch. Both kept: wmma_l gathers FP16 rows (and rotates Xs / the swiglu output Hh
in fp16 when the layer is Hadamard-folded), otherwise the split's rotate then quantize_scatter
or the NVFP4 gather.
Merge resolution as in f-145-perfprs-withvolta: iq_kernels.cu (IQ3_XXS / IQ3_S staged sign-mask tables over the EXPDEC grid staging), prefill/kernels.cu and prefill/prefill.cpp keep both sides; CMakeLists keeps both test blocks.
The switch made every card answer as sm_75, so a Tesla P100 (sm_60) launched the m16n8k8 prompt-attention kernels, whose sm_60 code is a __trap(): the engine died on the first long prompt with "prefill copy_i32: unspecified launch failure". A card below sm_75 now ignores the switch and runs the kernel it runs without it (the same tokens).
Claude Code asks for max_tokens 32000 on every request and the Harness for 16384; on a 32K context the
prompt fits but prompt + max_tokens does not, and the server answered 400 "request exceeds the context
window ... requests are never truncated" before the first token.
Service.prepare() (the one place chat/completions, messages and responses all pass through) now clamps
max_tokens to ctx - CTX_SLACK - prompt when the prompt fits, logs one line, and the answer_room thinking
budget (3/4 of max_tokens) is taken from the clamped value. A prompt that alone does not fit is still the
plain 400 ("no room to answer"). --no-fit-max-tokens / "fit_max_tokens": false brings back the strict
llama.cpp 400; --fit-max-tokens stays accepted.
Tests: serve/test_server.py MaxTokens (shortened by default in the three dialects, exact fit and one over,
prompt-too-long 400, thinking budget follows, strict mode), serve/test_context_overflow.py (default
service), serve/test_monitor.py (error record uses the strict mode).
tools/nvfp4_local_pack.py convert|ple|mtp|verify reads NVIDIA's ModelOpt checkpoint and writes what the existing NVFP4 path takes:
the CSF block scales are decoded to plain E4M3 (experts then repack bit for bit), MXFP8 weights are dequantized exactly and take the
Q8_0/BF16 policy of nvfp4_convert, the n-gram table becomes a one-tensor GGUF of 90-byte rows, the MTP head the folder mtp_pack reads.
ngram.cpp reads that table as a new PLE format (I8 bytes, strata.ple.format = nvfp4x16).
An adopted model reports the parent of the person's own GGUF folder as its data folder; the next preparation sent that as the folder for the packs and Control refused it ('a new data folder must be named Strata'). A preparation from a GGUF folder now uses Strata's own download folder. Found by the real-box check on P2003; test fails without the fix.
Control answers a refused folder with exit 5 and the plan (ok:false, issues); the host threw that away and the page said only 'The files could not be checked'. The plan with its reasons now reaches the page, which says 'Strata can't run these files yet' and keeps the checker's codes under Details. Found by the real-box check (a Gemma vocab GGUF on P2003).
- Models: the family switch (Qwen3.8-Flash-Next, Qwen3.6-35B-A3B) is back in the navbar without the word Model; version, context and computer use, the tuning line and Load/Tune sit in one card beside This computer, so the tab fits the first view; image generation moves into Expert mode
- Tune: one result card for faster, already the fastest, couldn't measure (busy PC, with plain copy and Try again), failed and stopped, driven by the host's code, reason_code and values; the same Tune dialog frame for all of them
- Tune running: the step icon glows and a small dot beeps instead of a ring spinning around a square tile; reduced motion respected
- Expert mode: Context memory and Low-RAM mode show the active tuning's values, the engine settings list what the tuning sets, and loading does not re-prepare the model because of them
- the tuning line (Using your tuning / Switch off) is a calm line with an icon
- strings in 8 languages; tests in tune-outcome.test.mjs
- Settings > Storage: downloaded optimized models show family + quantization with the full name as a tooltip, size aligned, Delete at the end
- Engine > Diagnostics: one View logs button; Save logs lives in the logs view
- About > Privacy: six short lines across the box instead of one long paragraph
- Monitor: the request summary is a plain sentence ('In 18 requests your average output was 47.4 tok/s and prompt reading 1,332 tok/s, reusing 91K tokens.'); the screenshot leaves out Model state and Conversation cache
- Tasks: Bypass permissions replaces 'Allowed to make changes' and is the default for a new task; an opened task has an empty left navbar and no Model section, its name in the page, and a readable Markdown last answer; no deleted-tasks list, restore or suggestions; + icon opens the editor modal (Draft it with AI inside it); removing says 'Task removed'
- strings in 8 languages; tests in ui4-fixes.test.mjs
- while a real run works, an animated A/B card shows what the assistant is doing (host activity), elapsed time and Stop (TimeSkipWorking); the sidebar no longer opens at the start and opens only when the finished run changed files; a run that changed nothing says so in the chat and has no tab
- the finished A versus B (TimeSkipCompare) keeps one window size whatever was made: text and Markdown rendered with changed words marked; HTML as two sandboxed previews (no permissions, no scripts, no outside requests) with a View code toggle; pictures side by side, click to enlarge; code and other files as a folded line diff; several files through a compact file switcher
- host: changed pictures are listed as pictures and served from the run's own folders by GET /api/timeskip/file (images only, inside the run, size-capped)
- strings in 8 languages; tests for the file kinds, word and line diffs, the HTML policy, the activity words and the picture route
- status line under the newest message: pulse, a rotating playful word only while it thinks, the real action once a tool runs, elapsed time and tokens; only the newest status frame is ever shown and it is cleared on idle, done, error and stream end (src/lib/live-status.ts)
- reasoning is one quiet line (what it is thinking about, then 'Thought for 38s'); opened it follows the newest line unless the person scrolls up
- goal bar above the input: small, icon-only pause/resume, edit, remove, from the goal frame, the SSE goal event and /api/goal (src/lib/goal.ts); resume sends goalAction:'resume' with no user bubble
- all strings in 8 languages; tests in tests/live-chat.test.mjs
Locales: both sides added keys from s2676, so ui2's 88 keys are renumbered s2849-s2936 in all 8 files. events.mjs: ui2's verbsFor target plus agent2's ask card. index.mjs: the node:path import takes both sides' names (relative, sep). The workspace default is ui2's (Documents/Strata/Workspace).
- approval cards: one plain question per tool call (server/agent/ask.mjs maps commands and files to sentences; src/lib/ask.ts holds the translated templates), a compact calm card, the exact command under Details
- Tasks: the opened task shows schedule, what it may do, last result, instructions and last answer; back link before the title like the chat's navbar; pause/resume and delete icons, Edit, Run now; pause and play icons added to the sprite
- Models: 'This computer' shows graphics memory and RAM bars with what the chosen version takes, and one plain line about the SSD (src/lib/native-fit.ts, from the catalog's memory_gb and the hardware report; the n-gram table is not counted)
- Models: one-line version list: Recommended (the fixed four), Swift, GSQ-RCO, NVFP4, Gyro, Unsloth groups, Use your own GGUF; RAM+VRAM in bold, download size muted, 'Uses SSD N GB' only where it applies
- Tune: five steps with their own animated icons and states, a live speed readout and sparkline, a result card with the gain and what it was saved as; reduced motion respected; the tuner now reports each measurement's speed
- catalog: memory_gb and mem_total per version (tools/strata_mcp.py)
- tests: ask, native-fit, tune-phase, autotune speeds, memory figures; headless checks (ui-flows ui2/ui2d, scripts/approval-flow.mjs), light and dark
- agent context: the Harness reports a cached prompt as cacheReadTokens, apart from inputTokens; the context now counts input + cache read + cache write + reply of the last request (a long agent chat showed 0-3%; checked against a real Swift IQ2_XS 64K run: 8,152 tokens, matching the engine's own 8,042-token prompt + 110 reply)
- first workspace: Documents\Strata\Workspace (display name Strata) unless STRATA_WORKSPACE names one, never the app folder; a saved first workspace that is the app folder moves there and its chats follow through an alias
- workspaces can be renamed (display name only, path kept): PATCH /api/workspaces/:id, pencil in the sidebar, 8 languages
- tests: workspaces.test.mjs, cache-aware context in functional-fixes.test.mjs
#1498 pins the KV host copy with cudaHostRegister under WSL; the switch forces either order. Measurements: 5 interleaved pairs on an RTX 3060, an RTX A4000 and a Tesla P100.
- agent path: live stats frames and a final tok/s from the stream timing; the context is the last request (one source)
- tool rows say what happened in words (Wrote a file, Ran a command, Couldn't write the file) with the file or command; approval cards show it too
- a launch that returned without error reports 'Started.' instead of 'nothing to show'
- agent system prompt replaced by a short plain one; goal, skill, workflow and fork tools left out (first message about 7.5k -> about 4.3k tokens)
- Models: the running model reports the profile it was started with and the page shows it; Reasoning popover names the loaded native model
- installed model with other settings: 'Apply settings and load', no size, no download wording
- plain chat: agent:false or tools:false answers directly and never runs a tool
- tests: stats/context/verbs/launch/prompt size, server-side enable_thinking=false with the agent's request shape, plain chat over HTTP, ui-flows profile scenario
cmake/strata_ggml_tq_overlay.cmake writes, at configure time, a copy of the pinned ggml-cuda headers (and ggml.h, quantize.cu) into
the build folder with TQ2_T / TQK6 / TQK7 routed the way IQ2_XS is: same Q8_0_16 tile layout, dot kernels and tile configs (the
fork's own configs for them are copies of IQ2_XS's), three template instances, the tile loader and tq.cuh from third_party/ggml-tq.
Only the strata_mmq target sees the copy; every other type is compiled from the untouched files. Anchors are checked: another
llama.cpp turns the overlay off with a message (FP16 prompt path). moe_mmq.cu knows the trellis block geometry itself.
The tile loader and the trellis tables are agentionai/llama.cpp's (MIT, ggml authors; Laurent Zuijdwijk), ported from
agentionai/llama.cpp 93a24e5 ggml/src/ggml-cuda/{tq.cuh,mmq-load-tiles.cuh,tq-lut-i8.cuh}.
Checked: Gyro-S and Gyro-M exact_pp 4096/20000 residual dumps IDENTICAL to rc2 (R9700 gfx1201); Q2_0 and IQ3_XXS IDENTICAL to f-split
on gfx1201 and sm_86; ctest ggml_tq_overlay.
index.mjs: createNativeControl gets control, appUpdater and hardwareProbe; the agent bridge gets componentsDirectory() and the bundled Harness folder.
native-control.mjs: this tree's file kept, s4b's parallel choice, fit_max_tokens, instance parallel/mcpServers re-applied after buildConfig; its own retry around control.start is folded into load-host's: ONE second try in all, which carries the memory change and/or the drop of several-at-once, with load-host's plain sentence (FALLBACK_NOTE only when no memory change applies). Test added.
locales: both sides had added keys s2571.., so s4b's seven task keys are renumbered s2615-s2621 in all 8 files.
native-control.mjs: this tree's file kept (S6 updates, load retry, remove), s5's buildConfig split out of loadEngine, auto-tuner, tuneConfig/tuneStatus/autotune, autotune and recheck operations and presets re-applied.
NativeModels.tsx: s5's Tune model button (the real tuner) and NativeTune dialog; the old short-benchmark Tune button stays out. native.ts: job progress field.
The planar kernel of #2340 is the wide kernel's arithmetic, but it was launched without the 256-GRF mode that #2207 gives the GGUF-layout
wide kernel, so on an Alchemist card it spilled: mmvq_bench 2560 x 248320 on an A750, 4 columns, 1.78 ms (GGUF layout, large GRF) against
15.4 ms planar, 8 columns 4.8 against 48 ms. Same rule as #2207 (STRATA_MMVQ_LARGE_GRF, default on Alchemist only), same arithmetic.
On 2x RTX A4000 behind a host bridge cudaDeviceCanAccessPeer says 1 and cudaDeviceEnablePeerAccess succeeds, and
cudaMemcpyPeerAsync then returns success having moved nothing (1,048,576 of 1,048,576 floats wrong in both directions
in a stand-alone test; right without peer access enabled, the driver stages it). The peer prompt path trusted it: the
peer's input rows were zeros and its results never reached the primary, the first prompt's MoE output was garbage and
the logits NaN (--peer-device 1, --peer-reserve-mib 1500..2100; STRATA_PF_PEER_COMPACT is not the cause).
PeerExperts::open now copies 64 KiB each way once after enabling access; a wrong result turns peer access off again and
takes the host route (the one STRATA_PEER_NO_P2P=1 asks for), and says so. A test that cannot run keeps the old trust.
Reported by @CaptRauti on #2666 (WDDM, 2x RTX 3060: the no-P2P route is the one they ran).
Built on P2006 from the f-split tree and run through the Terminal archive: resolve (kfd gfx1103), tune, serve and client, TUI, Ctrl+C/SIGHUP, saved preset, memory back to baseline.
Both files are one GGUF with the n-gram table inside: 58.5 GB and 92.0 GB downloads for 27.6 GB and 35 GB of VRAM + RAM together.
Neither is a default; the MCP recommendation names them only where the 2-3-bit sizes and the Coder do not fit. Pinned to the Hub
revision 1ada08422e40 (sizes and SHA-256 from its LFS pointers). Model license: Qwen Community License 1.0.
For tq2_t / tqk6 / tqk7 experts (--synthetic, --synthetic-hadamard), the
gate and down rows on every ISA the CPU has against the scalar kernel bit
for bit, and the scalar kernel against ggml-cpu's vec_dot on the same
Q8_0 activations. The CPU path of check (b) already runs through the new
kernels via native_gu_rows / native_down_rows.
Ported from agentionai/Strata rc2 7d0ecd0 (rc2 = 9ff02c9), author Laurent Zuijdwijk.
Expert-cache misses of the Gyro files run on the CPU workers. Until now those
rows went through ggml-cpu's vec_dot one token at a time. Strata now has its
own kernels for the three trellis types (tq_cpu.hpp), chosen at run time like
the i-quant ones:
- the 32768 x 4 int16 state table (one 8-byte lookup per four weights, built
from the codebook in third_party/ggml/ggml-common-tq.h, now also visible to
host code), scalar state extraction, four table rows per YMM by broadcast
loads + VPBLENDD, VPMADDWD (AVX2) or VPDPWSSD (AVX-512 VNNI);
- the weights of a block are decoded once for all the tokens routed to the
expert (a verify window), then each token is a few instructions;
- one fixed arithmetic for every ISA: exact int32 sums per Q8_0 sub-block,
four float FMA chains, a fixed final sum. AVX2, AVX-512 and the scalar
reference give the same bits, and a token gives the same bits alone or in a
group of any size (no group-size rule needed, cf. #152).
native_gu_rows / native_down_rows take them for types 144-146 on AVX2 and
AVX-512 CPUs. STRATA_NO_TQ_KERNELS=1 restores ggml-cpu's vec_dot,
STRATA_TQ_ISA=scalar|avx2|avx512 caps the ISA. A CPU without AVX2 keeps
ggml-cpu.
tq_cpu_parity (CPU only, ctest tq_cpu_parity and tq_cpu_parity_avx2):
AVX2 / AVX-512 vs the scalar kernel bit for bit for groups of 1-8 tokens,
vs ggml-cpu's vec_dot (rel ~1e-7, float order) and vs to_float in double
(rel ~1e-5, the int16 table), plain and Hadamard-rotated, for Gyro-S
(tqk6/tqk7) and Gyro-M (tq2_t) experts through the engine entry points.
`tq_cpu_parity --bench` is the micro-benchmark (clock-calibrated cycles).
Zen 5, one thread, weights streamed from DRAM, core cycles per 128-weight
block (one token / per token in a group of three):
ggml-cpu vec_dot, AVX2 build: 108-117 / 108-116
ggml-cpu vec_dot, AVX-512 build: 64-66 / 61-62
Strata AVX2: 47-49 / 22-23
Strata AVX-512: 47-49 / 22
(Q2_0's AVX2 kernel: ~20; the repacked AVX-512 Q2_0 kernel: ~7)
Ported from agentionai/Strata rc2 7c85f1d (rc2 = 9ff02c9), author Laurent Zuijdwijk.
hadamard_rows / hadamard_rows_f16 still exit on an invalid block, like
check() does on a failed launch, but now through one helper whose message
names the entry point, the block, the row width and the requirement (a power
of two in [2, 1024] dividing the width), and where the mismatch comes from.
Ported from agentionai/Strata rc2 48ff8b9 (rc2 = 9ff02c9), author Laurent Zuijdwijk.
Not changed to an fp32 rotation: hadamard_rows_f16 already does its
arithmetic in fp32 and rounds once on the store, so the only extra error is
the fp16 rounding of the SwiGLU output before the rotation, while an fp32
copy would cost a T*K*640 float buffer or a fused kernel on the non-MMQ
fallback. The comment states this, the precision cost and the parity bound
(relative L1 < 2e-3 against the float rotation); the gate/up input refers to it.
Ported from agentionai/Strata rc2 50a1972 (rc2 = 9ff02c9), author Laurent Zuijdwijk.
The v5 rewrite for Hadamard-folded experts was a re.sub on "v[34]": on any
other header it changed nothing and still wrote hadamard.txt, an
inconsistent pack the engine refuses at load. native_experts_v5() now
rewrites v3 and v4 only and otherwise returns an error naming the version
found and the supported ones; main() prints it and exits 1 before any file
of the pack is written. Tests: both rewrites, the refusal for v2/v5/v6/v10
and a missing header, and a full pack run that must leave no
native_experts.txt or hadamard.txt behind.
Ported from agentionai/Strata rc2 235787c (rc2 = 9ff02c9), author Laurent Zuijdwijk.
run_impl called hadamard_device_signs for every folded layer of every chunk,
which takes a process-wide mutex and may cudaMalloc. Prefill::init now
resolves the per-layer gate/up and down sign pointers on its device into
Impl::had_sx / had_sh, as peer_had_signs does for the peer and the layer
split's helper (P.had_sh), and run_impl only reads them. carve no longer
warms the device table on every relayout (init's lookup does that now, once);
the comments on peer_had_signs and the new members say what is resolved when.
Ported from agentionai/Strata rc2 3d191fa (rc2 = 9ff02c9), author Laurent Zuijdwijk.
run_impl asked hadamard_device_signs for the down projection's signs at a
literal 640 (and gate/up's at N), while carve and the peer path resolve them
at lay.fmt[l].n_ff / n_embd. The widths are part of the device copy's key, so
the two only shared an entry because the widths happen to match. All lookups
now use the layout's widths.
The other 640s next to the rotations are the row width of the prompt path's
expert buffers and products (swiglu, quantize, GEMM), fixed for this model
like N; Prefill::init now refuses a native layout whose experts are not
N x 640 instead of assuming it. No other sign or rotation width in the tree
is hardcoded (verify, peer and remote experts use the geometry).
Ported from agentionai/Strata rc2 ff463c8 (rc2 = 9ff02c9), author Laurent Zuijdwijk.
Decode reads the int8 codebook tq_lut_i8 (one global scale, 0.85% relative
RMS); the FP16 prompt path reads the fp16 codebook and the MMQ path is the
GGML fork's tile loader. The comment over tq_load / tq_apply now says that
only the decode-once and per-entry kernels are bitwise equal to each other,
and that native_expert_parity bounds the decode path against ggml's float
dequantizer at relative L1 error < 3e-2 (the prompt dequantizers at < 1e-6).
The same goes into a short "Numerics" section of docs/GYRO.md and the parity
test's header.
Ported from agentionai/Strata rc2 457047e (rc2 = 9ff02c9), author Laurent Zuijdwijk.
nat_xr_ (T x N floats) was taken from the verify arena for every pack, though
only a layer whose gate/up is Hadamard-folded reads it. It is now carved only
when some layer of the native layout has had_gu, as the prompt path does with
its rotated copy; the one reader is under that layer's f.had_gu, so it never
sees the nullptr.
Ported from agentionai/Strata rc2 687b7e6 (rc2 = 9ff02c9), author Laurent Zuijdwijk.
The GPU path for Hadamard-folded experts is now validated end to end on
CUDA sm_86, sm_89 (also a two-GPU layer split and peer decode) and sm_120,
and on HIP gfx1151, so `strata generate` no longer exits without
STRATA_APR=1 (the variable is ignored if set). It logs a warning instead for
the two setups that have not been run end to end yet: a HIP GPU other than
gfx1151, and the P2P prompt peer (Prefill::set_peer).
docs/GYRO.md and tools/gyro_setup.sh no longer set STRATA_APR=1; the setup
script's AMD note now says what was validated on gfx1151.
Ported from agentionai/Strata rc2 0fabd20 (rc2 = 9ff02c9), author Laurent Zuijdwijk.
Gyro-S is 58.5 GB (54.5 GiB). Gyro-M and AMD have now run end to end;
long-prompt prefill is measured. The measured section now has the int8
decode numbers (rc1 includes them and --prefill auto), the server
protocol, Gyro-M and the 24 GB RTX 3090; the cache-size rows stay,
labelled as the 2026-10-02 build.
Ported from agentionai/Strata rc2 c494121 (rc2 = 9ff02c9), author Laurent.
Upstream 0.1.39 calls the decode-once loaders as
S::template load<STAGE_GRID>(row, kbx, iqs, s_grid) (the IQ types can stage
their grid table in shared memory). The trellis loader has no grid to stage;
it takes the template parameter and the table pointer and ignores them.
Found by the first CUDA build of rc1 (CUDA 12.4, sm_89).
Ported from agentionai/Strata rc2 07e9afd (rc2 = 9ff02c9), author Laurent Zuijdwijk.
set_peer and set_stage_helper no longer decline an APR pack. Their MMQ
share now takes the same rotations as the primary's MMQ path:
- gate/up input: the primary sends its rotated copy (m.Xr = H (s * x), made
once for its own MMQ quantize) instead of `mixed` to the peer / helper
card, over P2P or the mapped host route. One rotation serves both cards,
bit for bit the same input; `mixed` still feeds the shared expert.
- down input: on the peer / helper card, hadamard_rows on H between
mmq::swiglu and mmq::quantize, in the compact (per group) and the
layer-sized buffers, with that card's own copy of the layer's sign
vector (PeerPrefill::had_sh, made at setup on the peer's device).
- The fused int8 prompt kernels never take a folded layer (fused_nat), and
bind_stage_helper already refuses layers on the fused kernels, so the
helper's folded layers are always on MMQ with the separate
swiglu -> rotate -> quantize passes.
- The trellis types need nothing more there: the peer gathers the blobs
with mmq::gather_native and runs the same MMQ instances (STRATA_MMQ_TQ).
Not run on a GPU yet. Needs a 2-GPU box: Gyro-S with --peer-device (and
separately a layer split with STRATA_PREFILL_HELP=1), the first token's
logits and a greedy generation against the single-GPU run.
Ported from agentionai/Strata rc2 c64e1ec (rc2 = 9ff02c9), author Laurent Zuijdwijk.
Port of the agention llama.cpp CUDA change (perf/tq-cuda-decode 0005): the 2048-point trellis
codebook as int8 pairs with one global scale (max |point| 3.984 -> 127; relative RMS error 0.85%
of the codebook std). A trellis step's 4 weights are two 2-byte entries = one int, so a decoded
lane (TqLane) is 4 ints instead of 16 floats (5 registers instead of 17) and tq_apply is 4 dp4a
instead of 16 int8->float conversions + 16 FMAs. load/apply stay split: decode-once and per-entry
kernels remain bitwise equal. The dequantizers (prompt path, embedding) keep the fp16 codebook.
native_expert_parity --synthetic tqk6/tqk7 tq2_t/tq2_t and --synthetic-hadamard: 0 failures (RTX
3090, CUDA 12.4); GPU rel vs reference 1.18e-2 -> 2.13e-2 (tqk6/tqk7), 2.20e-2 (tq2_t), within the
3e-2 bound; gpu decode-once vs per-entry: bitwise equal.
Ported from agentionai/Strata rc2 87bad56 (rc2 = 9ff02c9), author Laurent.
The Gyro config used --prefill 512, so the prompt path borrowed only 364 cache slots. Measured on
an RTX 3090 (24 GB, cache auto = 15,756 slots, ctx 131k), same prompts: 4K-token prompt 767 ->
1,325 tok/s, 16K 817 -> 1,870 tok/s, on par with ISTA IQ2_XS under setup.py's config (1,401 /
2,058); decode unchanged within noise.
Ported from agentionai/Strata rc2 a4d70dd (rc2 = 9ff02c9), author Laurent.
docs/GYRO.md: build against the agentionai llama.cpp fork, pack the published GGUF unchanged,
Strata's MTP runtime, server config, RTX 5090 measurements (full and 16/8 GB-sized expert caches)
and known limits. tools/gyro_setup.sh does every step on Linux (CUDA or HIP).
Ported from agentionai/Strata rc2 759675e (rc2 = 9ff02c9), author Laurent Zuijdwijk.
carve() takes m.Xr (T x 2560 floats, the Hadamard-rotated copy of `mixed` for
the MMQ path) from the MoE set of the shared region, but moe_set_bytes, which
sizes that region (and bytes_needed / the borrowed slots), did not count it.
With an APR pack and MMQ on, the MoE set is the region's largest, so its last
take overflowed and init failed with "device buffers for a chunk of N tokens
do not fit" at every chunk and cache size, borrowed or own buffers alike
(reported on an RTX 5090 with the published Gyro-S). Workaround on an older
build: STRATA_PREFILL_MMQ=0 (the FP16 prompt path takes no Xr).
Ported from agentionai/Strata rc2 09042dd (rc2 = 9ff02c9), author Laurent Zuijdwijk.
- iq_kernels.cu: dq_q6_k (llama.cpp's dequantize_block_q6_K, two halves per
thread) in dq_dispatch; embed_type_supported and iq_row_bytes take Q6_K
(embedding only) - Gyro-S's token_embd.
- ngram.cpp: PleTable reads Q8_0 rows (170 B; PLE_ROW_BYTES_MAX raised), mapped
and direct SSD I/O - Gyro-M's per_layer_token_embd.
- native_expert_parity: --gguf-embd (random token_embd rows via iq_embed_rows
vs ggml to_float), --gguf-ple (random PLE rows via PleTable, both I/O modes),
--load-check (the engine's start-up checks for a pack + GGUF, no GPU load).
gfx1151: Gyro-S token_embd (Q6_K) and Gyro-M token_embd (Q8_0): 300 rows,
0 values differ; Gyro-M PLE (Q8_0) and Gyro-S PLE (IQ4_NL): 320 rows, 0
differ, mmap and direct. --load-check on packs of both published files:
0 failures. (Measured before the rebase onto 0.1.39.)
Ported from agentionai/Strata rc2 cc287a6 (rc2 = 9ff02c9), author Laurent Zuijdwijk.
- iq_kernels.cu: the TQ lane dot split into tq_load (16 codebook values) and
tq_apply; vec_dot_tq_q8_1 = apply(load), Split<144..146> + kSplit, so
verify windows decode each weight lane once for all entries (bitwise
equal to the per-entry kernels).
- MMQ: with STRATA_GGML_DIR = the agention fork, the fork's
mmq-instance-{tq2_t,tqk6,tqk7} are built and moe_mmq takes the types
(STRATA_MMQ_TQ). prefill.cpp rotates a copy of `mixed` (Xr) before
mmq::quantize and H between swiglu and quantize; device sign vectors are
made at prefill setup.
- hip_prefill_mmq_parity: TQ gate/up (1280x2560) and down (2560x640, odd
blocks per row) passes; MMQ_PARITY_REPEAT; the output sentinel is now set
with hipMemsetAsync on the test stream (a null-stream hipMemset could land
after the MMQ kernel and make every output look unwritten).
- Rebased onto upstream 0.1.39: the fused int8 prompt kernels (STRATA_PF_FUSED)
never take a folded layer. The multi-GPU prompt paths (set_peer,
set_stage_helper) decline a Hadamard-folded pack here; a later commit
gives their MMQ share the rotations.
gfx1151 (ROCm 7.2.1, STRATA_PREFILL_MMQ=ON): MMQ parity passed 3 runs x 10
repeats (rel_l2 1.4e-3..3.2e-3); native_expert_parity --synthetic and
--synthetic-hadamard tqk6/tqk7 tq2_t/tq2_t: 0 failures, decode-once bitwise
equal. CUDA 12.8 sm_120 full build compiled (incl. TQ MMQ instances) before the
rebase onto 0.1.39.
Ported from agentionai/Strata rc2 dc19786 (rc2 = 9ff02c9), author Laurent Zuijdwijk.
native_expert_layout takes gu_row / d_row / up_off / down_off from
iq_row_bytes, which had no cases for the trellis types and returned 0:
every gate, up and down row read row 0 of the blob, and up/down read the
gate matrix. The dequantizers (flat superblock indexing) were unaffected,
which is why they matched ggml bit for bit while the expert matmul did not.
native_expert_supported now also refuses a type without row bytes.
gfx1151 (ROCm 7.2.1): native_expert_parity --synthetic and
--synthetic-hadamard tqk6/tqk7 tq2_t/tq2_t: 0 failures (gpu rel = cpu rel,
cpu-gpu 5e-5..1e-3).
Ported from agentionai/Strata rc2 dae1f66 (rc2 = 9ff02c9), author Laurent Zuijdwijk.
- third_party/ggml/ggml-common-tq.h: the fork's TQ2_T/TQK6/TQK7 block
structs and 2048-point codebook (device table).
- iq_kernels.cu: Fmt<144..146> (the fork's tq.cuh lane decode against q8_1,
4 q8_1 blocks per 128-block), dq_tq in dq_dispatch (prompt FP16 path,
embedding), is_iq / row bytes; hadamard_rows / hadamard_rows_f16
(shared-memory FWHT in the CPU's butterfly order) and
hadamard_device_signs; native_expert_grouped rotates h when
NativeExpertLayout::had_block is set.
- verify.cpp (local GPU), remote_experts.cpp (helper GPUs), prefill.cpp
(FP16 fallback; folded layers never take MMQ) rotate x / h into their
own buffers; the shared expert and the CPU doorbell copy keep x.
- native_expert_parity --synthetic-hadamard; ctests for tqk6/tqk7 and
tq2_t when ggml is the agention fork.
- generate.cpp: APR packs run only with STRATA_APR=1 until validated end
to end on a GPU.
- hip_backend.cmake: gfx1151 as an unvalidated architecture.
Rebased onto upstream 0.1.39: native_expert_grouped takes the separate
SwiGLU / rotate / q8_1 passes when the down weights are folded (the one-pass
swiglu_q8_1 kernel has no h to rotate); the new decode peer GPU
(peer_experts.cpp) rotates its private input copy and passes the down signs,
as remote_experts.cpp does.
Ported from agentionai/Strata rc2 6c7abde (rc2 = 9ff02c9), author Laurent Zuijdwijk.
- hadamard.{hpp,cpp}: hadamard.txt parser, process-wide spec, in-place
signs + normalized FWHT per block.
- expert_layout_load reads native_experts.txt v5 + hadamard.txt and sets
NativeFmt::{had_block, had_gu, had_d, gu_signs, d_signs}; inconsistent
packs are refused.
- native_quant_act / native_quant_h rotate before quantizing, which covers
every CPU consumer (expert_source's doorbell copy, the pool's h).
- generate.cpp refuses Hadamard packs until the GPU paths rotate too.
- native_hadamard_test: Strata's CPU expert vs the fork's ggml graph
(ggml_mul(signs) + llama_mul_mat_hadamard + ggml_mul_mat): bit-identical
on real Gyro-S (TQK6/TQK7) and Gyro-M (TQ2_T) experts, also through
ExpertPool::run_split_multi_native; synthetic Q8_0 reproduces the
unrotated float expert to 1.2%.
Built with -DSTRATA_GGML_DIR=<agention llama.cpp> (ggml-cpu supplies the
trellis vec_dot kernels).
Ported from agentionai/Strata rc2 1587897 (rc2 = 9ff02c9), author Laurent Zuijdwijk.
- Type tables: PTQ1_0/TQ2_T/TQK6/TQK7 (143-146, 128-value blocks) in the C++
and Python GGUF readers.
- iq_pack.py: validates prism.hadamard.* and writes hadamard.txt (block,
sign vectors, folded roles per layer); native_experts.txt becomes v5 for
such models so an engine that cannot rotate refuses the pack. Anything
it cannot apply (dense folded weights, inverse tables, gate without up,
Q2_0 experts, missing sign widths) is refused. 3 new unit tests.
- embd_bf16_pack.py --gguf: a GGUF's token_embd dequantized to BF16, for
embedding types without a GPU dequantizer (Gyro-S: Q6_K).
Ported from agentionai/Strata rc2 d864b6a (rc2 = 9ff02c9), author Laurent Zuijdwijk.
- runtime start/restart/verify that fail print the run config's engine log path and its last lines (the line was only 'server_exited')
- doctor: on Linux with a SYCL or HIP run config, a /dev/dri/renderD* (and /dev/kfd) the user cannot open is GPU_DEVICE_ACCESS (warning) naming the render group;
seen on P1006 where the engine only said 'No device of requested type available'
P2006 (Radeon 780M, gfx1103) resolved the gfx1200/gfx1201 package as usable: HIP detection looked at the PCI vendor only.
The Linux kfd topology (gfx_target_version) names each card's gfx target; a package whose archs list none of them is skipped with the reason.
Unknown targets (Windows, no kfd) are not a refusal, as with CUDA.
- strata-gate plugin: Manual asks before a change (no blocked attempt first); children of a Manual chat reach the same card
- preset.mjs: the Harness standard preset minus scheduler, search without a key, unanswerable question tool, plan mode
- helper agents projected into the existing agent rows; Stop stops the helpers; the run ends when they have reported
- /goal through the Harness goal Remotes; goal rounds stream into the chat
- native-parallel.mjs: request slots by default in the installed app (fit policy, one-line switch), batch-mtp, one-slot retry
- plain wording for agent failures and tool errors, original text kept as details
- stage-harness.mjs: pruned production Harness for the installer (not run yet)
0.1.41 scaled each stage's miss term by its probed link (0391e478);
0.1.44 scales from a 4x gap against the fastest link (9d41e18b, #2007).
On an x16+x4 dual-3090 rig (26.9 vs 6.4 GB/s, 4.2x) either rule moves
auto off the balanced placement (K=27 -> K=19/K=31): decode -4..-8%,
prefill +15..+19% at 10K / -6..-9% at 90K vs 0.1.40.x at matched
calibration. Opt-in restore next to STRATA_SPLIT_LINK_ABS, default off.
Found qualifying the Terminal archive on real boxes:
- engine calibrate crashed with ModuleNotFoundError (tools.calibrate / setup are not in the archive): refuse with exit 5 naming STRATA_INSTALL_DIR and tune run --mode api; --plan reports tool_available
- models import --plan failed with "fingerprint catalog is unavailable": data/gguf_fingerprints.json is now in the archive
- tune plan/run --engine <package exe> could not load the package's libraries: engine_env puts the package folder first; --engine component:strata-engine accepted
- a boolean candidate (--kv-grow) was passed as "--kv-grow 0" ("unknown argument: 0"), and the protocol harness turned "0" into a bare flag (on): flag_argv
- an unmeasured candidate's reason showed the engine usage dump; the reason line comes first now
ErrorNotice and toasts map known failures (server_exited, engine missing, network, integrity, Harness missing, Control) to a sentence and a next step with the original text under Details. The install progress sums a list of parts (model files, engine) into one count with a time left. The Read pictures block is hidden when the catalog has no pictures. Control reports the last server log lines of a failed start. Reading size scales headings, code, tables and reasoning. All eight locales.
On an MI50 ROCm queues are suspended while the kernel reclaims around the HMM-tracked arena; the cached
reads of the new rule churn the page cache. The gfx906 backend keeps the old room >= read_bytes rule;
other HIP cards keep the current rule (the PR applied it to every HIP build).
Co-authored-by: KevinX8 <[email protected]>
native-control runs Control's tools/strata_mcp.py on Control's Python (installerEnvironment: STRATA_NO_ACQUIRE stays for the
process; only the install job the user starts may fetch model files), in a writable per-user install folder. The catalog,
hardware (nvidia-smi) and recommendation come from it, the install plan and job go through it, and a failed catalog is an
empty one. Calibration stays off in the packaged app. packaged-control tests adjusted and extended.
In the app's mode the install downloads the model the user chose, prepares it with the bundled tools, installs no Python
package, fetches no engine and compiles nothing, and writes a run config naming component:strata-engine into a per-user
folder (STRATA_INSTALL_DIR). Only bundled tools run (run() refuses pip, git, cmake and anything else). The engine
component is looked up if present; a model can be downloaded before an engine is installed.
stage-control.py adds setup.py, the preparation tools (static import closure), ref/load.py and data/ to the staged Control.
GuidedSetup read catalog.recommendation.family without checking that a catalog has a
recommendation; the packaged host's catalog had none and the app showed 'couldn't show this
screen'. GuidedSetup and NativeModels now tolerate no catalog, no recommendation, no families
and families without sizes. A test renders both components with each of those shapes.
The default takes the pipelined decode on two stages only with the draft layer on the first card (mtp_first); asked
for by hand it was taken without a word, and without mtp_first the draft chain shares the last card with its
window (2x R9700: the pipeline lost to the serial loop before 0.1.44). Recommend, never force: it stays on as
asked and a note says what it costs, why mtp_first is not on (no --mtp / --spec 2+ / draft_vocab.bin, or
STRATA_MTP_STAGE0=1 to ask for it) and how to turn it off. No default output changes.
Reported by @dinesh-chander on #2637 (closed by the reporter).
On RTX 4060 Ti 16 GB + RTX 3060 12 GB (48 layers, IQ3_S) the first pick was K=2 and the #1616 retry
(every stage at least 3 layers) landed on its own edge, K=3: the 3060 ran 3 layers, 12 GB idle, 452 tok/s of
prefill against 802 at 28 layers (the prompt flows through the stages as a pipeline). For two GPUs the retry
now first asks for a placement in which each stage owns at least floor(layers x its room / all rooms) layers
(split_rules::share_floors, at least 3, trimmed to fit); the three-layer rule stays the second try and the
first pick the last, and the log says which one was taken. Nothing changes where the first pick has no
one- or two-layer stage (equal cards), and three or more GPUs keep the #1616 rule alone.
Reported by @Tworks25 on #2644, with the sweep of layer_split 12..32 (PR #2643). split_rules_test gets the
reporter's rooms (14.2 GiB and 5.50 GiB) and the identical-card and degenerate cases.
Tauri single-instance plugin: a second launch focuses the open window and starts nothing. On Windows the shell puts itself in a
kill-on-close Job Object, so killing it, a crash, logoff or shutdown ends the host, Control, the owner host, the server and the
engine. The host follows its parent (STRATA_EXIT_WITH_PARENT) and takes SIGHUP and SIGBREAK down the clean shutdown; every engine
it starts is tied to the host with runtime start --die-with-pid (drain 15 s, then ended); it runs runtime cleanup once at start.
The shell waits 60 s for the host's clean stop (the host's own deadline is 45 s) before ending it.
lifeline.popen ties every engine, image encoder, MCP server and sd-cli to the server that started them (kill-on-close job on Windows,
PR_SET_PDEATHSIG from one anchor thread on Linux, a pipe watcher elsewhere). Engines and encoders get their own process group so
Ctrl+C, a closing terminal and a console Break reach the server only, which stops the engine the clean way (STOP, QUIT, wait 20 s,
terminate 5 s, kill). SIGHUP, SIGBREAK and the Windows console close/logoff/shutdown events take Ctrl+C's path; the handlers are
installed before the model loads and the browser launch no longer holds the main thread (#1872). The owner guard escalates SIGTERM
to SIGKILL after a grace period. strata_control/procs.py records every launch and finds leftovers; the next engine start, runtime
start, runtime stop and the new runtime cleanup end them after re-checking identity. runtime start --die-with-pid for app-owned
instances. tests/terminal/lifecycle_matrix.py is the close-path matrix.
Tesla P100 (sm_60) prompt read, interleaved 5 pairs each, thread vs warp scorer: 30000 tokens 339.29 vs 335.45 tok/s (1.011) and 338.98 vs 330.20 (1.027), 60000 tokens 338.88 vs 330.15 (1.026), 5/5 pairs faster each, below the +3% bar for a new default. Same bits either way. docs line; qsa_scores_parity runs with the switch on.
The z440 A4000 Q2_0 gate with STRATA_PF_FUSED=1 on both arms found the residual dump different from base: gu_covered() took Q2_0 on every CUDA card and d.ok on sm_80+ now required the Q2_0 gate/up setup.
Interleaved A/B, 6 pairs, --pcie-frac 0 vs --pcie-frac 0 + the switch, decode ALL: RTX 3060 IQ3_XXS 53.36 vs 53.16 tok/s (0.996, 3/6 pairs faster); RTX A4000 Q2_0 61.72 vs 62.17 (1.007, 4/6). The author's own +2.2% is below our +3% bar and does not reproduce on Ampere. Declined; the PR author's commit stays in the history only as this revert's parent.
"--ple-io mmap" as ONE element of "args" (or "--ple-io=mmap") reached the engine as a single
argument, which it stops on ("unknown argument: --ple-io mmap"); the usual form is two entries.
The server now splits such an element into the flag and its value and says so at startup
(never one in a flag's value place). A config with a JSON mistake (a missing comma between two
args) gave a traceback; it now names the file, line and column. The slow-PLE-probe warning says
how the two entries are written in a config. --ple-io mmap, ram and direct themselves parse
correctly in the engine (checked on the 0.1.45 build).
Reported by lama7 on #2267.
PleTable::gather_batch matched the Direct-mode prefetch slots by position, so rows
prefetched as A..F and gathered as D,E,F,A,B,C (or A gathered as A,A,A) were read a
second time. Each requested token now looks its key up among the prefetch slots and
decodes from the matching slot into its own output position; duplicates share a slot.
All outstanding tickets are drained on a failed collect (the first error is kept)
instead of dropping the rest. The decoded bytes are unchanged.
Reported by @hotschmoe, who also supplied the fixture and the patch; the patch is
used as proposed. ple_reader_test --selftest gets the reorder / duplicate / subset /
missing / over-the-window cases, compared against the mapped reader.
Approval label is Needs your approval on every card; example-card results lose the Demo prefix. A finished agent tool shows Approved only when the person approved it. Other Demo labels (simulated-model toasts, estimates, preview test, time-skip, context panel, tasks, serving) become truthful Example/Illustrative/Simulated wording in every locale.
The page sends the chat's mode with each turn. The bridge switches the session to our preset with the Harness's own /permission command, verifies it through the permissions projection, and fails the turn closed if it cannot. Manual is read-only with ask (a change needs a one-shot approval), Plan is read-only with never (refused, no card), Bypass is workspace-write with never. Tests run the three modes against the real Harness with the scripted worker.
setup.start passes it to the server it spawns (subprocess.call env, or execve for Docker); the runtime host sets it in
the owned server's environment. The test checks the start's child environment.
- engine-install.mjs extracts with tarBinary(): System32\tar.exe on Windows (bsdtar reads .zip and .tar.*), /usr/bin/tar
elsewhere.
- util.mjs which() asks System32\where.exe on Windows (first match), /usr/bin/env which elsewhere.
- the tuning and Time-skip state default to %LOCALAPPDATA%\Strata\tuning and \time-skip on Windows
(apps/host/server/platform-paths.mjs; POSIX keeps $XDG_STATE_HOME/strata). The Python side resolves the same folders
(strata_control/paths.py, used by the runtime directory and the tune and timeskip commands), so the host and the
terminal agree.
- tests: apps/host/tests/platform-paths.test.mjs (both platforms from either host) and tests/terminal/test_state_paths.py.
The terminal archive is a new folder with no run configs. models.resolve_install() now looks at the install folders
setup.py remembered in settings.json (installs, next to data_dir) when the archive's own folder has none, read-only,
and reports how it found them (explicit, here, remembered, default). The run configs name their engine and model
files by absolute path, so the old install and its data folder keep serving: nothing is copied, moved or created.
tests/terminal/test_migration.py builds today's layout in a temp dir (strata-<model>.json, engine/, run scripts,
.venv, a separate Strata-data with GGUF, packs and MTP, settings.json) and checks the listing, inspect, a profile and
serve --plan, a plain start and the explicit setup path leave every file, size, mtime and hash as they were.
A box with no DNS (p620) can build the Linux archive from a folder of wheels fetched and hash-checked elsewhere:
pip still runs --require-hashes against the lock, with --no-index --find-links.
strata runtime start/status/stop/verify/restart now work on Windows. The owner transport (loopback-only TCP, 256-bit
token, state file in a private directory) was already there; the supervisor around it was fcntl/getuid/killpg only.
- strata_control/winproc.py: LockFileEx byte-range leases (shared and exclusive, on a byte beyond the data so the file
stays readable) and process identity (OpenProcess + GetProcessTimes). Liveness never uses os.kill(pid, 0): on
Windows that is TerminateProcess.
- runtime.py: state directory %LOCALAPPDATA%\Strata\runtime with a protected one-entry ACL (existing ones must be
unshared); owner-only checks through private_files.is_unshared; the state file's address must be a loopback port;
the owner host is detached with CREATE_NEW_PROCESS_GROUP|CREATE_NO_WINDOW; the server runs in a kill-on-close job
object (serve/winjob.py, terminate() added), so the whole tree ends with the host however the host ends; a graceful
stop closes the server's stdin pipe (--owner-fd 0, serve/owner_guard.py turns the EOF into the Ctrl+C drain path);
the deadline kill ends the job; the ownership marker records the child's creation time, so a PID that was reused
by another process is not mistaken for the recorded child. A PID from a state file is only compared, never signalled.
- A connect failure to a lost owner is a typed ProfileError (exit 3) instead of a raw OSError.
- `strata serve` and calibration take the activity lease on Windows too.
- tests/terminal/test_runtime.py runs on Windows (32 tests were skipped): a .cmd stands in for the fake engine, the
signal tests have Windows variants; new tests: a second client attaches from another process, wrong/stale tokens
cannot stop the owner, a recorded PID of an unrelated live process is never touched, a reused PID with another
creation time is not the recorded child, the liveness check does not end its target, the directory ACL, shared and
exclusive leases.
The shell now builds and tests on Windows. Three fixes found by running the installed NSIS build:
Tauri's resource_dir() is a verbatim path (\?\C:\...) that node.exe cannot take as its entry script, so the host died at once and the window hung; the path is made plain.
The self-test only knew the macOS Resources layout; it now finds resources next to the exe (Windows) and under lib/Strata (Linux).
A host that exits early is reported at once instead of after the 60 s health wait.
@tauri-apps/cli 2.12.3 is a pinned devDependency with a lockfile (npm ci), and the npm scripts call it instead of cargo tauri.
package_terminal.py builds the terminal archive for windows-x86_64 (.zip), linux-x86_64 and darwin-arm64 (.tar.gz)
from the pinned python-build-standalone CPython 3.12.15 (SHA-256 per target, from the release's SHA256SUMS) and a
per-target lock, packaging/locks/runtime-<target>-cp312.txt, installed with pip --require-hashes --no-deps
--only-binary=:all: at BUILD time only (pip is the interpreter's own, no upgrade; the result must equal the lock).
packaging/lock_wheels.py makes a lock. Console-script launchers that name the build path are removed; a VERSION
file and the read-only gguf_reader tool join the payload.
- bin/strata.cmd and bin/strata.ps1 (Windows), bin/strata (POSIX): the bundled runtime first, PYTHONNOUSERSITE=1,
no PYTHONHOME/PYTHONPATH; a path with spaces works.
- START-HERE.bat, SETUP.bat, UPDATE.bat, setup.sh, update.sh use a python/ folder beside them when present (no
Python lookup or install).
- strata --version (one line: version and interpreter), strata serve --engine mock, a typed exit-5 message when the
server's packages are missing (strata.py preflight).
- packaging/smoke_terminal.py extracts an archive in a clean env (space in the path, PATH without Python, dead proxy)
and runs --version, doctor, profile list, install, serve --engine mock (health, models, a chat answer).
- tests/terminal/test_packaging_pins.py: digests, lock shape and agreement with requirements.txt, launcher order.
setup.py mixed acquisition (pip, downloads, engine fetch and compile) with configuration and start. A start of an
installed model used to check the engine version against the release and download a newer one, and moved model
files into the data folder.
- strata_control/acquisition.py: forbidden() / require() / AcquisitionRefused (exit 5, code acquisition_refused,
names SETUP / UPDATE / `strata install`). STRATA_NO_ACQUIRE=1 forbids a whole process.
- setup.py: start() runs inside forbidden(); download, pip_install, run, every urlopen (now _urlopen), the
engine fetch/compile and CUDA-library installs call require() first. A plain start only says when the engine is
older than expected (engine_update_note) and no longer touches the network; UPDATE / setup keep
update_installed_engine. A plain start reads the data folder from settings.json and creates, moves and copies
nothing (data_folder_in_place); setup, check, calibrate and update still gather files as before.
- strata install [--update] [setup options]: the explicit installer entry; a folder without setup.py says so.
- tests/terminal/test_no_acquisition.py: the start/serve import graph reaches no installer code, no serving module
runs pip/git/cmake/npm or opens a URL (listed exceptions are request-time or loopback), and a real start with the
network and every subprocess mocked makes no request and runs no installer.
Tuning, Time-skip, the revise command, the native MCP client, setup and
calibration jobs and the native server all go through server/python-resolver.mjs;
the Microsoft Store alias is skipped because it must start.
A Windows checkout (autocrlf) has CRLF in LICENSE, and the bundle verify
required the file to start with b'MIT License\n'. Normalize line endings
before the check, pin the file to LF, and test a CRLF member.
14 test files took the path of `new URL(rel, import.meta.url).pathname`, which on
Windows is "/C:/..." and cannot be resolved (esbuild "Could not resolve"). Strip the
leading slash before the drive letter. No behavior change on macOS/Linux. Went from 14
whole-file failures to 6 real test failures (listed in the S0 notes). Easy to drop if the
owner would rather have a different fix.
Follow-up to the owner's rebuild branch strata/rebuild-2026-10 (small, test-only).
IMPLEMENTATION_STATUS.md, ACCEPTANCE-EVIDENCE.md, QUALIFICATION-KIT.md and the
evidence logs (text only, about 140 KB). The 170 MB of screenshots under
apps/host/docs/evidence and checkpoints are not in git; they are in
StrataSplit\evidence-archive. The Electron mentions in IMPLEMENTATION_STATUS.md are
history (Electron was replaced by Tauri); the superseded top-level desktop/ folder is
not carried over.
From the owner's rebuild branch strata/rebuild-2026-10.
packaging/package_terminal.py builds the self-contained terminal archive with its own
Python runtime, monitor_bundle.py the portable monitor, acceptance_matrix.py maps the 67
acceptance rows to evidence, README.md documents the target matrix. The built archive
(release/artifacts/*.tar.gz, 42 MB) is not in git.
From the owner's rebuild branch strata/rebuild-2026-10.
strata_control/tuning.py (the job contract and the HyperCore-Tune decision), tune_engine
(measures through the real API and records memory), tune_history, tune_apply (explicit,
reversible apply and rollback of a recommendation) and tune_api, with tests/tuning
against a stub engine. Review-only: nothing here starts a sweep from the UI, and the
sweep has the known gaps listed in the S0 notes (e.g. --kv-grow 0 / --expert-cache 0
command lines).
From the owner's rebuild branch strata/rebuild-2026-10.
The Time-skip state machine: checkpoint, revise (a bounded Harness turn), check and
decide, with a durable job record, cancellation, and the "inconclusive means ask the
person" rule. The host routes are in apps/host; the CLI entry points are in strata_cli.
From the owner's rebuild branch strata/rebuild-2026-10.
A Harness profile for Strata, a bridge that drives a Harness agent session against a
Strata server (_bridge.py, bridge_e2e.py, bridge_tests.py), the tool-execution /
approval / cancel end-to-end test (tool_flow_e2e.py) and the bounded revision turn the
Time-skip job uses (revise_once.py). Tested only against a scripted mock worker; the
Harness is not wired into the app's chat yet.
From the owner's rebuild branch strata/rebuild-2026-10.
The Tauri shell (src-tauri Rust, 400-odd lines) that starts the Node App Host and
opens the owner's UI, with the macOS entitlements, icons and generated schemas, and
the build scripts in apps/desktop/scripts that package the app around a private Node
sidecar (digest-verified fetch). The Node binary, Rust target/ and DMGs are not in git;
src-tauri/resources/node/ keeps only its .gitignore placeholder.
Windows and Linux packaging is not working yet (the sidecar lookup expects "node" and the
scripts are bash-only); see the S0 notes.
From the owner's rebuild branch strata/rebuild-2026-10.
The UI source from the 14 October handoff (src/, public/, index.html, vite and
tsconfig, tests, audit-fixes, design docs) and the App Host (server/*.mjs): model
import and monitor routes, API-provider catalog and streaming (OpenAI chat, OpenAI Responses,
the Messages API), serving, tunnel, tasks, plus the host routes for Time-skip
(server/timeskip.mjs) and the tuning plan/reports. src/lib/time-skip.ts renders a real
job's own record.
The UI design is unchanged. The rebuild routed #/tuning to a new TunePanel; that is
reverted: src/screens/ModelRun.tsx and src/main.tsx are the handoff's again (#/tuning
goes where the handoff sends it), and src/components/TunePanel.tsx and
src/styles/tuning.css, which only that route used, are not carried over. Left out of
git on purpose: dist/ (build output), node_modules, and docs/evidence + docs/checkpoints
(170 MB of screenshots, kept in StrataSplit\evidence-archive).
From the owner's rebuild branch strata/rebuild-2026-10.
requirements.txt keeps the runtime packages only; requirements-build.txt pins
cmake and ninja. setup.cmake_build installs only the helper that is missing, right
before a local engine or image-encoder build, so the ready-made engine path (and
serving) never runs pip for build tools. ram_gb() reads hw.memsize on macOS and no
longer crashes without /proc/meminfo. docs/INSTALL.md notes the split; two tests in
tools/test_setup_pins.py cover it.
From the owner's rebuild branch strata/rebuild-2026-10. setup.py merged onto 0.1.45
without conflicts (0.1.45's MIN_ENGINE and pins are unchanged).
New serve/ready.py, serve/owner_guard.py and serve/launch_identity.py; server.py
gains the queue wait deadline (QueueWaitExpired/QueueCancelled), the opt-in
STRATA_BATCH_TELEMETRY plumbing (BSTAT/PSTAT parsed off the engine's token stream,
dropped from request output), the engine stage-plan reader, a main(argv, ...)
entry the terminal can call, and per-replica selection hooks; telemetry.py reports
GPU names; the tests for all of it.
server.py merged by hand onto 0.1.45: one conflict, in StrataEngine._take_control, where
0.1.45's STRATA_ADMIT_SHORT_FIRST ordering and the rebuild's queue deadline both edit the
acquire step; both are kept (ordering check first, then the deadline-bounded acquire).
Every line 0.1.45 added to server.py (the #2518 pick_slot fix, the havok multi-user
switches, the Qwen3.6 parallel/vision work) is still present.
From the owner's rebuild branch strata/rebuild-2026-10.
include/strata/core/batch_telemetry.hpp (BatchWindowSampler, StageInFlightSampler),
its unit test, and the hooks in the generate.cpp batch loops and the --batch-groups
pipelined loop. Off unless STRATA_BATCH_TELEMETRY is exactly "1": no new output, no
new state beyond two disabled samplers, so the default output is unchanged.
Host-side C++ only (no CUDA/HIP/SYCL code), so it builds on every backend.
From the owner's rebuild branch strata/rebuild-2026-10. generate.cpp and
CMakeLists.txt merged onto 0.1.45 without conflicts.
The command-line and TUI client with profiles, doctor, monitor, serve, calibrate,
bench, the runtime owner, component registry and offline policy, plus tests/terminal,
the component/offline tests, the smoke tools, the terminal CI workflows and the
terminal docs. Tuning and Time-skip modules of strata_control follow in their own
commits (strata_cli/cli.py imports them lazily).
From the owner's rebuild branch strata/rebuild-2026-10 (1e4171e2, based on 296b59d2).
README.md and tools/calibrate.py applied as small 3-way patches.
Keep node_modules, Tauri target/, DMGs, the Node sidecar, release archives, the
built App Host dist/ and the Mac evidence screenshots out of git. The two
.terminal-*venv entries are from the owner's rebuild branch strata/rebuild-2026-10;
the rest are new for the clean integration branch.
The SYCL build of the sm_75 series failed with 'out-of-line definition of bf16 does not match any declaration'
(sycl/src/prefill/gemm.dp.cpp); the four gr_* writers would have failed at link. Gemm::bf16_converts_x() answers false.
Ported from c3c's #1581 (the native-IQ and VRAM-loan parts are not in this commit). Default unchanged: Turing keeps returning false for q4_0.
Co-authored-by: c3c <[email protected]>
The v1 kernel keeps its exact previous code (and 37.9 KB of shared memory) unless the flag is set. launch_i8 picks the Turing
single-stage layout by the (emulated) compute capability, so the kernel and the launch agree under STRATA_EMULATE_CC.
No sm_75 card is available here to confirm the series, so the kernels that were on by default for compute capability 7.5
in the submitted PRs now follow one switch, include/strata/core/sm75_series.hpp. Each keeps its own variable, which
wins in both directions. The one-read prompt top-k (#2093) was on for every CUDA card as submitted; it is now Turing only
like the rest. STRATA_EMULATE_CC=75 counts as a Turing card, so a 75-virtual build on an sm_86 GPU runs the series for tests.
block_scores_thread_kernel gave a lane one (query, head) and the whole
128-value query head; ncu on a CMP 50HX showed it bound by the MIO pipe
(mio_throttle first): one shared-memory key load per 4 FMAs. Lane 0's
xor-shuffle tree splits into four independent quarters - c[j] sums groups j,
j+16, j+8, j+24, j+4, j+20, j+12, j+28 - which only meet at the end
(c0 + c2, c1 + c3, then their sum). Now four lanes share a query, lane j
holds quarter j of all four heads, a key load feeds four heads (16 FMAs),
and two xor shuffles finish the same tree. The same pairs in the same order,
so the same bits.
qsa_scores_parity on both cards: all 141 M scores identical to
block_scores_kernel; per call 1,024 queries at 140K 7.76 -> 6.17 ms (-20 %),
at 200K 10.78 -> 8.54 ms (-21 %), 2,048 queries at 40K 12.60 -> 11.57 ms.
agentbench 34K -> 190K: AGENT GATE PASS on the rerun (the first run diverged
at turn 2 - decode's known run-to-run variation; the rerun of the same binary
matched all 36 turns), incremental read -0.6 % median, about -1 % from 150K
up, decode unchanged.
On cards without the TF32 scorer (sm_75) the prompt path scored every
(query, key block) with a whole warp: 20 shuffles and five 512-byte loads per
score. block_scores_thread_kernel gives each lane one (query, head), keeps the
128-float query head in registers, reads each key once from shared memory as
a broadcast, and adds the 32 four-value partials in the tree lane 0 of the
xor-shuffle sum computes. Float addition is commutative, so the same pairs in
the same tree give the same bits; the heads are summed 0..3 from 0.0f as
before; the tail block keeps block_scores_tail_kernel. STRATA_SCORES_THREAD=0
selects the old kernel.
Measured on 2x CMP 50HX (TU102, sm_75, unlocked to RTX 2080 clocks):
- qsa_scores_parity (new): 141 M scores bitwise identical on both cards
(normal values, wide magnitudes with zeros, small integers with ties);
per call 4.6-5.2x faster, e.g. 1,024 queries at 200K: 52.4 -> 10.8 ms,
at 140K: 35.9 -> 7.8 ms. 199 registers, no spills.
- nsys, one +4.4K-token turn at 140K: block_scores_kernel was the top prompt
kernel at 1,045 ms per card (17 %).
- agentbench, IQ3_XXS, layer split, 262144 context, 34K -> 190K (with the
tokenizer cache): engine read per turn at 190K 9.25 -> 6.96 s (476 -> 632
tok/s, +33 %), time to first token 10.49 -> 7.13 s (-32 %); median
incremental read 576 -> 682 tok/s; decode unchanged (73.3 tok/s).
AGENT GATE: all 36 turns word-for-word identical to the base run.
The Turing (v1) prompt attention kernel used 37.9 KB of shared memory per
128-thread block, so one block ran per SM (4 warps, 12.5 % of the warps an
SM can hold), and each 32-cell chunk gathered its scattered K and V rows
after the previous chunk had finished: ncu on a CMP 50HX showed
long_scoreboard (global load latency) as the top stall. For int8 K/V
(--kv int8, mode 1):
- the next chunk's K/V rows and scales are loaded into registers while the
current chunk computes (Turing has no cp.async), then stored where the
gather stored them;
- K is read only by the scores and V only by p.v, so they share one buffer:
V is written over K after the scores' barrier (the softmax's barrier
orders it before p.v). 37.9 -> 29.2 KB: two blocks per SM.
The same bytes in the same places and the same arithmetic, so the same bits.
Other KV modes keep their code. qsa_prompt_attn_parity now prints a checksum
of the new kernel's output, so two builds compare bitwise.
Measured on a CMP 50HX (sm_75), qsa_prompt_attn_parity, 2,048 queries:
int8 21.45 -> 12.91 ms at 40K (-40 %), 21.94 -> 13.31 ms at 140K (-39 %);
all output checksums (int8 and fp16, 4K/40K/140K) identical to the previous
kernel. agentbench 34K -> 190K (IQ3_XXS, layer split, 2x CMP 50HX): AGENT
GATE PASS (36 turns identical), incremental prompt read time -3.5 % (median
per-turn ratio 0.965), decode unchanged.
With --pcie-frac 0 the pool never plans PCIe experts, yet every layer of a
verify window still runs the flag B wait, the staging copy and the grouped
launch over zero groups: six kernels, ~18 us a layer on an RTX 2080 Ti
(kernel-node nsys). STRATA_NO_PCIE_GROUP=1 records the window graph without
them; a plan with PCIe experts then stops the engine with a message.
Engine A/B on an RTX 2080 Ti (Flash-Next Q2_0, 512 tokens, two runs each, with the open sm_75
decode PRs applied): Spanish chat 87.8 -> 89.8 t/s, code 106.1 -> 108.5,
32K 78.4 -> 80.1, 128K 75.0 -> 76.8, 390K 74.7 -> 75.1.
Two kernel pairs of a verify window's layer run as one launch on Turing,
every byte as before:
- the plan's host-flag wait folded into its copy (143-amdk's
wait_copy_i32_from_mapped, gfx12-only until now). STRATA_FUSE_WAIT on
CUDA: 2 = the plan only (Turing default), 1 = also the CPU rows'
copy_rows_from_mapped_after, which makes every row block poll the flag
over PCIe and was -1.5 % on an RTX 2080 Ti, 0 = neither.
- native_quantize_q8_1_il: a projection input's q8_1 blocks and their
interleaved copy (native_q8_1_interleave) in one kernel; an int of the
copy is four codes gathered with two shuffles. Used for the GDN qkv and
out-projection inputs and the QSA input and out-projection. STRATA_QIL=0 / 1
forces it off / on (Turing default on).
RTX 2080 Ti, Flash-Next Q2_0, 512 tokens, two runs each: the plan fold
+0.6..+1.4 % decode, the fused quantize another +0.5..+1 % (code 108.0 ->
109.1 t/s). The fused quantize writes the same bytes as quantize +
interleave at 2560/6144/10240 inputs and 2-4 columns.
The exact layout gives each Q2_0 row four warps, a half block per thread and a
cross-warp reduction. At the engine's 2560 inputs (40 blocks a row) 48 of the
128 threads have no chunk and the reduction dominates: the QSA attn_q
projection (2560x12288) at three columns took 80 us (~110 GB/s) on an
RTX 2080 Ti.
q2_0_warp_rows_kernel: lane l runs the chunks of the exact kernel's threads
l, 32+l, 64+l and 96+l in their loop order, adds the four sums as warp 0 adds
the other warps' partials, then the same warp_sum - every output is bit for
bit native_q2_0_mmvq_kernel's (one column) and launch_multi's exact layout
(two to four). 1/2/3/4 columns: 36.6/60/68/88 -> 24.8/31/31/37 us.
Default on sm_75 only; STRATA_Q2_WARP=0 keeps the old kernels, =1 takes this
one on any CUDA card.
attn_chunk_kernel<1> saturates the SM's shared-memory pipe on Turing: its
time grew linearly with the queries while the SMs were half empty (two
16-byte q reads per head per cell with a two-way bank conflict, and 12
warp_sums = 60 shuffles per cell).
attn_chunk_q8_v3_kernel computes the same operations in the same order:
- the lane's slice of the 12 query heads is read into registers once;
- the 12 lane sums are reduced with reduce12_gfx906 (16 shuffles, every
sum warp_sum's pairs);
- the probabilities are stored cell-major, so the value loop reads a
cell's 12 as three 16-byte broadcasts.
RTX 2080 Ti, with the merge: 2051 selected cells, 1-4 queries
51.6/78.5/110/146 -> 34.5/41.0/73.0/84.9 us; 600 cells
45.9/74.6/105/137 -> 28.6/32.8/33.5/33.3 us. qsa_prompt_attn_parity
reports the same errors with STRATA_ATTN_V3=0 and 1.
Default on sm_75 only; STRATA_ATTN_V3=0 keeps the default kernel, =1 takes
this one on any CUDA card.
The fused prompt-expert kernels (moe_fused.cu for the Strata Q2_0 pack,
moe_fused_iq.cu native_kernel for the native packs) on sm_75, with
STRATA_PF_FUSED=1 STRATA_PF_FUSED_SM75=1:
- cp.async -> plain 16-byte copies (the barriers around them stay),
prefetch.global.L2 kept;
- mma m16n8k32 / m16n8k16 s8 -> four / two m8n8k16 (exact int32 sums,
the same d as the sm_80 instructions), MAGIC as the C operand;
- two activation stages on Turing (64 KB of shared memory);
- native_kernel takes Q2_0 gate/up (64-value blocks, a block per stage),
so the canonical Q2_0 pack (Q2_0 / Q2_0 layers) runs fused; on Turing
only that pair is set up;
- Turing, Q2_0: the two halves of a stage share the weight scale, so a
row's dot is d (dx_0 dot_0 + dx_1 dot_1) - 11 % fewer instructions.
RTX 2080 Ti, one layer, 512 experts top 10 (prefill_fused_iq_test,
Q2_0 / Q2_0 pair added): 2048 tokens MMQ 12.98 ms -> fused 6.59 ms,
16384 tokens 55.08 -> 37.16 ms; fused vs MMQ rel RMS 2e-5, the same
error against the FP32 reference. In the engine (canonical Q2_0 pack,
98K prompt, 16K chunks): prefill 58.3 -> 53.3 s; layer 0 expert rows vs
MMQ: median rel 1.6e-7. Teacher-forced 2048 tokens at P = 16K / 32K /
64K: perplexity vs MMQ +3.4 % / -4.5 % / +0.6 % (FP16 path vs MMQ
+0.7 % / +0.1 % / +0.3 %) - trajectory noise, no systematic shift; the
verify phase (same tokens) 42.36 s MMQ, 42.28 s fused.
A borrow copies the donor's checkpoints up to the match, so a match that reached a pinned shared prefix gave the borrower the pin too. Such a match now takes the conversation as before. The borrow's guard is guard()/unguard()/guard_blocked(), and the refusal before evicting counts the pinned conversations along with the donor.
Rebased by hand onto 0.1.44.1 (evict_oldest() honours the guarded donor).
Co-authored-by: BlueKingMuch <[email protected]>
Sibling subagents with the same system prompt and tools no longer cost each other their history: only the K/V up to the checkpoint and the checkpoint itself are copied into the session (conversation_snapshot_restore_prefix); the parked conversation stays where it is and is pinned while the outgoing one parks. A match on a parked conversation's live state or newest checkpoint is that conversation going on and is taken as before.
Rebased onto 0.1.44.1 by hand: the pinned shared prefix (#879) and evict_oldest() now also skip the borrowed donor, serials follow every erase, and park-gain (#2265) and the pin= sibling rule gate the outgoing park.
Co-authored-by: BlueKingMuch <[email protected]>
A client renders the history again for its next turn, without the reply the slot decoded or the assistant header,
so the slot's held stream (prompt + reply) is no start of the new prompt and pick_slot fell back to the least
recently used slot. With two chats in two slots and two next turns arriving together, whichever was read first
could take the other chat's slot, and the engine's slot copy then overwrote the cache the second turn needed
(0 tokens reused, a full prompt read). pick_slot now also counts the held tokens up to the last <|im_start|>
(where the engine takes its turn checkpoint) when the prompt agrees with them, and a pipelined layer split with one
slot per group (--batch 2 on two cards) no longer ignores what its slots hold.
Least recent use keeps the newest checkpoints and every turn adds one, so after a few short turns all 6 sit in the
last 1-2K tokens. An agent client that rewrites its history a few thousand tokens back then resumes from the system
prompt: on 2x CMP 50HX with the pi coding agent a 210,528-token prompt that was the same as the session for 204,349
tokens resumed at 9,525 and read 201,003 again (100 s).
With the flag the victim is the checkpoint whose removal leaves the smallest gap for its distance from the newest, so
the kept points thin out geometrically: after 200 turns of 1,000 tokens they sit 0, 1K, 3K, 8K, 43K and 200K before
the end, and an edit 6K back reads 8K again (least recent use: 200K), 42K back 43K. The same number of checkpoints, no
more RAM; never the root, the newest or a pinned one, the tail rule first, the plain policy without a candidate. The
resume is the usual exact-prefix match.
On the box: an 82,593-token conversation grown turn by turn, a turn ~6 steps back rewritten, resumed at 73,610 tokens
in 10.2 s against 40.0 s read in full; with the expert placement fixed (--adapt-swaps 0, --pcie-frac 0.17) the
greedy answer was the same text as the full read. conv_cache_test: the spread cases (a 200-turn simulation against
least recent use, the fallbacks). docs/DETAILS.md: a paragraph after the prompt cache's options.
A conversation that only advanced a few hundred tokens since its last park does not need a fresh 3 GB copy: the entry it would supersede still holds this conversation's prefix, and the next turn drops the copy anyway (drop_superseded). Reported in #2264 over 1,418 park events on one long single conversation.
The flag is opt-in and defaults to 0, which parks every turn exactly as before. What this adds is a way to ask for the gate without a rebuild: on the reporter's rig it is 3.26 GB instead of 9.79 GB per benchmark, the superseded class disappears, and the decode median moves -3.4 %, inside that arm's 92.2-124.1 tok/s spread.
parked_prefix_tokens() reports exactly the entries drop_superseded() would remove - it finds them the same way, by the deepest checkpoint the outgoing chain still holds - so the gate cannot name a different entry than the park would drop. conversation_cache_test covers it (7 checks; 4,226 pass).
Fixes#2264
A conversation that was restored and is parked again keeps its unchanged K/V
pages and appends the new ones (92a3d5f). The appended segment reserved room
for later turns, growing geometrically up to 16 MiB - per buffer, and a parked
image has one per K/V layer and part (k, v, their scales, the indexer rows, the
draft layer). With Qwen3.8-Flash-Next (IQ3_S, int8 K/V) a 32K-token main agent
parked again took 1.49 GB instead of 0.97 GB, a 17K-token subagent 1.16 GB
instead of 0.74 GB. The cache counts that capacity against its budget, so with
2816 MiB the reserve decides which conversations are evicted.
The reserve is now at most an eighth of what the buffer held before the append
(at least 64 KiB, and at most 16 MiB as before). Small turns still fill the
reserve instead of adding a transfer each: the 4096-append test keeps its
segment bound, and 256 turns of 1 MiB onto a 16 MiB buffer take 28 segments
(17 before).
The docs said parking falls back to a full capture when the reserve would evict
another conversation; that check went in 6cd29d9. With the reserve bounded the
two captures differ by at most an eighth of the K/V, and the docs now say what
the reserve is.
A standing ban of a whole script (65,929 IDs) was normalized five times per request: in _openai, twice in
validate_request and twice in engine_key. About 200 ms of Python per request, whatever the answer length.
A list is now normalized once. The same object again in a request is answered from the last call; an equal list in
a later request is found by its compact JSON text, which tells False from 0 and "1" from 1 (== does not: [[7, False]]
== [[7, 0]]). Only a list that normalized without an error is stored. Lists under 64 entries skip the cache.
No change to what normalize, engine_key and validate_request return or raise.
PromptEncoder keeps the last prompts with their ids and resume points (one per
conversation-cache slot) and encodes only the text after the longest shared
special-token boundary; the ids equal a full encode. Rebased onto 0.1.44.1
(prepare() keeps the new `cancel` argument; forget_prompt on a context refusal;
count_tokens reads without storing). SECURITY.md notes the in-memory prompts.
Based on gputier's #1852 (8 commits) and blange48's #567.
Co-authored-by: Guillaume PUTIER <[email protected]>
Co-authored-by: benoit lange <[email protected]>
Bit-identical (gate: 10/10 + exact_pp 4K/20K IDENTICAL with STRATA_DEQUANT_WIDE=1 on the RTX 3060 and the RTX A4000), and the time
to the first token of a 150 / 300 / 450-token prompt falls 12-20% on the RTX 3060 (5 of 5 pairs each), the prompt read +2.3%.
STRATA_DEQUANT_WIDE=0 turns it off. Arch-gated to the measured sm_86 and sm_61. Follow-up to #2178 (Techannel), the kernels this switches on;
the same stores are what #1525 (sergqwer) proposes for Q8_0 / IQ4_XS.
A build folder configured while the option's default was OFF keeps
STRATA_Q6K_EXPERTS=OFF in its CMakeCache.txt even after the default
flipped to ON (465cd61d): CMake does not rewrite an option already in
the cache, and source_hash() fingerprints the source files but not the
CMake option values, so setup recompiles the engine with the Q6_K
gate/up expert kernels still disabled. A model whose routed experts are
Q4_K/Q6_K (Unsloth UD-Q4_K_XL) then fails to load with:
strata-q36: blk.34.experts are Q4_K/Q6_K, which the native expert
kernels do not take (build with -DSTRATA_Q6K_EXPERTS=ON for Q6_K)
Passing -DSTRATA_Q6K_EXPERTS=ON explicitly overrides the stale cache
entry, so the engine always has the kernels its default build promises.
Same pin in the Dockerfile build for consistency. Not hardware-specific:
it is a CMake cache-staleness issue, independent of GPU/platform.
The output sum of token t is done after token t + 1's first barrier (sk / sq double-buffered), off the recurrence's chain. Same arithmetic: the
3000-token prompt's logits and 8 greedy steps are bit-identical to the base engine at chunk 512 and 2048 (R9700: 913 -> 885 ms, 607 -> 573 ms).
With many slots on a small card (8 sessions on a 12 GB RTX 3060, UD-Q6_K_XL) the sessions left too little VRAM for the hybrid
mode's staging pair, CPU-output buffer and expert slots, and the start died in a bare cudaMalloc failure. place_experts now adds
up what it is about to allocate and exits 1 with "not enough VRAM left for the hybrid experts' buffers and slots (N MiB needed,
M MiB free ...)" and the way out (fewer slots, a shorter --max-context, fewer --expert-slots). Only a start that would have
crashed changes; the placement and every answer are as before.
tools/qwen35/q36_vram_refusal.py: demanding starts (8 slots, long contexts, every expert as a slot) must reach READY or exit 1
with one of the engine's refusals, never a raw allocation failure.
--pipeline-windows runs its own overlapped decode loop with its own hand-offs, so
STRATA_SPLIT_OVERLAP now switches itself off when it is set (one log line says so).
Rebased onto main 1735d64: the early launch of the next stage keeps #871's graph
variant (the next stage's refresh_ar() runs before its capture and prelaunch, and
not again in its run() when it was prelaunched), resets the plan error word like
run()'s launch, and the chain path checks that word after its sync as well.
Review on #936 (justxiami): the reading stage's wait_flag_ge spun with no bound,
so a lost publish would leave that stream spinning.
- wait_handoff: the reading stage's graph now waits with a bound (default 30 s,
STRATA_SPLIT_WAIT_MS, 0 = unbounded, GPU clock as in gpu_stamp). On timeout it
sets a mapped error word and returns, so the graph completes; the host checks
the word after the window and fails it with an error that names the setting,
instead of using a stale hand-off.
- The host's per-layer wait (20 s, #267) still normally ends a stalled window
first; release_gpu_waits already raises the hand-in flag.
- Test hook STRATA_TEST_HANDOFF_DROP=N: the first stage's N-th window withholds
its flag (a lost publish), to exercise both paths.
Only active with STRATA_SPLIT_OVERLAP=1; the default path is unchanged.
Each verify window of a layer split runs its stages strictly in turn: stage 1 finishes, the host syncs its stream,
then stages and launches stage 2's graph. Under HIP the sync and the graph launch both sit on the critical path of
every window.
With STRATA_SPLIT_OVERLAP=1 the hand-off carries a ready word (mapped, one per hand-off buffer):
- the writing stage's graph ends with handoff_publish: the payload with volatile stores (RDNA keeps plain stores to
mapped memory in L2), a system fence per thread, and the last block raises the flag;
- the reading stage's graph starts with wait_flag_ge(flag, 1);
- Verifier::run captures the next stage's window on this thread, then stages and launches it on a helper thread
(prelaunch) while this thread serves its own layers; it then serves the next stage's layers directly and syncs
the earlier stage once the chain is done;
- release_gpu_waits also raises the hand-in flag; an early return after the prelaunch releases the next stage.
Batch windows and --split-device 0 keep the serial order. Off by default.
Measured on 2x RX 7900 XTX (ROCm, Linux), Swift 1.5 IQ3_XXS, 512-token greedy answers, median of 8:
decode 53 -> 70.1 tok/s (code prompt), 45 -> 55.9 tok/s (prose prompt); greedy output byte-identical with the
flag at 0 and 1; quiz 47/51 both ways. Not measured on NVIDIA.
Split-overlap launch merged with 0.1.44 HostGraphLaunch (STRATA_VERIFY_ASYNC_LAUNCH): the launcher object is
kept in an optional so an early-launched window has none.
Co-authored-by: bsvinay <[email protected]>
Squash of SuperDistance's three commits of PR #2050 (decode between the pieces of a read on the pipelined groups, the
piece lines counting the rows the slots decoded meanwhile, the stall watchdog staying on for a read in pieces),
applied after #2326 here: the BYIELD give-way is #2326's give_way lambda, the concurrent block runs first and the
piece path serves a read that STRATA_PIPED_CONCURRENT leaves to it (pictures, configurations where it is off).
Co-authored-by: SuperDistance <[email protected]>
On Linux a thread inherits its creator's CPU mask, and the host thread is pinned to the one core the pool leaves it
(aux_cpus.hpp). The read's thread and the doorbell thread were created from it, so they shared that core with the host
thread that pumps the windows, and every window waited there for the doorbell of each layer: 6.1 rows/s during a read
on 4x RTX 5080, 4 stages (blange48 on #2326). Windows places the host with CPU Sets, which new threads do not inherit,
so it did not show there. Both threads now move off the host's core and the pool workers' CPUs (on an SMT machine with
the default pool: the idle sibling threads; without such CPUs, every CPU but the host's), and the threads that already
run on the host's CPU (the prompt path's copy threads, started by the first prompt) move the same way at a read.
--aux-cpus places them all when it is on. STRATA_PIPED_CONCURRENT_UNPIN=0 keeps the inherited CPUs (an A/B).
Measured on Linux (WSL2, Ubuntu 24.04, 2x RTX 5090, 4 streams, a cold 32K prompt; same binary, UNPIN=0 / default): the
slots decoded 25.9 and 23.7 rows/s during the read with the threads kept on the host's CPU (76-81 threads there), and
75.6 and 62.8 with this change; with the n-gram table in RAM (WSL's disk reads it 7x slower than Windows) 110.7 rows/s,
as on Windows (107.0). piped_read_join_bench.py (cold32k, followup, two_joiners; table in RAM, 2-3 runs each): 24-37
rows/s during the 30K+ reads kept on the host's CPU, 95-127 with this change, and 925-1,065 stream tokens during the
cold 32K join against 454-484. On Windows, STRATA_TEST_CONC_ON_HOST_CPU=1 (a test) puts both threads on the host
thread's CPU set first: 18.6 rows/s kept there, 105.4 with this change.
The flag works only through a helper cache (--expert-cache-device1..3), and on a plain layer split it is inert
(MULTI_GPU.md, #1447), so the check looks at the helper caches themselves. #2275's rig runs a plain split with the
flag set, and the mode would have stayed off there for a cache it does not have.
The opt-in's section: how the read and the windows share the GPUs, the doorbell thread, the give-way at a chunk, where
the mode is off. Measured against main (a0116b27) on 2x RTX 5090 in one window (main, this, this, main): the stops,
the streams' rate during the reads with the same answers every run, the joining prompts' TTFT, a question during a
read, the same randomised load on both, the doorbell thread's first wait as an A/B; 16 GB cards on each side; a weaker
host as well in #1906's shape; the default path's check; text equality.
tools/piped_read_join_bench.py, the client that measured it, is #2050's, unchanged (the same file as there).
From a review of the concurrent read:
- With external serving batch_poll still finds a window that will not finish (its graph ended or failed with a layer
to ring, or no layer rang for 20 s): the served count is read before the stream query and the ring count after it,
at most one query a millisecond; the last ring's time is an atomic. batch_serve reads a window's row count after its
served count, so a window launched while the doorbell thread was descheduled is not served with the last one's rows.
- #1996's pinned upload rings (0.1.43.1) grew a slot at its first larger upload, a pinned free and allocation on the
read's thread: the drafter's records and the prompt path's routing tables reserve their slots before the read
(PinnedRing::reserve, Prefill::reserve_uploads).
- Off, said once, where serving a doorbell makes a CUDA call (--pcie-mode dma, STRATA_FS_SLOTS, the helper caches) or
the expert source recycles the buffers it hands out (the GGUF in place, unbuffered, STRATA_IO_PREFETCH); a prompt
with pictures is read the usual way. The expert sources' read counters are atomics.
The check is a pure function, concurrent_read_off (batch_read.hpp), with a test in batch_read_test.
- pump's all-idle branch turned the stall watchdog off when the last slot finished mid-read; it stays on while a prompt
is read until the request ends.
- Between windows the doorbell thread slept 50 us, a whole timer tick on Windows (up to 15.6 ms): it now waits on a
condition each launch notifies (STRATA_TEST_BELL_SLEEP_US=N, a test, restores the sleep for an A/B). With nothing
decoding the main thread waits for the read's end or a line instead of sleeping. A pool failure the doorbell thread
set is reported before pump clears the flag for the next window.
- The read's log line ends with the slots' rate and, per stage, its windows and the host time that set each up. The
priority stream's cudaGetLastError runs only under the opt-in.
With a layer split and --batch-groups > 1 a prompt read is one sp.run, so every decoding slot stops for the whole read
(#1906); #2050's pieces bound the stop but drain the pipeline at each piece. With STRATA_PIPED_CONCURRENT=1 the read
runs on a worker thread while the main thread keeps the groups going through the pipeline (pump) and takes BSTOP and
BYIELD meanwhile. The prompt's K/V goes to the main sessions and the slots' to their own; the windows' streams get the
device's highest priority on CUDA (STRATA_PIPED_CONCURRENT_PRIO=0 keeps the default).
- The slots' doorbells are served on a thread of their own that makes no CUDA call (Verifier::batch_serve, now
batch_poll's per-layer service too, and set_external_serve). Two hangs found while building this, read from the
stall dumps against the PDB, had #31's shape: the main thread waited for the driver's lock in cudaSetDevice while its
holder waited for the GPU (a cudaFree in the drafter's prompt pass; cuBLASLt's lazy load of a GEMM module), and a
window on that GPU waited for its doorbell.
- MtpDrafter::reserve_prefill captures the drafter's prompt graphs and sizes its records before the read.
- Prefill::stop_at_chunk: a BYIELD ends the run at its next chunk, also with no slot decoding; the pipeline is drained
there and the part read gives way as #656 does. A read nobody waits for is not cut.
- The reader is joined on every way out (an exception too). Off beside STRATA_PREFILL_HELP and STRATA_SPLIT_MTP_BATCH,
which allocate during a read.
The prompt path gathers a chunk's PLE rows on its own thread and the batch windows gather theirs on the main thread,
from the same table (ss.ple.table). Today they never overlap - a prompt run's read-ahead ends with the run, and the
windows run between runs - but a prompt read beside running windows would race on the reader's tickets and the
prefetch slots. Every public method that touches them now holds a recursive mutex (gather holds it across its issue
and collect). Uncontended it is a few nanoseconds per call; nothing else changes.
Squash of ruibeikaa's six commits of PR #1441 (the first fixed b/a, then runtime-learned b/a, then calibrated b/a, the
segment planned once, --prefill-pipe 1 following STRATA_PREFILL_EQUAL), applied onto 0.1.44.1 with the conflicts in
generate.cpp (Options, BSlot, the lend's chunk with --prefill-head-alias) resolved.
Co-authored-by: ruibeikaa <[email protected]>
A bigger chunk is only taken when the experts, the 0.77 GiB reserve and the chunk's buffers (0.5 MiB per token) still fit without giving the packed
decode copies back: 131072 cells -> 512, 100000 -> 2048, 32768 with --batch 4 -> 1024, --batch 8 -> 512 (hybrid as before).
Prompt chunk (owner decision): with every expert in VRAM the auto rule picks 4096, 2048 or 1024 (the chunk's buffers cost ~0.5 MiB per token,
the sessions' caches 45 KB per cell and session; 1024 at most with --batch N so the other slots' windows are not held for a whole chunk), else 512.
The hybrid mode keeps 2048 and falls back to 1024 / 512 on a card with little room. STRATA_Q36_CHUNK=512 or --prefill 512 gives the old size.
R9700 pp4096: 512 3390, 1024 4495, 2048 5353, 4096 5750 tok/s. Not bit-identical to 512 (other GEMM tiles): see docs/QWEN36.md for the KL.
Decode batches of up to 8 rows take the router and the shared expert's gate in one product (rows of 257), as a single token already did.
k_conv_seq's tile mode switched off the history write for batch rows too (grid y is the group count there): fixed.
k_gdn_seq_cols started every token with a chain of global loads; they are issued one token ahead now. Same arithmetic: the 3000-token
prompt's logits and the next 8 greedy steps are bit-identical to the previous engine at chunk 512 and 2048 (R9700: prompt 931 -> 902 ms
at chunk 512 together with the tiled conv, 616 -> 591 ms at 2048).
q36_kernels.cuh: one warp per row for the shared expert's and attention k/v Q8_0 products (grid capped at 1024 warps: 2048 rows as 2048
warps took 21 us, 5 us as 1024), shared gate+up+SwiGLU+q8_1 in one launch, k_moe_out with 1024 threads and the next layer's q8_1 image, k_add_rms
with the q8_1 image, router top-k and expert grouping in one launch. CUDA: the division / exp the --use_fast_math TUs they mirror use are spelled out.
Engine: prompt GDN conv in tiles of 16 tokens side by side (same arithmetic per token), --draft-vocab N (the MTP draft head over the first N rows only,
a draft proposes and the verify window decides: output unchanged), the packed Q8_0 default on Windows too (it was Linux only), STRATA_Q36_OLD_KERNELS=1
switches all of it off.
R9700, UD-Q4_K_XL: decode 82.5 -> 120.6 tok/s (greedy 10 prompts identical), --mtp 2 160 -> 174.
For that geometry only: the s27 gate/up kernel and the fused SwiGLU/q8_1, then the old down kernel (the new down kernel is slower at n_ff 512).
Flash-Next's shapes (2560/640) take the same paths as before; the path is still STRATA_EXPERT_V2K (default on gfx12).
The same kernel measured on an R9700 (gfx1201): qsa_scores_gfx1100_parity --full compares 223,664,777 scores and 11,895,800 ids bitwise equal
to the warp scorer (35 cases), the scorer stage with 256 queries is 3.6x faster at 131K and 4.0x at 240K, and a fresh prompt reads 8.7% faster at
131K tokens (4 of 4 pairs) and 16.4% faster at 230K (3 of 3), with identical answer tokens and byte-identical exact_pp residual dumps at 70K and 131K
tokens. So on gfx12 it is on for batches of >= 64 queries with >= 16,385 live blocks (65K cells and more) unless STRATA_SCORES_THREAD=0; gfx1100
stays the author's opt-in (STRATA_SCORES_THREAD=1). A one-time stderr line says when it engages by default.
The persistent read's barrier timeout uses __builtin_amdgcn_s_memrealtime, which gfx11 and gfx12 do not have (clang: "needs target feature
s-memrealtime"): the PR as ported failed to build for gfx1100, gfx1151 and gfx1200/gfx1201. On those parts the barrier compiles to an immediate
abort, fused_gr_persist_init refuses the read with a start-up line, and the default read is used. An R9700 (gfx1201) measured the read +0.7% / +1.0%
per window slower than the default in the PR thread, so nothing is lost. The silent, permanent barrier abort at run time that the same thread
describes is noted in docs/AMD_HIP.md.
The verifier reads the hyper-connection twice per layer at 1-8 tokens: three small launches with two global
dependencies, the down projection on 41 blocks. Opt-in STRATA_HC_PERSIST=1 runs it as one kernel of 80 blocks with two
grid barriers on an atomic counter: the down projection split by column slices over all 80 blocks, a distributed
reduction, then the up projection and the same epilogue as gr_up_kernel.
gfx1030 (2x RX 6900 XT, layer split): the read 1.35-2.10x at 1-8 tokens; ms per verify window -4.2 % at 4K and
-2.8 % at 32K (one binary, the variable unset / set, 6 alternating rounds, every round the same way). Another summation order (not bitwise); against a
double-accumulation reference (STRATA_HC_PERSIST=3) as close as the default read.
Residency is decided by running every token count once at start-up with a 50 ms barrier timeout and comparing with
the default read; a timeout keeps the default read for that token count and larger ones, wave64 cards are refused.
One line per card says what was decided. Only the verifier's reads use it (one stream per card); the drafter's reads,
the prompt path, CUDA and SYCL are unchanged (CUDA and SYCL get two empty stubs). With the variable unset both entry
points return in their first line: the output is bitwise the same as main.
Measurements, the simulated smaller cards and a stand-alone timing tool:
bench/results/2026-10-12-hc-persist-gfx1030/.
block_topk_wide_kernel is plain shared-memory / atomicAdd code with no sm_90
feature, but its definition and the dispatch branch sat behind
#if !defined(__HIPCC__), so on AMD a context past the register kernel's reach
(about 135K cells) always fell back to the 4-pass ref radix. The ids are
identical by construction (the same radix rule), which the bench states:
256/256 at 256 queries x 65537 blocks, 1/1 at nq=1.
Measured on a Ryzen AI Max+ 395 (gfx1151, ROCm 10.1), qsa_select_bench:
- nq=1, 131073 blocks (a decode window at 512K context): 2.24 -> 0.77 ms (2.9x)
- nq=256, 65537 blocks (a prompt batch at 262K): 3.16 -> 1.43 ms (2.2x)
End to end this moved the deep-context decode of a 512K-token prompt from
19-22 to 24-26 tok/s and the 512K prefill from 473 to 634 tok/s on that PC
(together with the split scorer of another change). STRATA_TOPK_STREAM=0
still restores the register kernel.
Upstream aa7b0cb gates PcieAuto to CUDA single-GPU (#if !defined(STRATA_USE_HIP)); the
machinery itself is engine-generic (pool ms per missed expert + miss counts vs the probed
link rate), and this build's start-up H2D probe is already best-of-4 with a +-0.7% spread,
so the HIP exclusion was unmeasured-only, the same class as the ROUTE_TAIL_SKIP gate.
Same opt-outs as CUDA: layer splits excluded, STRATA_PCIE_FRAC_DEFAULT=old restores the
fixed start-up rule. Provenance: MistVVK/XeStrata commits 05309096/055cf9b0/c71cac34/
5952e8e7 (their measurement-stabilization cluster) demonstrated the failure mode and the
fix class; their exact subsystem is not portable (it is their own engine code).
(cherry picked from commit acb3deb0522a8fc351a4047d2bd11b8ab7a73dca)
Co-authored-by: Zac Chen <[email protected]>
The engine turns 18 exact (bit-identical) switches on by default on gfx1151
(src/core/arch_defaults.cpp) but keeps gfx1150 out of its arch defaults
(src/core/arch_defaults_test.cpp asserts arch_default_env("gfx1150").empty()).
A gfx1150 install gets the 17 that apply here - the gfx1151 set less
STRATA_HCD_EXACT, which copies the gfx1151 hipBLASLt table's solutions 1176 / 1177
and so does not fit beside a gfx1150 table - written into the config's "env"
beside STRATA_HIPBLASLT_TUNING. The Radeon 890M report (docs/AMD_HIP.md;
bench/results/2026-10-07-community-gfx1150) measured the same greedy answers with
all 17 on and with all 17 off, decode +2% and prompts +3-6%.
The set is setup's own (SETUP_ENV): a re-run refreshes it, and it does not linger
after the opt-in is turned off. A new test (tools/test_setup_strix_halo.py,
Gfx1150Switches) keeps GFX1150_SWITCHES equal to the gfx1151 arch-default table
minus STRATA_HCD_EXACT, keeps the engine's own default path for gfx1150 empty, and
checks the SETUP_ENV wiring. Setup's suites pass (strix_halo 40, amd 36, choices 47).
Co-authored-by: onionbutties <[email protected]>
STRATA_EXPERIMENTAL_GFX1150=1 takes the Radeon 890M / 880M iGPU (Ryzen AI 300,
Strix Point, gfx1150) as a unified-memory card like Strix Halo: it joins AMD_ARCHS
and the UMA handling (new is_uma_apu helper), with notes of its own. The engine
already builds and runs for gfx1150 (0.1.40.2, #1217; tools/hip/gfx1150-hipblaslt-100401.txt
is calibrated on it); no release ships a ready-made Windows engine for it yet, so
a Windows user builds one (STRATA_HIP_ARCHS=gfx1150 tools\hip\build_windows.bat) and
starts setup with --prebuilt dist\. Unset, the default path is unchanged: the test
that asserts gfx1150 is not in AMD_ARCHS (tools/test_setup_strix_halo.py) still passes.
Co-authored-by: onionbutties <[email protected]>
The engine turns 18 exact (bit-identical) switches on by default on gfx1151 (src/core/arch_defaults.cpp) but keeps
gfx1103 out of its arch defaults (src/core/arch_defaults_test.cpp asserts arch_default_env("gfx1103").empty()). A
gfx1103 install gets the 17 that apply here - the gfx1151 set less STRATA_HCD_EXACT, which copies the gfx1151
hipBLASLt table's solutions 1176 / 1177 and so does not fit beside a gfx1103 table - written into the config's
"env" beside STRATA_HIPBLASLT_TUNING. One 780M (Ryzen 7 8745HS, 60 GB, GTT 50 GiB, ROCm 10.1, Q2_0, 4K prompt)
measured the same greedy token ids with the 17 on and with them off, +13% to +15% prompt and decode +3.2% (the
decode half alone); bench/results/2026-10-12-community-gfx1103.
The set is setup's own (SETUP_ENV): a re-run refreshes it, and it does not linger after the opt-in is turned off.
The gfx1103 note now names the native dot4 / WMMA kernels and docs/RADEON_780M.md, which documents the opt-in
build, the table and the measured numbers. Setup's suites pass (strix_halo, rocm_pins, amd, choices: 143 tests).
Refs #1423.
Co-authored-by: AlexisCarbonell <[email protected]>
Calibrated on a Radeon 890M (Strix Point, gfx1150) with
tools/hip/tune_hipblaslt --workspace-mib 32 over the 16 dense GEMM geometries of
the shipped gfx1150-hipblaslt-100401.txt at T = 64, 128, ..., 16384: two passes,
each row keeping the solution with the lowest mean of all six repetitions. The
three rows slower than plain hipBLAS (the tiny bf16 N=1 K=2560 shape at T = 64,
128 and 256) are left out, so those buckets route to the nearest good row. Every
remaining row beats plain hipBLAS: 1.07x to 16.7x per GEMM, geometric mean 5.0x.
The solution IDs are scoped to hipBLASLt 1.5.0 (version 100500, the ROCm
10.2.0a20260930 nightly the engine here was built with).
The Radeon 780M (gfx1103, RDNA3, 12 CUs) builds and runs (issue #1423), but had no hipBLASLt table, so the prompt's
dense GEMMs ran on plain hipBLAS. This adds tools/hip/gfx1103-hipblaslt-100401.txt, calibrated on one 780M with
ROCm 10.1's hipBLASLt 1.4.1 (version number 100401) over the gfx1150 table's 144 GEMM cases at T = 64..16384 with
the engine's 32 MiB workspace.
Two tune_hipblaslt passes back to back; each row keeps the solution with the lowest median of all six repetitions,
and a row is left out when no candidate beats plain hipBLAS. 143 rows, 1.05-23.30x per GEMM, geometric mean 4.95x;
the one shape left out is bf16 N=1 K=2560 T=64 (best 0.039 ms against 0.024 ms on plain hipBLAS, sub-100 us
timings), where the engine falls back to hipBLASEx.
Measured on the 780M (Ryzen 7 8745HS, 60 GB, GTT 50 GiB, Q2_0 + MTP, 32K context): a 4K prompt read 90 tok/s on
plain hipBLAS against 187 with the table (medians of an interleaved A/B; with the gfx1103 exact switches and the
WMMA opt-ins, 277 at 4K and 272 at 32K). hip_prefill_hipblaslt_gemm passes with it (launches=4 fallbacks=0) and
hip_prefill_hcd_exact_parity skips, because the table picks no 1176 / 1177 for the HC down GEMM - the gfx1150 table
gets the same skip, which is why STRATA_HCD_EXACT is not in the gfx1103 switch set. Setup installs for gfx1103 only
with STRATA_EXPERIMENTAL_GFX1103=1 and then picks the table up when the installed hipBLASLt reports 1.4.1.
Docs, the community report (runs.csv, tune-summary.csv, the server config, the engine's start-up lines) and the
setup tables list carry the numbers. Refs #1423.
The switch exists for CUDA (#1372) and is compiled out on HIP. On gfx11 it now runs the WY form of the gated delta rule in
chunks of 16 tokens, every product one 16x16x16 WMMA (FP16 operands, FP32 accumulation, the state in the accumulators), with the
output norm and the z gate fused: a prep kernel for T and P of every chunk, then a scan with a block per value head.
It rounds differently from the FP32 recurrence, so it is opt-in; the default path is unchanged. A gfx1100, gfx1101, gfx1102, gfx1150 or gfx1151 part, 64 tokens and
more, an output row stride divisible by 8 and no STRATA_GDN_REC_HEADS take it; anything else runs the existing kernels.
tests/hip/gdn_chunked.cpp: bounds on the FP16 output and the carried state against the existing recurrence over 0-16,385 tokens
(chunk and stride edges, a split request), a double-precision CPU check, and bit-identical output with the switch off or unset.
tests/hip/gdn_rec_head.cpp unsets the switch, as its variants 0 and 1 are the references there. hip_gdn_chunked --bench and
--bench-default time this path and gfx1151's default kernels, one per process (the switch is read once).
gfx1151 (Ryzen AI Max+ 395, UD-IQ4_XS): a layer's recurrence + norm takes 1.60x less time than the default kernels at 16K and
32K tokens; a whole prompt 1.4-2.1% less at 4K-128K after a warm-up prompt (11 pairs each: 1.8-2.1% with the docs/STRIX_HALO.md section-5 switches,
1.4-1.5% without them, 10 or 11 of 11 faster); first-token KL against the default 0.006-0.007 on average, the same top token in
12 of 12 prompts. Run on gfx1151 only; kernels.cu compiles for gfx1030, 1100, 1151, 1152 and 1201 (docs/AMD_HIP.md).
Co-authored-by: libratechw <[email protected]>
Q8_0 has no minimum term and the F16 kernel's output already differs from the int8 kernels', so the KQ opt-in is
not needed for it. UD-IQ4_XS has five Q8_0-down layers (4 IQ3_S / Q8_0, 1 IQ4_XS / Q8_0): they left the fused path
for MMQ, and since fused_ring() asks every layer, the pack kept MMQ's buffers. d_covered_on() is the one test, for
native_supported() and experts_native(); gfx12 keeps d_covered().
UD-IQ4_XS, gfx1151, 3 interleaved rounds, prompt tok/s (v0.1.42 / 65d809c6 with STRATA_PF_F16=1 / this):
4K 926.6 / 992.2 / 1054.8, 32K 1257.4 / 1364.6 / 1397.1, 128K 1202.0 / 1309.4 / 1333.6.
native_f16_kernel takes the IQ gate/up formats (IQ2_XXS, IQ2_XS, IQ2_S, IQ3_XXS, IQ3_S, IQ4_XS) and the Q2_0, IQ4_NL
and Q8_0 down: load_unit / convert decode each sub-block once into an int8 weight tile in LDS with its scales as
half2, each wave turns its own rows into F16 (gufo's Q8_0 decode: sign flip, byte permute into 1024's mantissa,
one packed multiply), and the F16 WMMA accumulates all of K. The work item, the activations staged as F16, the WMMA
loop and both epilogues move to moe_f16_common.cuh, shared with the K-quant kernels (moe_f16_kq.cu, now on it; its
output bits are unchanged). The switch becomes STRATA_PF_F16=1 (was STRATA_PF_F16_KQ) and covers both; the K-quants
still need STRATA_PF_FUSED_KQ=1. gfx12 keeps the int8 kernels.
hip_prefill_fused_iq (gfx1151, ROCm 7.14.1 clang 23, top 10 over 512 experts, one 128-expert launch, ms):
- IQ3_S / IQ4_NL at 16384 tokens: gate/up 11.65 -> 9.87, down 6.11 -> 4.71; layer at 8192 37.3 -> 32.2.
- every pair is faster at 4096 and 16384 (IQ2_S gate/up 14.38 -> 9.85, Q8_0 down 6.80 -> 4.79), and at 128-2048
tokens equal or faster.
- FP32 reference rel RMS as the int8 kernels' (IQ3_S / IQ4_NL 1.456e-2 against 1.454e-2); against the int8 model
and MMQ the F16 path sits at 2.4-2.7e-3, so both checks allow 5e-3 under STRATA_PF_F16=1 (f16_on).
- With the weights decoded to F16 in LDS (18 KB a block) gate/up was 9.83 and down 5.71. Neither a VGPR cap nor
dropping the sign table (4 blocks per WGP) nor decoding after the products moved gate/up; without the decode it is
8.98, without the weight loads 9.30.
Off by default (the output differs from the int8 kernels').
moe_f16_kq.cu's bodies are gfx11's WMMA layout (and trap on other targets); v0.1.42's gfx12 native kernels share
experts_native's launch, so the F16 path is taken only off gfx12.
down_kernel takes gate/up's structure for UD-Q4_K_XL's Q5_1 down (47 of its layers): the codes and high-bit words
packed in LDS, w = q d + m per wave in F16, H staged once as F16 from its int8 blocks, FP32 outputs at the routed
rows. The activation staging and the WMMA loop are shared with gate/up (ActStage, mma_tiles). Q8_0 down stays on
native_w11_kernel.
hip_prefill_fused_iq (gfx1151, one layer, 16384 tokens x top 10 over 512 experts): down 7.10 -> 4.83 ms per
128-expert launch; Q4_K / Q5_1 layer 62.7 -> 53.5 ms (85.3 on the int8 kernels); FP32 reference rel RMS 1.304e-2.
UD-Q4_K_XL's routed gate/up was 6/10 of the prompt-time gap to gufo on gfx1151 (128K: 23.1 s against 13.9 s).
moe_f16_kq.cu adapts gufo's paired RoutedF16GEMMKernel (97b8645f, MIT) to Strata's tables, int8 activations and H:
the Q4_K / Q5_K codes stay packed in LDS and each wave turns its own rows into F16 after the read, so the F16 WMMA
accumulates all of K in F32 with no per-block epilogue; the activations are staged once in LDS as F16 for every wave;
the epilogue writes H as int8 per 32 features for the existing down kernels. One block per work item; two tiles of
an expert per item from 64 routed rows per expert on average.
hip_prefill_fused_iq (gfx1151, ROCm 7.14.1 clang 23, one layer, 16384 tokens x top 10 over 512 experts):
- gate/up kernel 14.63 -> 8.46 ms per 128-expert launch; Q4_K / Q5_1 layer 85.3 -> 62.7 ms, Q5_K / Q5_1 89.9 -> 66.2.
- against the FP32 reference the error is the int8 path's (rel RMS 1.305e-2 / 1.302e-2: the int8 activations
dominate); the int8 model check allows 5e-3 under this switch (2.5e-3 measured with Q8_0 down).
- the output bits are the same with 64- and 128-row items.
Off by default; it needs STRATA_PF_FUSED=1 and STRATA_PF_FUSED_KQ=1, whose output already differs from MMQ's.
After a prompt the borrowed expert-cache slots are refilled from the experts' files. With the GGUF read in place an
expert is three tensors, and the refill first assembled each 4 MB blob in a staging buffer (a copy on the calling thread)
and then copied that to the slot. The pieces are contiguous in the mapping, so they can go straight to their offsets in
the slot: blob_parts() hands them out, ExpertCache::fill_slot_queued_parts copies them on the same queue. The bytes in
the slot are the same. Off by default; gfx1151's table turns it on.
STRATA_CPU_IQ_ALL_EXACT1 reproduces ggml-cpu's AVX2 single-token dot bit for bit, which only
holds if the ggml-cpu in this binary is that dot: a STRATA_ISA_FLOOR build runs ggml's AVX1/SSE
dots, and a native build on an AVX-512 CPU run with STRATA_FORCE_AVX2=1 runs its AVX-512 one.
The guard is #2215's (ggml_cpu_has_avx2 / ggml_cpu_has_avx512).
Co-authored-by: Andy Slater <[email protected]>
Three changes for two-card AMD PCs whose faster card does not come first in the
runtime's enumeration (Windows HIP orders the cards itself; HIP_VISIBLE_DEVICES
only filters):
- gfx1032 (RX 6600) joins the unvalidated build list - it shares the gfx1030 /
gfx1031 / gfx1034 dp4a path.
- STRATA_PRIMARY_DEVICE=N picks the visible ordinal the engine's own session
runs on; it is set before any context or allocation exists. The helper
caches take the other visible devices, the HIP arch check and the peer tier's
device queries follow it, and the primary's session is created on the chosen
device. A layer split still assumes device 0 and is refused with this set.
Unset or 0 is upstream's behavior unchanged.
- The helper-GPU caches are allowed beside the resident RAM mode: the experts a
helper holds are left out of the RAM copy as a split stage's are, and the two
adaptive tiers already keep each other's experts out. RemoteExperts::finish
spins on its stream like PeerExperts::finish - on HIP the CUDA branch's
cudaInitDevice(ScheduleSpin) does not exist.
Measured on an RX 6800 (16 GB, gfx1030) + RX 6600 (8 GB, gfx1032), Windows 11,
32 GB of RAM, Swift 1.5 IQ3_XXS at 160K context, the resident RAM mode with the
helper's cache left out of the copy: decode 37.5 -> 44.2 tok/s on 8K-token
requests with a 3800-slot helper; the CPU pool's share of the routed experts
falls from ~2.9 to ~0.4 entries per layer-window.
Rebased onto 296b59d2 (conflicts in generate.cpp, peer_experts.cpp, hip_backend.cmake, docs/AMD_HIP.md resolved by keeping main's code and adding the PR's). Changes: remote_caches is kept (still used by the --batch-mtp check); the spin in RemoteExperts::finish is HIP only (CUDA keeps its cudaInitDevice(ScheduleSpin) + blocking sync). NOT COMPILED YET, not run.
Co-authored-by: tuandat3019 <[email protected]>
The menu never hid NVFP4 on an AVX2 CPU; the engine runs a native pack's rows on AVX2 kernels that are bit-equal to
ggml's. The NVFP4 menu line and the install step now say, on a CPU with AVX2 and no AVX-512, that the CPU's share of
the experts runs on AVX2 rows (measured about as fast when memory-bound, up to ~12% slower on 4-token windows).
A note only: nothing is refused or capped. tools/test_setup_menu.py: 2 tests.
The fork stays off by default on HIP (#816, #826): an RX 6800 on Windows and 4x R9700 decode 12-45% faster without it.
The same card family on another PC goes the other way (#1272: 2x RX 6900 XT on Linux), and the output is identical
either way, so it is a per-PC choice - the open item of #1272.
- tools/calibrate.py: step 6, HIP only. With everything chosen before, the engine is started twice, with
STRATA_SH_STREAM=0 and =1 set explicitly, two rates each; "1" is kept only when it beats "0" by more than MIN_GAIN
(the rule of the other steps). measure() returns the choice as "env"; a failed start drops that candidate (#1337).
start_engine takes an optional env; apply_env() sets the chosen variables and removes the others of ENV_KEYS, so an
older calibration's choice does not linger.
- setup.py: --calibrate writes "env" into the config and the settings file; a reinstall applies a saved calibration's
"env" the same way (a calibration saved before this step leaves "env" as it is).
- docs/AMD_HIP.md: the SH_STREAM note says calibrate measures it.
- tools/test_calibrate.py: the step runs on HIP only, keeps the fork only on a win, falls back on a failed start;
apply_env; setup writes and later removes the variable.
Measured with the real engine (0.1.42 tag, 2x RX 6900 XT gfx1030, Linux, IQ3_S layer split, setup-style config): the
whole calibration took 982 s, the new step chose STRATA_SH_STREAM=1 (off 62.2 / 61.3, on 67.2 / 66.6 tok/s, +8.3%).
Separately, with a fixed protocol, the fork's output is byte-identical to off (4 of 4 requests, one card and the split).
The scorer of 65ce329 is plain FP32 arithmetic, so it runs on any HIP card as it is; without matrix cores the
prompt path otherwise scores on the warp kernel. Its tiling becomes template parameters: every tiling keeps each
score's fmaf chain over k = 0..127 in order and the same relu sum over the heads, so every tiling gives the same
bytes (RX 6900 XT, qsa_select_bench at 32K and 128K: 14 tilings, also on 60 and 72 of its 80 CUs through
HSA_CU_MASK, all byte-identical to the original kernel; the fastest tiling the same at every width). CUDA keeps
the tiling it was written with; gfx103x runs 32/128/16/8: 256 queries at 128K 2.69 ms (warp kernel) -> 0.695 ms
(the original tiling) -> 0.587 ms. FP64 gate passed, selections identical 256/256. On the 2x RX 6900 XT layer
split a 128K-token prompt reads 7.4% faster, 32K 0.8%. A HIP ctest runs it at 131K.
Rebased onto 296b59d2: the docs/AMD_HIP.md conflict (main's SGEMM/rocBLAS-table bullets from the ports before it) is resolved by keeping all bullets.
Co-authored-by: xjc10 <[email protected]>
rocBLAS picks the kernel for each FP16 product from its own table by shape, and on gfx1030 that pick is far
from the best kernel the same library holds for some shapes: N = 10240 K = 2560 (the GDN input projection)
below T = 1152 runs at 5.2 TFLOPS where the best of rocBLAS's 237 solutions for n = 799 is 6x faster (not an
alignment effect: n = 800 is as slow as 799).
tools/hip/tune_rocblas enumerates the library's solutions per shape and token bucket, checks each against the
default kernel's output and that it also runs at smaller odd token counts, and writes the winners (and
`default` rows bounding their reach) as tools/hip/<arch>-rocblas-<version>.txt; Gemm::f16_inplace runs the
table's solution for a shape (STRATA_ROCBLAS_TUNING: a file, or the directory from which the engine takes the
file of its own rocBLAS build; the version string, tweak hash included, is checked). One tiny default product
at startup opens rocBLAS's kernel library (a solution-index call before that fails although the index is
valid); a refused solution falls back to the default kernel for that call, after three refusals for good.
setup points the engine at tools/hip when a table for the card's architecture exists. Shipped: the gfx1030
table for ROCm 10.0.0's rocBLAS 5.6.0.8d1ae90e (112 rows, 76 with a solution).
Measured (RX 6900 XT, 6f32ec0 + #835 + this, temperature 0, the same answers with and without): a 799-token
prompt's GPU time 3,329 -> 2,985 ms (-10.3%), 495 tokens 2,477 -> 2,139 (-13.6%), 8,874 tokens 11,978 ->
11,459 (-4.3%); a PCIe-bound 1,108-token prompt unchanged (its saving reappeared as `wait copy`). ctest 66 of
70 (the 4 failures are outside this change); hip_prefill_rocblas_gemm, the new smoke test, passes with 0
fallbacks. Builds on #835. bench/results/2026-10-05-rdna2-rocblas-table has the per-run lines and the tuner's
summary.
(cherry picked from commit e35e103604)
Rebased onto 296b59d2: conflicts in setup.py (SETUP_ENV gained STRATA_ROCBLAS_TUNING beside the 0.1.44 entries) and docs/AMD_HIP.md (kept both sections) resolved.
Co-authored-by: xjc10 <[email protected]>
The rebased #704 under the name asked for there (upstream's STRATA_HC_Q8 now
reads the GGUF's Q8_0 bytes, a different thing). Opt-in. The four BF16 hc_*
projections per layer (1,200 MiB) are skipped by the loader and uploaded as
int8 codes with one fp32 scale per 32 values (675 MiB); the freed VRAM goes to
the expert cache.
- Q8 is a template parameter of the down/up kernels (single- and multi-token):
the BF16 instantiations compile exactly as before, the launch sites pick the
Q8 ones when w_down / w_up carry scales. The inject rows stay BF16.
- The staged / v3 / upmix / PF_HCDOWN / gfx906 split paths read BF16 rows and
are skipped for it; the prompt path expands the codes into a bf16 scratch for
its GEMM.
- The loader seeks with 64-bit offsets.
- A layer split keeps BF16 with a warning instead of exiting.
Fix-up to the ported #1123 commits. The route is not bit-identical to the native call
(the author measured at most 4.3e-6 of the largest output apart), so unset keeps the native
path on every card, gfx103x included; =1 enables it on gfx103x devices only (a mixed layer
split keeps the native call on the other cards), =all on every AMD device for A/B arms and
tests. The buffers are no longer freed under an in-flight GEMM: the stream is synchronised
before the FP32 copies grow, as the sm_60 FP32 route does. docs/AMD_HIP.md says so.
Port of PR #1838 (commit fb868db01) onto 0.1.44.1. Test file merged with #2134's foresight test (both kept). replace_resident now drops a Foresight slot for the outgoing expert while its RAM place is rewritten (#2134's copies read those places).
Co-authored-by: SanghunYun <[email protected]>
Rebased onto 296b59d2: the docs/AMD_HIP.md conflict (the gfx1031/gfx1150/gfx1152 bullets main gained) is resolved by keeping main's text.
Co-authored-by: vallicgrr <[email protected]>
STRATA_QSA_DUMP_LAYER=i keeps one QSA layer, STRATA_QSA_DUMP_STEPS=1 appends
the steps to each record.
pa_bench: the prompt sparse attention (qsa_prompt_attn_batch) on a
recorded selection - a STRATA_QSA_DUMP record of one layer and chunk with
its steps appended - random q and int8 K/V; median time and an output
hash for A/B of kernel variants (PA_BENCH_OUT=file writes the output).
On Turing the v2 int8 prompt attention (STRATA_PA_V2T=1) stages the next
chunk in registers, so its second shared-memory stage bought nothing - and
at 45.6 KB a block only one fit an SM (64 KB): four warps on four
schedulers, every latency exposed (ncu: 12.5 % warps active, tensor pipe
8 %). Smem2T: one K/V stage, and the q.k partials of cell group nt
(8 cells) written over the 512 bytes of the warp's own K slice that held
those cells (read by then, a __syncwarp between) instead of their own
8.4 KB. 19.7 KB a block: three per SM (168 registers fit three). The
same arithmetic in the same order - bitwise the same output.
RTX 2080 Ti, a recorded 130K selection (QSA layer 5, last chunk: 15871
queries x 2051 cells, tests/cuda/pa_bench.cpp): 113.8 -> 68.5 ms, same
output hash. Engine (Q2_0 pack, 16K chunks, fused experts): 98K prefill
53.3 -> 49.9 s, 4K 3.54 -> 3.42 s; first logits bitwise identical at 98K
and 4K, the same generated text.
On Turing the int8-KV prompt attention ran v1 because v2 stages its K/V
slices with cp.async. Here a warp loads its next slice into registers and
stores it into its other stage after the current chunk (the same bytes in
the same places); the MMAs are the m16n8k8 pairs v1 already uses. Accuracy
as v1 (qsa_prompt_attn_parity vs FP64: 3.7e-6 / 4.9e-6 / 4.5e-6 / 3.8e-6 of
3.5-3.7). RTX 2080 Ti, ms per chunk v1 -> v2: 32K x 1024 queries 13.0 -> 7.6,
64K x 512 6.7 -> 4.8, 1.5K 6.2 -> 5.0, 2.1K x 256 3.2 -> 2.3. Opt-in until
the engine A/B.
With STRATA_BF16_TC the prompt path converted xn16 (T x 10240) from BF16 to
FP16 before each of its two GEMMs (down and inject), in 16M-element slices:
~2.8 s of a 130K prompt read on an RTX 2080 Ti. The four xn16 writers now
store gemm.cu's conversion of the BF16 value (bf_to_hf_conv: the same clamp
and rounding as bf16_to_f16_kernel), and Gemm::bf16(..., x_f16) skips the
conversion on the tensor-core path (same slices, same GEMMs) and converts
back to BF16 on any other. Only when the stage runs BF16 on FP16 tensor
cores, without the BF16X2 low parts or a padded xn16 stride; the MTP draft's
own read keeps BF16. STRATA_XN_F16=0 turns it off.
First logits after a 98K prompt: bitwise identical; prompt read 1431 -> 1473
tok/s; a 4K run writes the same tokens.
The per-group dequant kernel writes 2 bytes every 64 bytes; 0.1.43 coalesced only the Q8_0 path (#1720). On compute
capability 7.5 the BF16 projections of the prompt path take a kernel that writes 16 bytes per thread; the values are
the same (a conversion, no arithmetic), so the output is bit for bit the old kernel's. Other cards are untouched.
The rocBLAS the engine ships has tuned gfx1030 kernels for FP16->FP16, int8 and
FP32->FP32 only; every FP16/BF16-input, FP32-output GEMM (rocBLAS HS/BS) runs a
fallback kernel at ~5 TFLOPS. Widening the inputs to FP32 is exact and SGEMM
accumulates in FP32 like the native call.
Nothing passes through FP16, so unlike STRATA_HIP_PROMPT_F16=1 (#835) the
route cannot overflow: an FP16 GEMM output above 65504 becomes inf and can
spread as NaN, while here it stays a finite FP32 value. It is slower than the
FP16 route, and not bit-identical to the native call (at most 4.3e-6 of the
largest output apart on random inputs).
RX 6800, Coder IQ1_M, fresh 12K-token prompts (4 each, A/B in one binary via
STRATA_RDNA2_SGEMM=0): prompt 312 -> 452 tok/s, GPU timeline 37.2 -> 25.5 s
(gdn 9.4 -> 2.8 s, qsa proj 3.6 -> 1.4 s, hc read 3.85 -> 2.84 s). Decode
unchanged. A 9-task code-repair benchmark: 9/9 first try on both, model time
458 -> 374 s.
Default on for gfx103x only; N < 64 keeps the native call; if the FP32
buffers cannot be allocated the native call runs.
Rebased onto 296b59d2: the conflict in Gemm::f16 and gemm.hpp (the CUDA FP16 algorithm fallback and the sm_60 STRATA_DENSE_F32 block that main gained) is resolved by keeping both; the RDNA2 call sits before the CUDA/HIP GemmEx call.
Co-authored-by: vallicgrr <[email protected]>
block_topk_wide_kernel reads a query's scores six times (four radix passes,
a count, the emit): past ~115K cells that is DRAM traffic. block_topk_sample_kernel
takes a bound from every 32nd block (the KS-th largest of those keys), reads
all blocks once keeping those at or above it, and when they hold at least
`width` cells computes block_topk_kernel's threshold, `above` and tie budget
over them and emits them in block order (bitonic sort): every block at or
above the threshold is a candidate, so the ids are the reference's.
Otherwise the query is flagged and block_topk_wide_kernel<true> runs it.
qsa_topk_parity --selftest and qsa_topk_active_parity: identical ids
(continuous, 64-level, equal scores - the last ones take the fallback).
RTX 2080 Ti, 256 prompt queries under a 512K capacity: 131K 0.51 -> 0.45 ms,
262K 0.95 -> 0.59, 400K 1.48 -> 0.71, 520K 1.84 -> 0.79. Used above 28,672
active blocks; STRATA_TOPK_SAMPLE=0 keeps the wide kernel.
A verify window's 1-8 queries scored and selected on a handful of SMs; at
420K cells on an RTX 2080 Ti the two took ~6 ms of a ~52 ms window.
- block_scores_halve_kernel: the decode scorer's warp sums as one
recursive-halving reduction (each value pairs lane l with l ^ o at offsets
16..1, the butterfly's additions operand for operand), the lane's query
values in registers (shared-memory reads were the limit, ncu mio_throttle),
5-8 queries as two calls of up to 4. Same bits (qsa_select_bench
QSA_BENCH_DECODE=1 hashes, 32K-524K, 1-8 queries). 420K: 1 / 3 / 4 / 6 / 8
queries 117 / 265 / 347 / 510 / 774 -> 101 / 118 / 125 / 238 / 255 us.
STRATA_SCORES_HALVE=0 keeps the old kernel.
- qsa_block_topk_split: the decode top-k over chunks of 2,048 blocks in six
launches (four radix passes into per-query global histograms, a count, the
emit); every CTA takes each digit from the complete histogram with
block_topk_kernel's rule, so the ids are identical (qsa_topk_parity
--selftest: continuous, 64-level and equal scores, 2K-524K). Decode calls
at 420K: 0.21 -> 0.033-0.064 ms; short contexts unchanged within 0.01 ms.
STRATA_TOPK_SPLIT=0 keeps the wide kernel (=1: on any card, for tests).
Both on sm_75 only by default.
block_scores_simt2_kernel: 8 x 8 products per thread, twice the blocks per
CTA and the next K chunk in registers; every dot is the same fmaf chain over
k = 0..127 and the same relu sum, so its scores are bitwise those of
STRATA_SELECT_SIMT=1 (qsa_select_bench QSA_BENCH_HASH=1, 131K-510K).
RTX 2080 Ti, 256 queries: 131K 1.21 -> 0.95 ms, 262K 2.01 -> 1.87,
400K 3.00 -> 2.82, 510K 3.77 -> 3.57 (the warp kernel: 7.7 / 15.3 / 23.2 /
29.6 ms). Opt-in like =1 (another summation order than the warp kernel).
Below stream_all_min() the MMQ path gathered every expert with its own launch
and, for a staged one, its own wait and release event. The staged walk now
groups like the streamed walk: its ring is STAGE_GRP (32) slots, two groups, and
it stages at most 15 entries ahead, so a group's staged slots are not refilled
before the group is gathered (one wait on its last copy, one event releasing
them all). stage_slots() counts those 32 slots wherever the ring is counted or
taken, so a prompt borrows every byte it writes.
STRATA_PREFILL_GROUP_GATHER=0 turns grouping off everywhere, as before.
gr_up75_kernel: two batches of 4 rows of w_up loaded before the dots, the
token activations read once per batch, the 4 xor reductions interleaved and
the row tails held 4 rows to a uint4 - bitwise gr_up_multi_kernel (gr_parity,
gr_multi_parity, fused_gr_bench hashes). Used from 3 tokens on sm_75
(STRATA_GR_UP75). The staged down kernel takes a token group from
blockIdx.y; sm_75 runs T 4 and 6 as pairs (STRATA_GR_DOWN_TG).
fused_gr_bench cold, RTX 2080 Ti, whole read: T3 53.2 -> 50.2 us,
T4 64.1 -> 57.6, T5 76.2 -> 69.3, T6 86.1 -> 70.9; T1-2 unchanged.
native_expert_bench on real Q2_0 rows of Flash-Next, RTX 2080 Ti, bitwise
equal to mode 0 in every case: 10 groups x 1 entry 47.3 -> 41.4 us,
20 x 1 83.7 -> 71.5, 30 x 1 120.2 -> 100.2, 20 x 2 124.2 -> 111.4. The down
projection of mode 4 loses with 3+ entries per group (12 x 3 36.5 -> 40.8,
16 x 4 56.9 -> 68.9), so cap_groups > 30 keeps mode 0 for it.
STRATA_SM75_EXP=0 keeps the CUDA layout; STRATA_EXP_MODE still overrides.
adapt() marks an evicted expert non-resident in host_res at once; d_res follows in apply_pending, once the round's
copies have landed. When the tier does not wait for them (STRATA_ADAPT_NOWAIT=1, or STRATA_ADAPT_LAG > 1) the next
window runs before that. doorbell_publish_res reads d_res, sees the evicted expert as resident, and when every routed
expert of a layer looks resident it does not publish the layer's activation. The pool plans from host_res and
computes the evicted expert on the CPU from the activation published before. Now every verifier launch (run,
run_slot_rows, batch_launch, pl_launch; a layer split's stages too) runs a pre-launch hook. It patches the device
tables from a list of changed entries: res_patch, a small kernel on the verifier's stream reading mapped memory; the
whole table when the list overflows or a device does not map it at the host's address. A failed whole-table upload in
apply_pending leaves its entries to the hook. Only where the tier does not wait. --batch-groups with a layer split
publishes every activation instead: set_always_publish, which now also turns E-6's device plan off.
--pipeline-windows keeps its fence. STRATA_ADAPT_EVICT_SYNC=0 restores the old behaviour. STRATA_ADAPT_CHECKRES=1
reads the tables back before each window, compares them with host_res, and counts the layer-windows that computed a
CPU expert with no activation published.
RTX 5090, IQ2_XS, --expert-cache 12000, 32K explain, 600 tokens: NOWAIT=1 34 of 222 windows with mismatched tables
(2470 entries), 1 such layer-window -> 0 / 0 / 0; LAG=2 55 of 223 (3222 entries: every swap), 5 -> 0 / 0 / 0. LAG=2
is reproducible (two runs of each arm, the same tokens), and the fix changes its answer from token 228. The hook's
cost under NOWAIT (6297 slots, 3 pairs): ms a round -1.1% chat, -1.2% 32K, noise. The default (the tier waits)
installs no hook: tokens and first-token logits identical to d5ea7133 (p2k 200 tokens, 32K explain 600).
Port of PR #1517 (commit eb4796045) onto 0.1.44.1. Conflicts with the 0.1.43 adapt overlap gate and the tail-skip kind -2 resolved; set_adapt_gate and set_always_publish kept side by side. Docs line added.
Co-authored-by: sergiywith <[email protected]>
The real data_folder() moves the model files of this checkout and of the data folder named in the user's settings.json into --data-dir and saves that folder as the new data folder. With a TemporaryDirectory the models are deleted on exit and settings.json is left pointing at a deleted directory (found on the M4 and on the PC: the test wrote %APPDATA%/Strata/settings.json and ~/.config/strata/settings.json). The test is about --source, so data_folder is mocked.
The 2-4 column dense projections of the verify windows ran the plain multi-column kernels on Turing, which are
bound by dp4a throughput there from 2 columns on. mmvq_il_parity --bench --emit-table on an RTX 2080 Ti (SM 1650
MHz) gives a table like the Volta/Pascal ones: IQ4_XS 2560x10240 at 4 columns 81.1 -> 37.4 us, Q4_K 74.5 -> 39.1,
the Q5_K head 1671 -> 1045 us. Bitwise equal (the decode writes the same tokens). No one-column table: the
engine's dedicated single-column kernels sum in another order, for a 0.5% gain. STRATA_SM75_IL=0 turns it off.
Q2_0 512K resident, 4K prompt: one-shot decode 52.6 -> 55.5 tok/s; serve steady state ~69 -> ~77 tok/s.
prompt_attn_i8v3_kernel: QK^T and PV as mma m16n8k32 on the int8 K / V codes as stored; the other operand in three
exact 8-bit parts (q scaled per head and 64-dim group, p * v_scale per row and chunk; X = rint(x * 65536) split into
h, l signed and m unsigned bytes, a u8 x s8 MMA): 24 bits, exact int32 sums, combined in FP32. One K and one V stage
per warp, 12-row partials, launch bounds (128, 3): ~25.5 KB, 3 blocks an SM. sm_80+ (STRATA_PA_SM80); Turing,
Volta and HIP keep their kernels. Unset: v2, byte-identical.
qsa_prompt_attn_parity: against FP64 IMMA 2.15e-6 - 2.64e-6, v2 2.78e-6 - 3.30e-6, the FP32 kernel 1.90e-6 - 2.51e-6.
IQ2_XS, first-token KL to the FP32 kernel (STRATA_PROMPT_ATTN_OLD=1): median 0.0027 (IMMA) against 0.013 (v2);
2K 0.0026 against 0.0248. 1.35-1.53x a launch; a 32K prompt in 8K chunks 358 -> 312 ms of attention.
Fix-up to the ported #1149 commits. The range counters and the saturating to_f16
are compiled for HIP only, so the CUDA kernels (hf_sat in the SwiGLU product, the
embedding image) stay exactly as they were, with no flag read; set_f16_range /
f16_range_read are no-ops there. strata_hip.h (the STRATA_HIP_GFX906 compat layer,
which goes first on that backend's include path) gets the cudaMemcpyFromSymbol
mapping the HIP shim already has, so the counters also compile for gfx906.
The verify window reads pcie_frac of a layer's missed experts over PCIe, a share
the link probe sets; the CPU's speed does not enter it. With a slow CPU the GPU
spins on the pool's rows: IQ2_XS with one AVX2 pool worker (emulated) runs
16.8-17.2 ms a round at the probe's 0.55.
Opt-in, STRATA_PCIE_BALANCE=1 (one GPU, no batch slots, no --pcie-frac):
- wait_flag_ge_stamped: the flag spin kernels also write the GPU clock as the
plan arrives, as the PCIe part starts and as the GPU waits for the CPU's rows
(no extra launch; captured only with the switch on);
- the host times its pool per step; after each window, least squares with
forgetting fit the pool's a + c n_cpu + d n_pcie (d: the link's DRAM reads
slowing the pool - on a DRAM-bound pool moving experts to PCIe saves nothing)
and the GPU's g0 + g n_pcie;
- a layer then reads over PCIe the m of its misses that minimizes the longer
side; every 64th layer that would keep them all on one side moves one, so
both fits stay measured.
IQ2_XS, RTX 5090 + 9950X3D, chat to the end of the turn:
- one AVX2 pool worker: 16.77 / 17.17 -> 12.98 / 12.82 ms a round
(fit: CPU 42-60 us an expert, GPU 28 us a PCIe expert; 0.8 of the misses
move);
- the 9950X3D's 16 workers: 3 pairs 12.19 -> 12.17 (pool DRAM-bound, few move).
Off (the default) the logits and 32 tokens equal v0.1.39 (2K, fixed cache).
Port of PR #1548 (commit a513394c4) onto 0.1.44.1. Conflicts with the 0.1.43 tail skip, PCIE_MIN1 and the fused waits resolved: the fused wait+copy launches stay off while the balance stamps are on. Docs line added.
Co-authored-by: sergiywith <[email protected]>
The up GEMM of each hyper-connection read writes `gated` (T x 10240 FP32, 336 MB at an 8K chunk) and gr_mix_r reads
it back with R at ~96% of the bandwidth - only fewer bytes help. gr_upmix_cuda_kernel (128 tokens x 32 columns x 4
streams a block, K = 320 in cp.async-staged slices of 32, BF16 mma.m16n8k16 with FP32 accumulation, the mix in
registers in gr_mix_r's order writing all its outputs) never writes or reads `gated`. Behind the gfx11 switch;
sm_80+ per device (STRATA_EMULATE_CC honoured), chunks of >= 33 tokens; else the pair. Unset: unchanged.
gr_upmix_parity (new ctest): bitwise the cuBLAS + gr_mix_r pair at T >= 33 on an RTX 5090 (cuBLAS 13.3), <= 1.8e-7 at
T <= 32, both as close to FP64. 1.42x / 1.81x / 2.19x the pair at 2K / 8K / 32K; IQ2_XS: an 8K prompt -4.4%, 32K
-3.9% (the hc read -19% / -24%). Logits identical with it on (600, 2K, 32K); the 32K greedy tokens moved only through
the time-driven suffix-draft policy (identical with --suffix-draft 0).
mmq::quantize(m.mixed, m.src_dev, ...) read and quantized every token's row of `mixed` once per routed expert (K =
10). mmq::quantize_scatter uses llama.cpp's quantize_scatter_mmq_q8_1_cuda (the pinned quantize.cu strata_mmq
already compiles): each token quantized once, its block written to all of its rows through slot_dev, the inverse of
src_dev. The same q8_1 bytes, so the default; STRATA_QUANT_GATHER=1 keeps the gather. No new device code (CUDA and
HIP compile the same kernels); SYCL's prefill.cpp untouched.
prefill_mmq_scatter_test (new ctest): every MMQ q8_1 layout (D4, DS4, D2S6), T = 1..2099: 0 differing bytes. IQ2_XS
600 / 2K / 32K and Q2_0 2K against e8ca9afd: identical first-token logits and 32 greedy tokens. RTX 5090 (96 MB L2):
the quantizer 2x at 2K, even at 8K (the chunk's input fits in L2), 1.4-1.9x at 32K; a 32K prompt with
--prefill auto:32768: 77.5 -> 58.1 ms of quantization.
Co-authored-by: sergiywith <[email protected]>
The staged IQ3_XXS / IQ3_S tables trade a per-block copy and barrier for
cheaper lookups, which pays only where the grouped expert kernels are
decode-bound. On GP100 (6.0: no __dp4a, 70-140 GB/s) it does; on an RTX 3090
Ti (sm_86), forced on with STRATA_IQ_STAGE_TABLES=1, the same real Swift 1.5
IQ3_S layers (20 experts, 24 entries) took 3-13% longer (71-76 -> 75-84 us;
those kernels read at 490-665 GB/s there) and IQ3_XXS was unchanged, the
output bitwise the same. 6.1 cards (P40, GTX 10) have __dp4a and are
unmeasured, so the default now asks for 6.0 rather than any 6.x;
STRATA_IQ_STAGE_TABLES=1 still turns it on anywhere.
docs/OLDER_GPUS.md says so, and that neither STRATA_Q8_SM60 nor the tables
is measured on 6.1: both build for sm_61, and tools/q8_sm60_check.cu passes
on an RTX 3090 Ti, where the kernel's dot is the hardware __dp4a.
IQ3_S was the one i-quant gate/up format whose codebook the grouped expert
kernels still read from global memory (IQ2_XXS / IQ2_XS / IQ2_S stage theirs
everywhere, IQ3_XXS on 6.x since the first commit of this branch). Its 512
codebook words are now staged per block with two 256-word tables of byte
masks - IQ3_S keeps one explicit sign byte per 8 values, so the tables hold
the masks of a sign byte's bits 0-3 and 4-7 - applied as
(g ^ mask) + (mask & 0x01010101): every iq3s_grid byte is odd, so a negated
byte never carries into the next. Bitwise the same output (iq_multi_parity
and native_grouped_parity on a GP100 die, with and without
STRATA_IQ_STAGE_GRID_MMVQ=1).
One GP100 die, a verify window's layer of real Swift 1.5 IQ3_S experts
(20 experts, 24 entries): 517 -> 393 us with Q2_0 down, 492 -> 369 us with
IQ4_NL down. Swift 1.5 IQ3_XXS (13 of 48 layers IQ3_S), four GP100 dies,
1M context, decode tok/s, medians of three interleaved engine starts:
36.0 -> 37.2 (10.7K document), 36.3 -> 37.4 (cached), 43.1 -> 44.5
(reasoning), 41.6 -> 43.0 (code), with the same drafts accepted.
The switch covers both formats and is renamed accordingly:
stage_tables_on(), STRATA_IQ_STAGE_TABLES=1|0 (was STRATA_IQ_STAGE_GRID18).
Still the default on compute capability 6.x only.
The tool converted only the shard it was given and hard-linked the others, which is enough for Flash-Next (every
natively served projection and the head are in shard 1) but not for Swift 1.5 IQ3_XXS, which keeps 78 of them in
shard 1 and 223 in shard 2: one run left three quarters of them in their K- / i-quants, silently, and the kernel
only runs Q8_0 matrices. Now any shard of the model may be named; every shard that holds such a matrix is written
as before (same tensor-info length, Q8_0 blocks appended, offsets checked) and the others are hard-linked.
Checked: Swift 1.5 IQ3_XXS in one run gives both shards byte-identical to two single-shard runs (301 tensors);
Flash-Next IQ3_XXS converts 301 tensors in shard 1 and links shard 2.
GP100 has no __dp4a, and on a GP100 die the verify window's dense projections read their weights at 40-80 GB/s
of ~730. With STRATA_Q8_SM60=1 every Q8_0 dense matrix (any shape) and the head get the packed planes of
STRATA_Q8_PACKED and run src/kernels/cuda/q8_sm60.cuh: a warp (half / quarter warp for narrow rows) per row,
16-byte weight loads through the read-only cache, the window's q8_1 activations copied once per workgroup into
shared memory as an int8 plane plus float scales, STRATA_DP4A (vmad on sm_60), a shuffle sum with no barrier per
row, persistent workgroups (4 per SM, STRATA_Q8_SM60_BPS). Shapes it declines (> 48 KiB of shared memory: a
6144-wide row with 7-8 columns) keep the usual kernel. Not bitwise the exact kernels (summation order); the
packed copies cost their size in VRAM (Flash-Next: 2.9 GiB of dense projections over the stages, 0.6 GiB head).
The GSQ-RCO packs have no Q8_0 dense matrices: tools/q8_dense_gguf.py writes a shard 1 whose natively served
projections and head are Q8_0, keeping the tensor-info block's length so every expert keeps its offset (the native
pack is reused) and hard-linking the other shards beside it.
PH402 SKU 200 (four GP100 dies), clocks locked at 1050 MHz, Flash-Next IQ3_XXS, 32K, layer split 12,24,36,
--prefill 2048, greedy, decode tok/s (medians, interleaved starts): GSQ-RCO shard 28.3, Q8_0 shard with
STRATA_Q8_PACKED 31.1, Q8_0 shard with STRATA_Q8_SM60 38.9 (+37%); per window dense 19.2 -> 11.2 ms, head
4.25 -> 2.1 ms; prompt reads unchanged. tools/q8_sm60_check.cu: 57 shape x column cases against a CPU reference,
max relative error < 1e-5. docs/OLDER_GPUS.md has the table, the conditions and how to use it.
Co-authored-by: ruibeikaa <[email protected]>
Windows sends a GPU's interrupts to one logical processor, usually the first,
and the host thread spins there (--host-core first, the default). --host-core
last moves it to the last physical core, but not on a hybrid CPU, whose first
core is its best P-core, so a 12th-14th gen Intel or a Core Ultra keeps the
host on logical processor 0 with the interrupts.
--host-core sibling (or STRATA_HOST_CORE=sibling) keeps the host on that same
first core but on its other hardware thread. The interrupts keep the first
logical processor, which nothing else is pinned to, and the workers keep
every other core, since they take one logical processor per core (with
--pool-affinity auto / p-cores, which also use the P-cores' siblings, the
host's sibling is taken out of their list). Without an SMT sibling it is
--host-core first. It works the same on Linux (the sibling from sysfs), where
the interrupts are usually spread anyway.
detect_cpu_topology records the host core's sibling (CpuTopology::host_sibling)
in each layout; pool_affinity_test checks, in every affinity mode, that
sibling puts the host there with no worker on its core, and that it is first
without SMT. The startup log names the placement.
The README's range check said "none beyond 65504" over 110K tokens without
the numbers. The engine now has the counter: STRATA_F16_RANGE=1 records,
per device, the largest |value| and the count beyond 65504 (or not finite)
where a value enters or leaves FP16 - the activation images (hf_sat, the
image writers' one funnel), the BF16 weights converted to FP16
(bf16_to_f16_rows) and the FP16 GEMM outputs as they are widened
(widen_rows_f16: an output rocBLAS wrote beyond the range is Inf there) -
and prints one line per prompt. Off, the cost is one flag read per value;
on, a 34.4K-token prompt read at 889.8 tok/s against 888.5. to_f16 (the
hc image for the FP16-weight GEMMs, the PLE key image) saturates like the
other image writers now, so every FP16 image is under the same rule. The
HIP shim gains cudaMemcpyFromSymbol.
Measured on one RX 6900 XT over 466K tokens in nine prompts (corpus
slices, two prompts built to push activations - delimiter floods, a token
repeated 4,000 times, 6,000-character runs, base64, five scripts - and
four batches of real requests): outputs peak at 409.5 (160x below 65504),
activations at 112.2, the BF16 weights at 11.56; nothing beyond the range,
nothing non-finite. The prompts built to push activations stayed below
the ordinary corpus. One model (GSQ-RCO IQ3_S); the table is in the
README's range section.
The README also gains the same-card comparison with #1006, which takes
these GEMMs as FP32 SGEMMs: 8.4K / 32.8K / 104K-token prompts 412 / 455 /
456 tok/s on the old path, 622 / 742 / 750 with #1006, 730 / 900 / 904
with this change (the card's FP32 rate against its FP16 rate), with the
phase times and what the SGEMM route covers that this change does not.
(cherry picked from commit e780c8581e)
The test ran only the FP32-out GEMMs, so the gfx103x prompt path's FP16-out
GEMM (f16_inplace, widen_rows_f16) and bf16()'s W conversion through the
scratch were not under ctest. Every shape now runs a second time with
set_f16_io(true), forced on whatever HIP card runs the test: X as the FP16
image for the BF16 products, a scratch of 64 rows so a 96-row weight converts
in two slices (the second written at Y + 64), ldy > N, an odd N (257), T = 1,
and a beta = 1 f16() call, which keeps the FP32-out GEMM. FP16-out results are
checked against the double reference at FP16's rounding (6e-4 relative per
element plus 1e-4, rel L2 < 1e-3); the row padding and the guards around Y
must be untouched, as before. The FP32-out runs keep their tolerance.
Found while adding them: the BF16 inputs were all zero. hip_bfloat16(float)
produced data 0 in this test's build (ROCm 10.0, gfx1030; not traced further),
so the two BF16 cases passed on nothing (rel L2 printed 0). The BF16 bits are
now made by hand (round to nearest even on the float's bits); with real inputs
the BF16 cases pass at rel L2 1.9e-07 (FP32 out) and 2.2e-04 (FP16 out).
On an RX 6900 XT (gfx1030, ROCm 10.0): 11 of 11 pass.
(cherry picked from commit 4c6c9fe374)
On HIP a prompt batch with fewer than 7,168 blocks (28K cells of context) took the original top-k kernel, a line measured on
gfx1201 before the cooperative scan. With the scan the register kernel is faster from about 15.5K cells on gfx1151 (2,048 queries:
original / register kernel 0.57 / 0.82 ms at 13K, 1.01 / 0.87 at 16K, 2.4 / 0.93 at 20K, 3.9 / 1.00 at 24K). The line is now
STRATA_TOPK_REG_MIN_BLOCKS (default 7168, as before) and gfx1151's table sets 4096. The selected cells are the same
(qsa_topk_parity, also run with the register kernel always on: ctest qsa_topk_parity_reg_always).
Ported from shinbunbun/llama-cpp-p100-patches (MIT), patches 29 `mmvq-iq3xxs-grid-smem` and 30
`mmvq-ksigns-smem`: the grouped expert kernels stage the 256-word iq3xxs_grid and two 128-word tables of per-byte
sign masks once per block, and apply a mask as (g ^ mask) + (mask & 0x01010101) instead of the per-lookup
unpack_ksigns / __vcmpne4 / __vsub4 chain (iq3xxs_grid's bytes are all >= 4, so no carry crosses a byte).
Bitwise the same output: checked end to end on a PH402 (GP100), greedy, three prompts x three interleaved rounds,
identical replies with and without. Measured only on GP100, where it is within noise (+0-4% decode), so it is the
default on compute capability 6.x only; STRATA_IQ_STAGE_GRID18=1 / 0 forces it on / off on any card.
block_topk_reg_kernel picked each radix digit with thread 0 walking up to 255 bins while the other threads waited.
Threads 0-255 now prefix-sum the bins in descending order, and the one where the count above reaches `width` takes the
digit (bin 0 when none of 255..1 does). The digit and the count above are the same, so the selected ids are the same.
Measured on a Ryzen AI Max+ 395 (gfx1151, ROCm 7.14.1), qsa_topk_parity with 2,048 queries (CTX 2048 10 <ctx> 1),
ms per call:
32K 2.19 -> 1.11 (-49%)
64K 2.54 -> 1.63 (-36%)
128K 4.83 -> 3.79 (-22%)
qsa_topk_parity --selftest: 19 PASS. UD-Q4_K_XL end to end, 3 interleaved rounds: the same text in every round; the
128K prompt moved +0.2%, inside the noise, so the gain is the kernel's and not yet visible end to end.
CUDA (RTX 3060, Windows, CUDA 13.0, sm_86), same call, ms: 4K 2.12 -> 0.975, 8K 2.04 -> 1.03, 16K ~1.9 -> ~1.1, 32K ~2.0 -> 1.27, 64K 2.53 -> 2.15, 128K 7.29 -> 7.29 (no change); qsa_topk_parity --selftest passes, ids identical in every case. Not built: SYCL.
The negative-scale-times-zero workaround was gated to an arch list (gfx1012 on
HIP < 7, gfx1151, gfx1200/gfx1201 since 0ff45cffb); the list only ever chased
the newest report. The workaround writes the scale's sign bit for code 1, which
is value-identical on targets whose compiler never folds: keep it on every HIP
build. Ported from PR #1540 onto the tree that already carries the gfx1200/1201
entries; the comment text is adjusted.
Co-authored-by: nekomario28 <[email protected]>
gemm_iq_f16_grouped: one launch per MMQ expert group, a block = 128 weight rows of one
expert x up to 256 of its token rows, each weight superblock dequantized once into shared
memory by dq_dispatch (the FP16 path's formulas), FP16 activations, WMMA with FP32 sums,
the next activation tile prefetched into registers. sm_70 defaults to it for the layers
MMQ takes and for IQ1_M (which rides the MMQ grouping); STRATA_PF_WMMA=0 keeps MMQ.
Co-authored-by: NoxestNoz <[email protected]>
(merge-resolved onto 0.1.44.1: the MMQ gather now zeroes the group tail itself and the quantizer takes a scale buffer)
The pool's default layout puts a worker on every physical core, and a layer's
experts are done when the slowest worker is. On a Core i7-13700K (8 P + 8 E
cores) the E-cores make the verify window wait, by an amount that changes from
run to run.
`--pool-affinity measure` (hybrid CPUs, native packs) times both layouts on
the pack's own experts before the pool is built: two experts of four tokens
per layer through run_split_multi_native, every layer in turn, the host
spinning ~150 us between two layers, three interleaved blocks per layout,
about a second in all. p-cores is taken unless all is more than 10% faster.
One line says what it read and took. It is one reading at start and is not
repeated.
Measured on this branch (0.1.42 + this change), Core i7-13700K, RTX 5090,
64 GB, Windows 11, Swift 1.5 IQ3_XXS, 128K context, one 8K prompt and the
same 1,500 forced tokens (--spec-follow), three interleaved rounds:
default (all, 15 workers) 146.6 / 151.8 / 158.0 tok/s, pool 6.3-7.6 ms per window
--pool-affinity measure 200.0 / 201.5 / 204.8 tok/s, pool 2.1-2.2 ms (read all at 16.4-17.6 ms, took p-cores)
--pool-affinity p-cores 200.3 / 203.9 / 204.9 tok/s, pool 2.1 ms
Over four server starts of one day on 0.1.41 it read all at 14.8, 21.9 and
25.3 ms against 6.1-6.2 ms for p-cores, and once all at 6.6 ms against 8.6 ms,
where it took all.
Without the value nothing new runs: the option parses as before and the
function is not called. The layout changes no result, only where threads run.
Built for CUDA only (sm_120, CUDA 13.0.88, MSVC 14.50 with nvcc's
--allow-unsupported-compiler); HIP and SYCL were not compiled (the change is
in generate.cpp, which both build). Not measured: other hybrid CPUs, Linux.
0.1.43 made the decode window lean under STRATA_VERIFY_NO_HOST with the device
plan (46a77907): the doorbell publish, flag waits and plan copy are dropped
because no host answers them. capture_batch() records batch windows through the
same record_window(), so a batch graph was also recorded lean, but the host batch
path (run_slot_rows / batch_poll) still waits for each layer's doorbell unless the
stage is all-resident. On a stage with mirrored experts the graph finished with
the ring silent: "verify batch: layer 0 never rang (graph finished)", then
UR_RESULT_ERROR_UNINITIALIZED and exit 139 at the first shared window.
Leave lean off while recording a batch window (batch_rec_). The solo window
stays lean. Batch tokens are identical to a run with STRATA_VERIFY_LEAN=00.
With the default --suffix-draft 3 the suffix block above already raises --spec by 2
(4 -> 6) and writes mtp_max_t = 4. The --lookup-chain block then computed
min(mtp_max_t + K, 8) = min(4 + K, 8), so with --spec 4:
K=1 -> max(6, 5) = 6 (unchanged: the flag does nothing)
K=2 -> max(6, 6) = 6 (unchanged: the flag does nothing)
K=3 -> max(6, 7) = 7
Measured on 0.1.40 by reading the cap back from /metrics engine.spec after a restart
per row; K=1 and K=2 were indistinguishable from not passing the flag.
Size from the cap actually in force instead, so K adds up to K of headroom while the
kVerifyMaxT = 8 ceiling and the mtp_max_t bound on the MTP's own drafts are preserved.
Behavior is unchanged when --suffix-draft is 0 or when the cap is already at 8.
Under WSL2 the NVIDIA driver caps what cudaHostAlloc(Mapped) can pin at about
1 GiB in all, so the KV streaming host copy (layer.cpp) cannot be allocated
and setup turns streaming off there: the whole KV cache must sit in VRAM,
beside the expert cache. cudaHostRegister of ordinary memory is not capped the
same way; the expert arena already registers tens of GiB with it on the same
machines (pinned.cu).
kv_host_alloc() maps anonymous memory and registers it Portable|Mapped, before
any page is touched, when /dev/dxg exists (WSL2, detected as generate.cpp's
under_wddm() does). Elsewhere cudaHostAlloc stays first. Either way the other
method is the fallback when the first is refused, and
STRATA_KV_HOST_REGISTER=1/0 forces the order. On native Linux and Windows the
behaviour is unchanged unless cudaHostAlloc is refused (by inspection; not run
there). The failure message no longer says WSL pins only 1 GiB.
Measured on one RTX 5090, Ryzen 9 9950X3D, WSL2 (157 GiB), Windows driver
591.86, IQ3_S, --max-context 1048576 --rope-scaling yarn --rope-scale 4
--kv int8 --kv-resident 32768, with this commit:
- start: "the K/V in 12.38 GiB of pinned RAM"; expert cache 11,609 slots
- tools/needle_bench.py: 4/4 found, 128K and 1M (1,015,070 prompt tokens) at
depths 10 and 90
- decode 99-124 tok/s with 0.5-1M tokens in context (4 runs); 150-191 tok/s
medians from 55-token prompts to 128K (single runs 113-196); prefill about
4,200 tok/s for a cold 1M prompt
kv_stream_parity still passes; it allocates its own host pools, so it checks
the streaming kernels, which this change does not touch.
Not tested: layer split or remote experts under WSL, Docker Desktop.
setup.py still never writes --kv-resident on WSL and removes it there
(upgrade_config); until that changes it has to be added by hand.
Port of PR #1498 (commit 79b43be18).
pin_cache_complement set lend_from_slot = -1 whenever --resident-budget-gib was given, so the experts of the cache
slots the prompt path borrows were in neither VRAM nor RAM during a prompt and were read from the model files on every
chunk, and again when the slots were refilled - even with room left in the budget. The budget branch now appends the
lend region's experts after the ranked ones, from the last slot down (the order the borrowing takes them), while they
fit; the budget stays a hard cap, and the ones past it keep the file fallback. The start-up line reports the count, and
the lend warning names a larger budget as the remedy when one is set.
Measured on an RX 6900 XT 16 GB (HIP gfx1030), Ryzen 9 5950X, 125 GB RAM, UD-Q4_K_XL, --resident-budget-gib 71 (the
copy held all 63.47 GiB outside the cache, 7.5 GiB unused; 1,231 lent slots = 3.60 GiB), engine 0.1.42, one run per
case, greedy: file reads of the lent experts per request 0.5-2.6 GB -> 0; SSD reads per fresh 8K / 16K prompt 2.8 /
1.9 GB -> 0.3 GB; prompt-path host staging 0.8-2.1 s -> 0.06-0.5 s per request; fresh prompts 887 / 7.8K / 15.8K
tokens 180 -> 194, 633 -> 676, 677 -> 698 tok/s. Decode unchanged.
Port of PR #1896 (commit 29063eb2b).
Rotation hands the evicted expert's exchange buffer over as its place in the
RAM copy instead of copying the bytes there. It required one block size for
all layers and fell back to the copy path on any other pack: Swift 1.5 IQ3_S
has seven sizes (1,510,400 to 2,662,400 bytes) and IQ3_XXS nine.
A swap exchanges two experts of one layer, so the slot the promoted expert
leaves has the size the evicted one needs. ExchangeStorage now keeps its
spares per block size (initialize_sized): an exchange takes a spare of its
layer's size and hands back one of the same size. commit() refuses a spare of
another size and an expert of another layer. initialize() keeps the uniform
geometry and its checks. exchange_buffer() takes the layer; reserve_exchanges()
takes the number of trailing buffers that only stage copies (--adapt-async's
second half), kept in the largest size alone. With --adapt-async 1 a rotated
round flips in the tick that sees the copies in: there is nothing for the job
to move, so the round ends one verify window sooner.
Still opt-in (STRATA_EXCHANGE_ROTATE=1); unset, nothing changes. The exchange
allocation grows to one spare of every size per swap of a round: 1.06 GiB of
page-locked RAM on IQ3_S with --adapt-swaps 80 (1.26 GiB with --adapt-async 1)
against 0.20 and 0.40 GiB.
Measured on a Core i7-13700K, RTX 5090 32 GB, 64 GB, Windows 11, Swift 1.5
IQ3_S, 128K, --resident-experts, --adapt-every 1 --adapt-swaps 80, p-cores:
one 8K prompt, the same 1,500 forced tokens, blocking tier, rotation off / on:
161.6-165.1 tok/s (five runs) / 184.9-188.7 (four runs); two runs of each
arm with the same cache size (10,681 slots) wrote identical --window-hashes
for all 434 windows
--serve, 23 coding-agent turns, greedy, three interleaved rounds, blocking tier:
147.8 / 159.2 / 158.4 (the build before) -> 175.2 / 184.9 / 172.8
the same turns with --adapt-async 1, one binary, two rounds:
166.0 / 170.1 -> 185.6 / 182.5
The tier swaps about 30 experts per verify window on this box (44% of the
experts fit in VRAM), so the copy path moved about 50 MiB of RAM per window.
tests/core/exchange_storage_test.cpp gains a three-size case (6,144 exchanges)
and passes built alone with MSVC 14.50. Built for CUDA only (sm_120, CUDA
13.0.88, MSVC 14.50 with nvcc's --allow-unsupported-compiler); HIP and SYCL
were not compiled (expert_source.cpp and generate.cpp are built by both). Not
measured: Linux, AMD cards, a layer split.
Port of PR #2042 (commit 8db375018).
The multi kernel reads the dense IQ3_S matrices (attn_q, attn_gate, ssm_out, attn_k, the shared experts' gate/up)
at 76-114 GB/s on the B70. The new kernel has the shape of the Q4_K..IQ4_XS wide kernels. unitrace, one card,
IQ3_XXS, T=1: worst attn_k (512 rows) 77 -> 123 GB/s, best attn_q (12288 rows) 114 -> 307 GB/s; these matrices take
0.60 ms of kernel time per token instead of 1.36 ms.
Unset or 0: the multi kernel, output identical to main. With the option the greedy output parts from the default at
token 198 of 256 (the sub-blocks are summed in another order).
Since 0.1.40.2 the waves_per_eu(8) variant of native_w11_kernel is uncapped on clang 22 and newer, because clang 22
computes wrong results with it on gfx1100. On gfx1151 with clang 23 (ROCm 7.14.1) the capped kernels give the same
bits and take less time, as alexdns1 measured in #1180. The cap now applies there again, per format
(hip_prefill_fused_iq, one layer, 2048 / 3584 / 8192 tokens, the same output bits hash in every case):
- gate/up IQ2_XXS / IQ3_XXS / IQ3_S / IQ4_XS and down Q2_0 / IQ4_NL: capped, 8-15% less time per layer.
IQ2_S gate/up is capped too, but its LDS leaves room for 3 blocks per WGP and the compiler keeps its 238 VGPRs;
IQ2_S / Q2_0 takes 2-4% less time from the Q2_0 down.
- IQ2_XS gate/up: not capped. Under the cap it spills 104 VGPRs and takes 61-66% more time.
- Q8_0 down: not capped. It spills 18 VGPRs and took -1.9% to +1.9% time (with Q4_K gate/up).
The test is the clang major version: other clang 23 builds are not measured, and clang 24 and newer keep the clang 22
rule. The template argument is the same in the host and every device pass; only the device pass for gfx1151 decides
the attribute (nw_capped). Other architectures, other compilers and -DSTRATA_W_LB=N behave as before: the gfx1100
disassembly is identical to before (kernel names aside). The file builds for gfx1012 / 1030 / 1100 / 1150 / 1151 /
1200 / 1201 and with STRATA_HIP_GFX906 (gfx906); no CUDA build here, and the changed lines are inside
`#if defined(__HIPCC__)`. SYCL has its own copy of this file, not touched. The path is opt-in (STRATA_PF_FUSED=1), so
default output is unchanged.
End to end on a Ryzen AI Max+ 395 (128 GB, gfx1151), UD-IQ4_XS, the section-5 switches of docs/STRIX_HALO.md with
--prefill 16384, 3 interleaved rounds of fresh servers: a 128K prompt read 1,126.1 -> 1,153.4 tok/s (+2.4%; +1.8%,
+2.4%, +2.6% per round). At 4K (868.0 -> 894.9, +3.1%) and 32K (1,182.9 -> 1,207.0, +2.0%) the medians moved too, but
within the spread of 3 runs. Decode unchanged; the same text for all 9 prompts.
prompt_attn_wmma_kernel interleaved a chunk's global reads with its
compute: the V slice was staged first, then each 16-cell tile loaded its K
scale and fragments between the q.k WMMAs, so a wave waited on each read
in turn (a thread trace on gfx1151: 56% of the kernel's time in
s_waitcnt vmcnt, 0.4% in WMMA). The chunk's reads now go out together
(fetch), then the same compute runs on them (chunk). The values and the
order of the arithmetic are unchanged.
gfx1151, ROCm 7.14.1 SDK (clang 23), hip_prompt_attn_wmma with
STRATA_PA_FAST=1, three interleaved runs against the unchanged kernel,
ms per chunk: 32K context / 2,048 queries 12.50 -> 11.29 (-9.7%),
1,500 / 1,500 3.39 -> 3.20, 2,100 / 256 1.30 -> 1.23. The output hashes
(NEWHASH) match with STRATA_PA_FAST=1 and =0. gfx12 compiles the same
change; it was not run there.
The wide kernels (Q6_K a2 and the generic native_mmvq_wide) spill at 2-8 columns with 128
registers per thread on the A770. They now launch with grf_size<256> there. Large GRF halves the
threads resident per Xe core, so it is the default on Alchemist only (strata::intel_gpu_gen);
Battlemage and other devices, and 1 column on every device, launch the same kernels as before.
STRATA_MMVQ_LARGE_GRF=1 forces it on, =0 off.
mmvq_bench now prints a checksum of the output (FNV-1a over the float bits) so the two modes can
be compared bit for bit.
Measured on an Arc A770 16 GB (xe, oneAPI 2026.1 JIT), same day. mmvq_bench Q6_K, 2560x2560 / 2560x10240 /
10240x2560, 1-8 columns: 1 column unchanged; 2 columns 1.3-2.0x, 4 columns 2.3-3.1x, 6 columns 3.0-5.0x, 8 columns
3.7-7.4x faster (e.g. 2560x10240 at 4 columns 289.3 -> 92.9 us, 74 -> 232 GB/s of weights); checksums identical
with STRATA_MMVQ_LARGE_GRF=0 and =1 in all 15 cases. End to end, a0116b27 + #2190 + #2193 + #2203 with and
without this commit, Coder IQ1_M, 128K context, int8 KV, --resident-experts, STRATA_VERIFY_STEPPED=1, 8K prompt,
256 tokens: decode 23.47 / 23.50 / 23.73 / 23.77 -> 25.99 / 27.10 / 27.49 tok/s (+10-17%), prompt unchanged,
output ids identical where the expert placement matched. Not built or run on Battlemage.
With --batch N the verify window ran QSA block scoring, top-k and decode
attention as one launch per row, and the GDN recurrence (window and 1-row
commit) as one launch per slot. At 4-8 live slots these serial launches are
most of the window's cost beyond the expert reads.
Two env switches, both off by default (off = the same launches as before):
STRATA_BATCH_ATTN_ROWS=1 block scores, top-k and decode attention for all
rows of the window in one launch each; every row
reads its own slot's indexer keys and K/V pools
(passed by value as kernel params, so the graph
captured per row layout stays valid). A slot that
streams KV (kv_mode 1) keeps the per-row path.
STRATA_BATCH_GDN_GROUPS=1 GDN recurrence in the window and the 1-row commit
for all slots in one launch (grid.y = slot group).
Conv commits stay per slot.
Same arithmetic per row: 4 concurrent temperature-0 prompts give the same
bytes with and without the switches (--batch 4 and --batch 8).
RTX PRO 6000 Blackwell, IQ3_S, all experts in VRAM, a0116b2, same day,
total tok/s (docs/BATCHING.md has the table):
--batch 4, 4 clients: 268.4 -> 311.7
--batch 8, 8 clients: 308.4 -> 374.8 (reserve 6144 MiB)
Window: 4 rows 14.3 -> 12.2 ms, 8 rows 24.6 -> 19.9 ms.
Built and run with CUDA 13.0 (sm_120) only. The .cu files are also in the
HIP build, which was not built here (no ROCm). The SYCL port has its own
copies of these files and is untouched.
Q4_K, Q5_K, Q6_K e IQ4_XS: il frammento di attivazione di ogni colonna si carica una volta per blocco
di pesi e serve tutte le righe del blocco; le scale Q6_K stanno in registro. Stesso ordine di accumulo e
di riduzione: uscite identiche bit per bit al layout esatto e alla chiamata a colonna singola
(b6_mmvq_bench, memcmp su entrambe le schede). Default spento.
Stacked on #1415. IQ2_S (type 22, 122 gathered) joins STRATA_CPU_IQ_ALL_EXACT1's list: ggml's AVX2
iq2_s dot sums the same int32 lanes as row_dot_ggml_one and ends with 0.125f * hsum_float_8.
The singleton gate now also requires nt < native_gu_mt_min, so with STRATA_IQ_MT_MIN=1 a lone token
keeps the multi-token kernel and native_expert_parity's width invariance passes with the switch on.
- Q6_K wide kernel (and the planar one): the six-bit offset (v - 32) was dpct's per-byte saturating char4 subtract, a per-byte loop on Xe;
no byte can saturate, so a 4-op SWAR form is bit-exact. Q6_K dense matvecs 59.5 -> 30.8 us (1 row), 77.4 -> 45.5 us (4 rows).
- STRATA_Q6_PLANAR (default on, =0 off): the Q6_K LM head and the MTP draft head are converted at load to ql/qh/scales/d planes (same bytes, the
GGUF-layout allocation is freed: no VRAM change) so a lane's loads are single aligned 16-byte loads; the draft head subset is gathered from the planar
head. native_q6_k_mmvq serves every registered matrix at any column count. Bitwise equal outputs.
- ld_i4_a4: the Q8_1 activation qs (4-byte aligned) as one dword-vector load in the Q4_K / Q5_K / IQ4_XS wide kernels (Q5_K 4 rows 81 -> 68 us).
With STRATA_QSA_SPLIT_ATTN=1 the verify window's and the draft layer's QSA attention run through the prompt path's int8
tensor-core kernel with each query's cells split among up to 20 blocks, and the splits are merged with the log-sum-exp
rescale. qsa_decode_attn_split() returns false (nothing launched) unless the KV is int8 on sm_80 or newer, or when its
partials would not fit the caller's scratch, and on HIP; the caller then keeps qsa_decode_attn_batch(). The sums run in
another order, so with the switch on the output is a rounding-level change. Unset, nothing changes.
The idea is from Eddoursul's fork (fd23d91); the kernel is written against this tree's qsa_prompt_attn.cu.
STRATA_SIDE_MIXER=1 runs the GDN gates and the QSA indexer (key projection, tail snapshot, query chain, block scores,
top-k) on a branch of the window's graph beside the attention's projections, joined before the recurrence and the
attention; =2 also runs the GDN gate projection and the QSA queries' chain on a second branch. A branch that projects
reads F4's interleaved copy, written on the main stream before the fork, and qkv is recorded before the branch so its grid
is dispatched first. Every result is bitwise the same as in line; STRATA_DF_BRANCH is off while this is on. Unset, the
window runs in line as before.
The idea is from Eddoursul's fork (34a35db, b063c13).
native_expert_grouped already decodes each weight part once per block, but the apply re-reads every entry's q8_1 block from global memory with 9 scattered 4-byte loads, so an extra row of the window costs ~60% of a whole expert. expdec_gu_kernel stages the group's entries in shared memory (two conflict-free 16-byte loads per call, the fp16 scale as a float), takes rb rows per block and stages the IQ3 codebooks too; the same lane does the same calls in the same order with the same operations (apply_u = apply on the block's words), so the output is bitwise the default kernels' (expdec_bench: identical on every cell, 2 cards). Gate/up IQ2_XXS/IQ2_XS/IQ3_XXS/IQ3_S/IQ2_S at n_embd 2560; the down (Q2_0, IQ4_NL) behind STRATA_EXPDEC_DOWN=1 (in the engine the Q2_0 down was ~10% slower). Knobs STRATA_EXPDEC_R/_DR/_MINB/_RB/_DRB/_MINBLK/_D20; one log line per (role, format) at the first launch. docs/EXPERTDEC.md.
q5_1_dot and q8_0_dot in kq_avx2.cpp held their per-token sums in the same
runtime-indexed __m256 acc[MAXT] that made bf16_rows_dot_multi spill: with the
array live the compiler leaves it in memory, and every token's FMA reads and
writes a stack slot. Both are now template <int NT>, dispatched 1..4 and, past
that, run four tokens per pass with the row's bytes re-read from L1 - the same
idiom the router change above and kq256_gu_rows already use.
Ryzen 7 9800X3D (Zen 5), GCC 15.2.0, build/native_expert_parity --synthetic
(ggml-quantized weights and activations), one thread pinned to core 0, at the
verify window's four tokens:
Q5_1 down rows [2560 x 640]: 190-197 us -> 155-156 us (1.24x)
Q8_0 down rows [2560 x 640]: 135 us -> 104 us (1.30x)
Same sums: 0 of 36 token-sets differ in any bit against ggml's own vec_dot over
1..8 tokens, both before and after; native_expert_parity reports 0 failures.
Left alone: q4k_dot keeps its runtime-nt array. Its token accumulators are not
what spills - the unpatched function carries 8 ymm stack ops for the sums
against 24 (q5_1) and 59 (q8_0); what q4k_dot spills is its weight-side unpack
arrays (q4l/q4h/scale_l/scale_h), which token chunking does not touch and
re-does. The same change measured +5% at four tokens and -4..-7% at six to
eight, so it is a wash at the window and a small regression above it, and q4k is
the gate/up format that runs most. The AVX1 path, the iq_avx2/q2_avx2 rows and
the engine's dispatch are untouched.
Backends: CUDA and HIP do not compile this file. The SYCL port does; not built
on this machine (no icpx here) - the router change in this branch built there.
Tests: native_expert_parity --synthetic q4_K/q5_1 and q4_K/q8_0 (0 failures,
0 bits differ), iq_avx2_parity, iq_avx1_parity, q8k_quant_parity,
router_dot_parity - all exit 0.
bf16_rows_dot_multi held its per-token sums in __m256 acc[MAXT] indexed by a
variable. With eight 256-bit accumulators live GCC also unrolled the column loop
and left the whole array in memory, so every token's FMA read and wrote a stack
slot (vmovaps 0xc0(%rsp) ... vfmadd213ps 0xe0(%rsp)). On a Ryzen 7 5700X3D one
layer's router [512 x 2560] at 4 tokens took 0.41 ms against 0.25 ms for the AVX1
copy in kq_avx1.cpp that kept its accumulators in registers, so every AVX2 CPU ran
the slower of its two kernels (router_dot_parity --cpu 2, median of 3). Pragma
GCC unroll 1 on the column loop changes nothing; the variable-indexed array is
what forces the array into memory.
Four tokens per pass, in four named accumulators. A row above four tokens is read
a second time, from L1: nt=8 measures 0.37 ms, the same as the spilled form. On
the same CPU the AVX2 kernel goes 0.41 -> 0.19 ms at 4 tokens and nt=2 0.36 ->
0.16 ms, and it now runs 1.3x ahead of the AVX1 copy instead of 1.6x behind it.
The loop body carries no stack slot any more (was 21 memory ops per iteration).
Untouched: the arithmetic and the reduction. Each token's FMA chain still runs
over the same columns in the same order and ends in the same hsum_float_8, so
every sum is bit-identical - the output for nt 1/2/4/8 at cols 2560/5120 hashes
the same before and after (FNV-1a over the whole out[]), and router_dot_parity
reports 0 failures with the AVX2 rel error unchanged at 3.16e-07.
Left alone: the expert row kernels (iq_avx2.cpp, q2_avx2.cpp, kq256_gu_rows,
kq256_rows) and the AVX1 path. The iq_avx2_parity bench over four format pairs x
1..8 tokens moved within 1.6% (noise), and the CPU parity tests (8 targets) pass.
The router stays what it was, an estimate for which experts to prefetch. CUDA and
HIP do not compile this file; the SYCL port does, and it builds there under icpx
2026.1 with no new warnings and no spills either.
The AVX1 copy in kq_avx1.cpp kept a comment saying it reads its rows "exactly as the AVX2
kernel does"; with the row loop chunked four tokens at a time that is no longer the
AVX2 kernel's shape, so the clause is dropped there. Comment only - no code change.
attn_chunk_kernel_pre75 (PR #540 by sskver: two cells per step with a 12-shuffle reduction, cell-major
probabilities, V loaded four at a time) is compiled in every CUDA build, not only with STRATA_EXPERIMENTAL_SM60, and
STRATA_ATTN_DAILY=1 selects it on any card for the decode attention (single query and batch: verify window and MTP
drafter) together with attn_merge_v2_kernel (Hardin22), a merge that skips the chunks the chunk kernel marks empty.
Bit for bit with the default kernels by construction: reduce12 adds the same pairs as warp_sum's butterfly at every
level, the V cells fold in the same order with the same fmaf, the masked cells stay out; the merge does the same fmaf
in the same order. Shared memory 15,872 B like the default kernel (spt shares sq's storage). STRATA_ATTN_MERGE_V2=0/1
and STRATA_ATTN_PRE75=0 switch the two halves separately. HIP unchanged.
qsa_merge_daily_parity (tests): memcmp of both kernels against the default ones on four KV formats, widths at the
chunk borders and non-resident pages; --bench times the chunk kernels back to back.
In a 131k decode block_scores_multi_kernel costs ~20 us more per call for every row of the verify window: not the keys (read once for all the queries) but the shared-memory and shuffle pipes, 4 x LDS.128 of the query slices and 20 shuffles per (block, query). block_scores_rows_kernel<NQ> keeps the NQ <= 4 queries' slices in registers and reduces the four heads' lane products with a scatter that keeps warp_sum's pairings (xor 16 and 8 leave one head per lane, then 4, 2, 1): every addition adds the same two operands of the same level, so scores and top-k ids are bitwise the multi kernel's. qsa_rows_bench measures the window's QSA kernels row by row and, with --rows, checks the parity (16 cases per card, 0 differ) and prints both kernels' microseconds: 2080 Ti 47/57/78/101 -> 36/38/49/55 us at T 1-4, 3060 73/109/152/194 -> 63/76/98/130. docs/QSAROWS.md.
justxiami reported (#1098) a 4xV100 layer split dying with
STRATA_GDN_CHUNK=1. Reproduced on 2xV100: the second card launched the
chunked kernels at the 48 KB default (a process-global std::call_once set
the opt-in on the first card only) and the engine exited with
"gdn_chunk wmma: invalid argument".
Two more per-process assumptions in the same path:
- gdn_chunk_cc() cached the first card's capability for the whole process
- the chunked workspace was thread_local with no destructor: the per-request
stage threads leaked ~50 MB per card per request (the second card ran out
of memory on the next prompt) and one card's pointers could reach another
Now: the opt-in and the capability are recorded per device (each attempt
clears the runtime error, and a refused wmma opt-in falls back to the FMA
path), and the workspace is one per (thread, device) with a destructor.
Verified on 2xV100 32K: STRATA_GDN_CHUNK=1/2 run to completion over repeated
rounds (1: 1900 tok/s prefill, 2: 1766), gdn_chunk_parity 0 failures.
CUDA events over one gdn_recurrence call per side (S=128, 16 q/k heads, 48 v heads, C=10240,
T=1024/4096/8192), all four paths and the max abs diff between the chunked wmma output and the
pipelined recurrence. RESULTS.md records the table and the error bounds for
bench/results/2026-10-02-gdn-chunk (measured on the V100-SXM2-16GB, CUDA 12.8, sm_70 build).
GPU, synthetic, no model. Compares through the real dispatch (STRATA_GDN_CHUNK /
STRATA_GDN_REC_WARP A/B) at T=64/128/200/320 - 200 exercises the < 64-token tail - and asserts
measured bounds, not equality: the recurrence is the FP32 floor (y abs 1.1e-6, state 3.0e-8), the
chain-split and the chunked FMA path are FP32-level (~3e-6), the wmma chunked path is the
f32->f16 staging level (y abs 2.3e-3, state 8.9e-5). The sync-free warp recurrence is asserted
BIT-EXACT against gdn_rec_cols_pipe (memcmp of y and the final state). A NaN or inf anywhere
fails the check outright - an unordered comparison would otherwise leave the max at zero.
On main's 0.1.38 recurrence the out norm keeps its raw FP32 output in place and writes only the
FP16 copy, so the kernel comparisons use that raw output (ref_full's yraw_out) and the norm is
checked once through its FP16 store.
The prompt GDN recurrence is a per-token chain; on a V100 that chain is what the 36 GDN layers
cost. This adds the chunked form (cumsum / kkt / fwd over 64-token chunks, chunk-serial scan,
the state in registers - the gdn-chunk-sm70 structure of llama.cpp-v100, hand-written CUDA):
a chunk is a triangular solve and a batch of matmuls instead of 64 sequential rank-1 steps.
Two variants: an FP32-only FMA scan and an m16n16k16 wmma one with the reference's f32->f16
staging. Both are FP32-level, NOT bit-exact (the sums are reordered; the wmma path stages
q/k/v and the state through f16) - gdn_chunk_parity bounds them.
Also two Volta variants of the recurrence itself: a sync-free warp-shuffle reduction (BIT-EXACT
vs gdn_rec_cols_pipe, but measured 2.6x slower - kept as the bitwise control) and a 4-accumulator
chain split of the pipelined kernel (FP32-level, measured +6-7% at T=4096/8192, the V100 default
now). All of this is compiled only into the STRATA_EXPERIMENTAL_SM60 build and dispatched only
at cc==7.0; sm_80 and up keep the trunk's kernel bit-for-bit. The env switches
(STRATA_GDN_CHUNK=1/2 chunked, STRATA_GDN_REC_WARP=0/1/2) still override.
Measured on the V100 (bench/gdn_chunk_bench, real shapes, one gdn_recurrence call): the chunked
path is correct but 4-6x SLOWER than the recurrence as implemented (4-warp CTAs underfeed the
tensor cores - see bench/results/2026-10-02-gdn-chunk/RESULTS.md), so it stays opt-in.
Fusions (all bit-exact vs the pipelines they replace; bench/micro/fusions_parity.cu
checks every one bitwise and times the before/after at decode shapes):
- gate/up dual GEMV + SwiGLU in ONE kernel (llama.cpp's mul_mat_gated):
s_gemv_pair_silu, one read of the activation for both projections and the
SwiGLU in the epilogue - the up intermediate never lands in memory. Rows are
accumulated by the shared row functions (s_rowdev.cuh) exactly as the
standalone kernels accumulate them. shared_expert uses it whenever both
projections are S-family (bitwise checked for all three silu kinds).
- silu(gate)*up + the intermediate quantizer fused (was 2 kernels):
swilu_quantize_q8_0/_scaled/_q8_K, warp-parallel swilu then the verbatim
quantizer body. Wired into the S2 expert entry points and shared_expert.
- every activation image of x in one pass (was 2-3 kernels): quantize_act_images
produces block_q8_0 + block_q8_K + bf16 from one read of x. Wired into
gdn_layer, qsa_layer and moe_shared.
- multi-weight bf16 GEMV (llama.cpp's mul_mat_multi): bf16_gemv_pair computes
two matrices from one read of x; used for ssm_alpha/ssm_beta and for the
indexer's q/k projections (q and k now one launch).
The call sites are gated on STRATA_EXPERIMENTAL_SM60 (the Pascal/Volta build),
so the ready-made engine runs exactly the pipelines it did before; the fused
kernels are the same code either way.
Measured (V100, CUDA events, n_embd 2560 / FF 640, 200 reps):
expert intermediate (swilu+q8_0) 15.7 -> 11.6 us M=1, 18.8 -> 11.4 M=8
activation images (q8_0+q8_K+bf16) 46.2 -> 30.4 us
dual GEMV + swilu (gate,up) 30.4 -> 14.8 us
bf16 pair (alpha,beta) 24.2 -> 14.4 us M=1, 23.7 -> 14.1 M=8
Parity: fusions_parity 0 failures (bitwise), plus s_gemv/s_gemv_q8k/s2_gemv_q8/
s2_expert_grouped/shared_expert/quantize_act/bf16_gemv/elementwise/mmvq_multi/
iq_multi/native_expert parity suites all pass unchanged.
Port of oldlocal commit 4672cf9 ("kernels (sm_70): the prompt attention's
latency pipeline - 2.2x over the FP32 kernel at 32K") onto the #600
prompt_attn_v70_kernel, the m8n8k4 Volta kernel now inline in
qsa_prompt_attn.cu. Still gated on STRATA_EXPERIMENTAL_SM60, so the
ready-made engine keeps exactly the code it had.
Two of the commit's three changes port; the third (splitting the dependent
WMMA chains into f0/f1 and fh/fl) is already inherent to the mma884 kernel -
its scores run two tg[rb] chains and its p.v four tmp[rb][nt] chains - so
porting it would only change the summation order. The two ported, measured
on a V100-SXM2-16GB (CUDA 12.8):
* the staging loop issues every global load of a thread before any of its
stores (2-4 in flight), instead of paying the HBM latency per piece;
* the next chunk's ids/page-table lookups double-buffer (row[2][CH]) and run
just before the previous p.v, where their latency hides.
The original commit measured, on the #371 kernel, 2,048 queries of a 32K
selection: 34.4 -> 15.8 ms (2.18x) int8, 33.5 -> 15.4 ms (2.17x) fp16 KV,
35.6 -> 20.3 ms (1.76x) K8V4 (its q4_0 V dequantizes to FP16 at staging, the
sm_75/80 kernel's same trade).
This port, same 2,048 queries of a 32K selection (qsa_prompt_attn_sm70_bench,
which also gained the K8V4 pools): the Volta kernel goes int8 12.11 -> 10.25 ms,
fp16 KV 25.70 -> 17.39 ms, K8V4 25.71 -> 24.12 ms, so against the FP32 decode
kernel int8 rises 1.73x -> 2.05x and fp16 0.80x -> 1.18x (K8V4, 0.85x -> 0.91x,
keeps main's #600 dequant-to-FP16 trade).
qsa_prompt_attn_parity passes all four cases. An A/B against the unpatched
kernel shows the kernel's output is unchanged - every FP64 and new-vs-old
figure is identical before and after (int8 5.08e-6, fp16 5.29e-6 vs FP64,
within the 4x gate).
The experimental build (-DSTRATA_EXPERIMENTAL_SM60=ON, Pascal/Volta) gains a
kv_gather_q8_wide_kernel: one uint2 (8 B) load of codes, one uint4 (16 B) store of
the dequantized fp16 and one scale per thread instead of two. The 8 values lie in
one 64-value group and the arithmetic is unchanged ((float) code * scale ->
f16_from_f32), so the output is byte-identical. The ready-made engine keeps exactly
the 4-wide kv_gather_q8_kernel and launches it unchanged, so its gather is bit for
bit the release engine's. On a device below sm_75 the wide kernel is selected at
run time inside the experimental build (STRATA_Q8_GATHER_WIDE=0 opts out).
Measured on 2x Tesla V100-SXM2-16GB (SM 7.0), CUDA 12.8, one card: +11.4% on the
32k gather microbench (15.85 -> 14.05 us/call, 401 -> 452 GB/s). kv_q8_parity
stays OK at worst INT8-vs-FP16 error 0.590 quantization steps.
New requests took the control lines in whatever order the lock fell, so a 500-token chat question could wait
behind several agent reads. With the env, a prompt of at most that many tokens goes first (arrival order within
each class); a longer one that waited STRATA_ADMIT_MAX_WAIT_S (30 s) counts as short. A request already in
acquire() gives the lines back when a shorter one came. Only the order changes, never what is read.
Ported from havok2-htwo/Strata 08830ddf onto 0.1.44.1: _take_control has no image-request line here, so its
gate is only the ordering; the first admission passes new=True.
The refusal 'this CPU cannot run the expert kernel' belongs to the canonical Q2_0 pack. An NVFP4, i-quant or
Unsloth pack is a native pack (native_experts.txt in the --pack folder) and its rows have AVX2 kernels, but a
config whose --pack points at the wrong folder reads like a CPU that is too old. On a CPU without AVX-512 the
engine now says the file is missing, and where it sits when it is one level down (<pack>/pack).
A layer split hands each prompt chunk's rows (21 MB per 512 tokens) to the next card through a pinned
host buffer: one D2H, a host stream sync, a new std::async thread, then one H2D before the next stage's
first kernel. The 3090s here have no P2P (topo: CNS) and cudaMemcpyPeerAsync is driver-staged and
slower (3.4 ms vs 3.2 ms for D2H + H2D), so the copy stays on the host route; nsys showed 3.8-4.0 ms per
512-row hop, of which ~1-1.4 ms was the thread start alone.
With STRATA_HANDOFF_PIPE=P (2..16, opt-in; unset = unchanged):
- the sending stage records an event when its chunk's work is done and one after each of P row-piece
downloads into the same pinned buffer, and waits on the host only for the first (on_stage_chunk's
checkpoint parts and the error check need the state, not the copy);
- the next stage waits on the GPU for each piece's event (cross-device cudaStreamWaitEvent) before that
piece's upload, so its uploads overlap the remaining downloads (separate PCIe links); a smaller
receiving chunk waits for every piece, then copies as before;
- the next stage's chunks run on one worker thread per sending stage, started and pinned once, whose
packaged_task futures carry the same results and exceptions as std::async's (it clears the CUDA error
state after each job, as a thread's end did). STRATA_HANDOFF_PIPE_WORKER=0 keeps std::async (A/B).
Same bytes in the same rows before the next stage's first kernel: exact. No device memory (events only,
the existing two pinned buffers per stage). Measured (T16-143l5, P=8): 512-row hops 3.8-4.0 -> 1.85-1.89
ms, 143-row hops 1.6 -> 1.0 ms; reads 1.2K about -1.5 %, 150 about -1 %, 9K (agent, --kv-resident) -3 %;
decode rows/s unchanged. batch_test16/32 (greedy and temperature 0.7), batch_interleave_test and
batch_overlap_test (+ the stage fault arm) byte-/token-identical to the reference dumps; an agent-config
run with checkpoint parts byte-identical to the reference binary.
STRATA_SCORES_MULTI=0, the pre-sm_75 attention (STRATA_EXPERIMENTAL_SM60 builds on a
V100/P100) and the gfx906 query swizzle make qsa_block_scores_slots or
qsa_decode_attn_slots decline; the engine keeps its per-row calls there, so a decline
is no failure. A declined case prints SKIP, the test exits 77 when every case was
declined, and STRATA_SCORES_MULTI=0 skips up front.
In a batch window every row reads its own slot's indexer keys and K/V, so each QSA layer ran the
selection and attention once per row: four rows (16 slots) or eight (32 slots) times qsa_block_scores,
qsa_block_topk and qsa_decode_attn_batch, each launch small (the top-k on one CTA, the attention on
cap/64 x 2 blocks). With the switch, per window (up to 8 rows per launch):
- block_scores_slots_kernel: grid (256, rows), blockIdx.y runs the single-query block_scores_multi_kernel
body with that row's slot keys (block_scores_multi_kernel is now a wrapper around the same body)
- qsa_block_topk over the rows in one call (it only reads the rows' scores and steps)
- attn_chunk_slots_kernel: attn_chunk_kernel's body with the row's own QsaAttnPools (an 8-entry table
passed by value, blockIdx.z picks), then the existing batched merge
Each row is computed by the same kernel body with the same reduction order as its own call. The
launchers decline (the per-row calls run) where that call would pick another kernel: STRATA_SCORES_MULTI=0,
the pre-sm_75 / gfx103x attention, the gfx906 query swizzle. A streamed KV cache keeps the per-row
resolve-then-attend loop. Default (switch off): unchanged.
Checked on 4x RTX 3090, T16-143gsc setup (layer split 12,24,37, int8 KV):
- SASS (sm_86) of every existing kernel in qsa_select.cu and of the default attn_chunk_kernel<M> and the
merge is identical before/after; the opt-in lane-cell instantiations differ only in register allocation
- new qsa_slots_parity (ctest, also with STRATA_ATTN_LANECELL=1): fp16, int8, q4_0 and int8-K/q4_0-V
pools, 1/2/4/8 rows of different slots with their own page tables and contexts 37..32768: scores, ids
and attention bitwise identical to one call per row
- batch_test16/32 (greedy and t=0.7), batch_interleave_test and batch_overlap_test (STRATA_PIPE_OVERLAP=1)
token-identical to the build without the change; with the switch off batch_test16 is byte-identical too
- batch decode rows/s (engine level, 30 s, back to back with the reference): 16 slots 683 -> 726 (+6.3 %),
32 slots 750 -> 802 (+6.9 %); STRATA_VERIFY_PROFILE "QSA projections + attention" per stage-window
0.89 -> 0.52 ms (16) and 1.52 -> 0.79 ms (32)
HIP: the new code uses only what the existing kernels use; SYCL has its own sources and only sees the new
declarations. Neither was built here.
Upstream's non-finite flush only marks running slots `tainted`; their checkpoints stayed. With
STRATA_SLOT_CKPT_MOVE a running slot's checkpoints are a resume source (active_source) and a slot's
moved turn checkpoint can be restored into the main session (lent_slot), so a request right after the
flush, logged as "reading this prompt from token 0", still continued from a pre-flush checkpoint.
- the flush drops a running slot's checkpoints too (only with STRATA_SLOT_CKPT_MOVE, the one reader of
them; they go back to the checkpoint pool)
- active_source and the lent_slot search skip tainted slots
Checked with STRATA_NAN_INJECT=600 on the T16-142gsc setup (next turns of chats still running in
slots): before, the two requests after the flush resumed 1628 tokens from slots 0 and 8 ("gave back ...
its turn checkpoint"); now both read 1661 tokens from token 0. A next turn before the flush still
resumes from its running slot. Default path (switch off) unchanged.
On a layer split with more than one group a batch read never gives way to a
BYIELD, so step 4 always failed with 'no such free slot' and steps 5-7 never
ran. --yield auto now expects the give-way only on a non-pipelined batch;
there D is read whole and E admitted beside it, both still equal to solo.
New step 7b: A ends in slot 1, --flush unrelated solo GENs, then F2 can only
start from slot 1's turn checkpoint. --dump writes every token list for an
A/B of two builds.
mmq::whole_tiles walks every visible GPU and every tile width through the
MMQ config table; the sort hook called it twice for each MoE layer of each
prompt chunk, on the host path between the routed-ids download and the
expert launches. Its verdict depends only on the layer's weight formats and
the device set, so MmqPlan now keeps it as a per-layer flag (taken only when
STRATA_MMQ_GROUP_SORT=1 and STRATA_MMQ_RESIDENT_SORT_NE is unset) and logs
how many layers qualify. Same order, same bits; without the switch nothing
new runs.
A pipelined batch window is followed on its stage's stream by the layout's commit graph, and the stage counts
as done only after both. When every row of the window is another slot's one token, that commit is
unconditional (each slot keeps its one row; commit_slot_prefixes allows nothing else), and most of the
graph only repeats what the window wrote: it puts each indexer tail back from its snapshot and appends the
same row again, and copies each PLE history back from the window's own snapshot of it. What it adds is the
GDN conv history and recurrence state, which the window ran without writing.
With the switch, such a window advances the GDN conv history (gdn_conv_l2_multi with commit) and state
(gdn_step_norm_multi with n_keep = 1, the kernel the commit graph runs for a one-row group) itself, as a
one-token solo window does, and takes no tail or history snapshots. No commit graph is captured for the
layout; batch_launch and commit_slot_prefixes launch none, layout_captured and ensure_batch_layouts do not
ask for one. Layouts with proposal rows (--batch-mtp) keep the commit graph. With the switch off every path
is unchanged.
Measured on 4x RTX 3090, UD-IQ4_XS, T16-142fcb (split 12,24,37), decode of 16 / 32 slots, 30 s each,
reference and new build back to back: 653.7 -> 686.0 rows/s (+4.9 %) and 714.1 -> 752.6 rows/s (+5.4 %);
the group step period 24.5 -> 23.3 ms and 44.85 -> 42.6 ms. STRATA_VERIFY_PROFILE: the window's GDN
recurrence takes 0.09 ms (16) / 0.18 ms (32) longer per stage-window with the state written in it.
batch_test16 (16 slots, a 1,200-token prompt) is token-identical to the reference dump; batch_test32
32/32 identical and its dump equal to the switch-off run; batch_overlap_test (STRATA_PIPE_OVERLAP=1) passes
and is token- and RESUME-identical to the switch-off run; batch_interleave_test parts 1-3 identical (its
part 4 fails at the same point with the reference build).
STRATA_MMQ_RESIDENT_SORT_NE=1 puts a fully resident layer's experts in row-count order, so each
16-expert group's max_rows (its tile padding) is close to its experts' own. It also moves the hottest
experts into the last, partial group, whose few tiles take MMQ's stream-k split: the K boundaries move
with the group's tile count and the prompt's bits change.
The new switch sorts only order[0, n - n % 16] (mmq_resident_sort_span) and leaves the partial group's
experts, and so its rows, where they were. A full group's launch has 160 (gate/up) or 320 (down) tiles
per column tile; mmq::whole_tiles checks on every visible GPU that every such launch runs one block per
tile (tile efficiency >= 90 %, NVIDIA) and that all tile widths share the I/K tiles and SRAM layout. Then
each expert's rows get the same bits in any full group whatever its group-mates and max_rows. Where it does
not hold (e.g. 108 or 128 SMs, AMD's stream-k grid) the switch does nothing. RESIDENT_SORT_NE takes
precedence when both are set; with neither the code path is unchanged.
prefill_mmq_tab_test part 4 runs a layer's groups as prefill.cpp does (src-gathered q8_1 rows, absolute
bounds, the group's max_rows) in ID order and in both sorted orders, for IQ3_S / IQ4_XS gate/up and
IQ4_NL / Q8_0 down: with the partial group kept every token's rows are bitwise equal (16 cases); sorting
the partial group too changes 133-1881 rows per case. 890 comparisons, 0 failed (RTX 3090).
Measured on 4x RTX 3090, UD-IQ4_XS, T16-142fcb (split 12,24,37, every expert resident), read time,
medians of 6, back to back: 150-token prompt 145.8 -> 136.2 ms, 1,200-token prompt 428.6 -> 363.4 ms
(RESIDENT_SORT_NE: 134.9 / 358.6 ms). The final residuals of five prompts (300-1,200 tokens,
STRATA_PREFILL_DUMP_R_ALL every position and STRATA_PREFILL_DUMP_R fp32) are byte-identical to the
reference build; with RESIDENT_SORT_NE every row differs. batch_test16 (16 slots, a 1,200-token prompt)
is token-identical to the reference dump.
STRATA_SLOT_CKPT_MOVE: an active slot is a slot source only when the main
drafter's rows come along or are not drafted from (no --mtp, --batch-mtp,
or a batch admission). The slot copy takes the main layers' cells only, so
a solo GEN beside active slots would draft from another conversation's
drafter rows below its start; it reads its prompt as with the switch off.
STRATA_CKPT_THREADS: a checkpoint thread widens its CPU mask before it
goes to the aux CPUs, so the mask the aux placement restores (a request
with aux_cpus=0) is the wide one, not the host thread's single CPU.
STRATA_ADMIT_MERGE_FIRST: a merged first window counts as one row in the
STRATA_DECODE_TIMING window statistics (its prompt rows are not decode rows).
The direct slot copy (STRATA_SLOT_COPY_DIRECT) waited for each of its ~116 copies on the private copy
stream. With the switch, conversation_checkpoint_copy and conversation_kv_copy only queue them (new
`queued` argument, default off), and the slot copy's closing sync_all is the one wait per device. A
streamed K/V state (kv_mode != 0) has its residency refresh on the legacy stream, which is not ordered
after a non-blocking stream: conversation_detail::order_after_queued records an event on the private
stream and makes the legacy stream wait for it first. The copies stay on the private non-blocking stream.
Same copies in the same order on one stream: tokens are identical. Measured on 4x RTX 3090 (T16-142,
24 admissions): the slot copy takes 0.5 ms instead of 1.3 ms; batch_test16 16/16, batch_overlap_test
and batch_interleave_test token-identical to the references; a --kv-resident 20480 run with a 21K
prompt is identical with the switch off and on.
A batch admission copied the main chain's longest checkpoint into the slot (~112 MB of host vectors, 7.6 ms, ~60 ms
on fresh pages). With the switch, the request's own turn checkpoint (or one it took out of the slot it is admitted
to) moves instead; a root, a pinned prefix and any other checkpoint the chain shares are still copied.
The chain then no longer holds it, so the request path also looks in the slots:
- while the main session holds its tokens (live, its cells below it), a slot's checkpoint is restored on the main
session as a chain one would be, with no K/V copy; when that slot is the one the request is admitted to, it moves
back into the chain first (the admission drops the slot's checkpoints), else the slot keeps it and the admission
copies it as it copied the chain's;
- an active slot's checkpoints are a slot source like an idle slot's (its windows write its cells from p on): a
second request with the same history after the main session moved on restores instead of reading it all;
- a slot source that is the admit slot moves its checkpoint into the chain instead of copying it.
The switch also keeps the dropped checkpoints' storage as STRATA_CKPT_POOL=1 does: a moved checkpoint frees
nothing at the next prune, so without it every save faulted in fresh pages (17 -> 38-60 ms, more than the copy saved).
Off by default: nothing changes. A session SAVE or a parked conversation right after an admission lacks the moved
turn checkpoint (the slot holds it).
STRATA_CKPT_POOL=1 (off by default): the serve loop keeps the running-state
vectors of the checkpoints it drops (LRU eviction, the tail one, the prune at
a new request, a batch slot's old one), at most two, and the next checkpoint
or slot copy goes into them. A save then resizes nothing: no zero-fill of
~118 MB of fresh vectors. Every byte is overwritten by the copy.
STRATA_CKPT_THREADS=1 (off by default): a layer split's checkpoint saves the
first stage's part on the calling thread and every later stage's on a
persistent thread of its own (the copies' private streams are per thread, so
short-lived threads would leak them). The threads leave the host thread's one
CPU first (to the aux CPUs with --aux-cpus, else to every CPU the process may
use): on it the copies only took turns.
Measured on a 4-stage split (T16, 24 batch admissions per arm): a turn
checkpoint takes 17.2 ms today, 10.4 ms with the pool and 5.6 ms with both;
the batch tests, the overlap and interleave tests and a driver run give the
same tokens with the switches on as off.
A batch admission read its header through the verify windows (T=4 + T=2 for the
usual 6 tokens) and then its first token in a window of the last prompt token
alone. With the switch the last windowed part leaves its last tokens (up to the
verifier's max_t, 8, the last prompt token included) to the first window: the
part's earlier tokens are read as before, the first window commits every row,
the drafter gets the rows of the prompt tokens in it as the prompt windows give
them, and only row T-1's output is emitted. Row T-1 sits at the first window's
position with the same history and Philox draw, and a row computes what it
computes in any window while every expert is in VRAM, so the token and the
state are the same.
Only for a BGEN admission with every expert of every stage in VRAM (no loan,
no swap pending, the zero-doorbell graphs), and not with --batch-mtp, the
pipelined windows, pictures, the elastic K/V, a peer device, remote caches,
foresight swaps, STRATA_LOGPOS or a checkpoint at the last token. The merged
window takes no drafter q (STRATA_SPEC_PROB), adds nothing to the draft
counts, and the "PP <n-1>" line is printed before it. Verifier::max_t() is new.
Off by default: the same path as before. Tokens (4x RTX 3090, split
12,24,36, 16 slots, STRATA_IQ_MT_MIN=1): 24 admissions of prompts of 3 to 65
tokens and chat headers with and without an assistant prefill (merged T 3 to
8), greedy, seeded and with penalties, on vs off identical; batch_test16 16/16
and equal to the earlier dump; batch_overlap_test token- and RESUME-identical;
batch_interleave_test equal; 47 sampled driver requests equal.
Measured (24 admissions, ~131 and ~1195 tokens): header windows 23.5 plus
first window 11.5 ms become one 7-token window of 27.6 ms, 7.4 ms less per
admission (send to first batch token 246.8 -> 239.4 ms short, 656.1 -> 648.5
long). A window row costs ~2.4 ms up to T=6 and ~4 ms at T=7 and 8 (T=1 11.4,
T=7 28.6, T=8 32.1 ms).
A pipelined read never gives way (batch_read_piece returns 0 when piped), so a
BYIELD that reaches the main loop is for a read that has ended and is dropped.
The main loop still drained the groups for it first, as for a request, unless
STRATA_PIPE_OVERLAP was set. With the switch the BYIELD is dropped as it comes.
A BYIELD the server sends during the read is queued and taken right after BADM,
before any group is restarted, so that drain costs nothing today (0 groups in
flight, 0.0 ms). Only a BYIELD that arrives once the groups run again (the
server reads the control lines late) paid for it: measured with one sent 60 ms
after BADM, drain 26 ms with 4 groups in flight and the first rows 50 ms after
it, a 23.5 ms gap in the slots' rows and about 14 rows lost (at 576 rows/s). With
the switch the longest gap is 7.1 ms, as in plain decode.
Off by default: the same path as before. Tokens: batch_overlap_test (a BYIELD
during B's read) on vs off token- and RESUME-identical, batch_interleave_test
output byte-identical, batch_test16 16/16, and 41 sampled requests of the
admission driver (8 with a BYIELD while 4 groups were in flight) equal.
One stderr line per batch admission ("strata trace: admission of slot N: ...")
splits it into the drain before BGEN, the prompt (batched read, header windows,
the checkpoints' device syncs, copies into RAM and eviction, the rest), the first
window, the slot copy, freeing the slot's old state and the copy of the turn
checkpoint into the slot. Each checkpoint gets its own line, and with pipelined
groups "first rows N ms after the admission" times the refill after BADM. A drain
for another line says how many groups were in flight and what it cost.
Only with STRATA_TRACE set; without it the log and the tokens are unchanged.
Measured (4x RTX 3090, split 12,24,36, 16 slots in 4 groups, 24 admissions):
drain 25-26, header windows 23.5, checkpoint 17.3 (sync 0.1, save 17.2), first
window 11.5, slot copy 1.3, slot freed 0.0, checkpoint copy 7.8 (60 on a slot's
first fill), refill after BADM 23.7 ms.
The >8-slot cap ran only before the slot sessions were carved. When fewer
sessions fit than asked for, the groups were resolved again with no row bound:
auto could pick a group of more than kVerifyMaxT slots (PGroup's rows overrun,
every window refused) and a --batch-groups N that no longer divides the slots
fell back to one serial group whose windows reach only the first 8 slots.
slots_in_windows() picks the most slots whose groups, resolved as
batch_groups::resolve does (a number given, auto, the default's one group for
--adapt-async 1), divide them with at most kVerifyMaxT rows each and at most
one group's worth per stage. It runs before the groups are resolved and again
on the slots that fit (the extra sessions are dropped like the per-stage trim);
the pump refuses a group over kVerifyMaxT rows as a safety net. The hand-off
buffers hold one row per slot only for pipelined groups, so --batch-mtp keeps
upstream's kVerifyMaxT rows. Log and comment wording without the g3 tags.
- The build option is off by default and why (36-38 MiB of VRAM per GPU under EAGER, ~70% of the upstream MMQ
instances' compile time per arch); the nine types with a pointer table.
- The skip-gather hack's 225/577 ms are no bound: it changes the routing and does less work.
- Engine exactness as measured: first-window logits (STRATA_DUMP_FIRST_LOGITS) and greedy tokens byte-identical,
2 vs 1 vs 0, on the 4-GPU split with --prefill 2048 (the streamed walk), with 9 GB less expert cache per GPU
(mixed in-place, copied and mid-group-flushed groups), and on one GPU with --expert-cache-per-layer.
The file mirrors parts of llama.cpp's ggml-cuda mmq.cuh; like the other
ggml-derived kernels (native_moe.cu etc.) it now includes the copyright and
permission notice the MIT license asks for. Comment only.
- ExpertCache::slot_guard() returns the guard open_sized applied (guard_, reset by close), not the request: 0 for
uniform slots, when closed, and on SYCL, whose open_sized applies none.
- tests/core/expert_cache_slot_guard_test: slot_stride, slot_room and slot_tail on a uniform cache (empty off_,
including the last slot) and on sized caches with and without the 256 B guard; no guard gives today's offsets;
close and reopen.
- generate: the "+256 B guard per sized slot" line only for a sized cache; a uniform one says it has no guard.
- prefill: STRATA_MMQ_DIRECT_LOG=1 (with STRATA_MMQ_DIRECT=1) prints each stage's totals of experts read in place,
resident and streamed after every prompt, so an engine run can show which walk read what.
Review fixes for the pointer-table MMQ build:
- STRATA_MMQ_TAB defaults to OFF, like STRATA_MMQ_KQUANTS: the engine loads every kernel at start
(CUDA_MODULE_LOADING=EAGER), and the nine tab instances took 36-38 MiB of VRAM per GPU (sm_86, measured with
a cudaMemGetInfo probe linked with and without the tab objects; equal under LAZY), with STRATA_MMQ_DIRECT unset
too. prep_group_kernel is compiled only with STRATA_MMQ_TAB, so a build without it has the same device code as
3adce85 (the 58 cubins and PTX of build/strata are byte-identical to engine/strata-g3b's).
- The fingerprint block and the tab TUs are skipped on STRATA_HIP_GFX906: that build compiles no .cu from here, so
the define, the uncompiled TUs and an unlinkable prefill_mmq_tab_test no longer appear.
- Tab instances only for Q2_0 IQ2_XXS IQ2_XS IQ2_S IQ3_XXS IQ3_S IQ4_NL IQ4_XS Q8_0, the types prefill_mmq_tab_test
checks bit for bit; Q5_0 and the K-quants (STRATA_ORCA_Q4KS_MMQ, STRATA_MMQ_KQUANTS) keep the gather.
- overread_bytes takes the largest over-read of any config, not the over-read of the largest K tile (the over-read
is not monotonic in the tile length).
- The test fails (instead of skipping) when a tested type has no tab, checks that Q5_0/K-quants report none, and
pins the sm_86 over-reads (0, 72, 136 B); the dead K-quant scale branch is gone.
- SYCL's moe_mmq.dp.cpp gets the STRATA_MMQ_DIRECT stubs (false/0), for a future migrated prefill.cpp.
- moe_mmq.hpp points at docs/DETAILS.md.
MMQ loads a whole tile's ids, and its stream-k fixup one per column tile of max_rows, past an
expert's last row (upstream behaviour on both paths; the values are not used). With the map
padded, compute-sanitizer memcheck runs the whole test clean: 0 errors, 874/874 bitwise equal.
A pointer taken from a dynamically indexed array parameter (mmq_x_tab by value) made nvcc copy
the table to local memory and read the weights through generic LD.E loads instead of
upstream's LDG.E.*.CONSTANT: the products ran 1.2-2.5x slower than upstream's in the unit test
(Q8_0 down 2.2-2.6x, IQ4_NL 1.7-2.1x, IQ3_S 1.2-1.35x), which ate most of the gather saving.
The kernel now takes x0..x15 as `const char * __restrict__` parameters (like upstream's x)
and picks an expert's with a select chain: same SASS load mix as upstream (no local memory),
kernel time within 1% of upstream over every type and J in prefill_mmq_tab_test (69.7 vs
69.3 ms in total), and still 874/874 comparisons bitwise identical.
mmq_plan marks a native layer direct when the pointer-table MMQ covers both products, gate/up
reads nothing past its matrix, the down over-read (72 B IQ4_NL, 136 B Q8_0) fits the 256 B
guard, and the blob is exactly [gate | up | down] in MMQ's matrix sizes; it says once how many
layers qualify.
In the MoE walk (both the staged and the streamed walk) compute() takes the expert's cache slot:
a resident expert whose slot has room for the over-read (slot_tail) and a 16-byte aligned blob
is not gathered - the group's pointer tables point at its gate|up and down in the slot, and the
guard goes into the group's GuardTab. Streamed, peer and unguarded experts are gathered as
before, at compact positions c = 0..ns-1 (gather_native, or gather_native_group with its
mid-group flushes unchanged); the end-of-group flush runs even when the last entry was direct.
At the group end one prep_group launch zeroes the direct experts' guards and the tails after
position ns, replacing the two memsets; both products run with the same n, bounds, ids,
max_rows and buffers plus .tab. STRATA_MMQ_DIRECT=0 (default) and 2 run the existing body.
PrefillStats::experts_direct, said on the generate summary when non-zero.
ExpertCache gets inline set_slot_guard / slot_guard / slot_stride / slot_room / slot_tail; the
4-argument open_sized declaration is unchanged (the SYCL engine defines it out of line against
the same header and ignores the guard). open_sized lays slots out at slot_stride(blob, guard);
blob_ stays the largest blob, so fills, verify_slot and the HIP staging read no further.
slot_tail works on uniform caches too (through slot_offset).
generate.cpp: slot_guard = Prefill::slot_guard_bytes() (mmq::direct_slot_guard(): 256 when
STRATA_MMQ_DIRECT is 1 or 2 on a pointer-table build, else 0) for native packs; the seven
round-256 slot costs (split-skip-if-fits, split placement, per-layer sum, profile sizing,
shrink_to, cache_bytes, stage sizing) use ExpertCache::slot_stride(b, slot_guard), and the
primary and stage caches get set_slot_guard before they open. Guard 0: the same arithmetic as
before, so the default layout is unchanged byte for byte.
A Strata-side copy of llama.cpp's MMQ outer scheduler (mul_mat_q, launch_mul_mat_q,
mul_mat_q_switch_J; fallback=false only) in src/prefill/moe_mmq_tab.cuh: each expert's base
address comes from a 16-entry table passed by value instead of x + z * stride_channel_x.
The launch geometry (J, tiles, stream-k grid and k split, fixup) is computed from the same
mmq_args, and process_tile / load_tiles / vec_dot / write_back / the fixup kernel are
upstream's own templates, so the results are the same bits. Instantiated per type from
mmq_tab_instance.cu.in in strata_mmq (same flags); moe_mmq.cu sees declarations only.
CMake turns it off unless the SHA-256 of mmq.cuh's mirrored region (mul_mat_q_process_tile
through mul_mat_q_case) is a verified one: 3cf03257 (the fetched pin; the vendored tree's
mmq.cuh is byte-identical). HIP and SYCL get nothing; Product::tab is a default-null field.
Also: overread_bytes, tab_fits, direct_mode (STRATA_MMQ_DIRECT, read once, 0 unless the tab
is built), direct_slot_guard, and prep_group (one launch: zero the direct experts' guards and
the two group tails). Nothing calls them yet; the default path is unchanged.
tests/cuda/prefill_mmq_tab_test.cu (STRATA_BUILD_TESTS): 874 comparisons on an RTX 3090, all
bitwise identical - IQ3_S gate/up, IQ4_NL and Q8_0 down (1..16 experts, rows
{0,1,7,8,9,31,64,100,512,1000}, every J 8..128, stream-k fixup on and off, max_rows above the
largest count), every other instanced type, mixed direct/gathered groups (compact c != q,
mid-group flush, last entry direct); negative control: guards left as fp16 NaN give non-finite
down outputs.
The main sessions and the batch slot sessions of a stage live on the same device and are
carved alike, so copy_to_slot / copy_from_slot (without a checkpoint) now copy the running
state and the QSA K/V directly (conversation_checkpoint_copy, conversation_kv_copy:
the snapshot code's private non-blocking copy stream, one device sync before and after) instead of the
checkpoint/K-V image round trip through host memory with a sync per layer. Same bytes into
the same places in the same order; the residency refresh for streamed K/V is shared with
conversation_kv_restore. Opt-in: STRATA_SLOT_COPY_DIRECT=1 (default: the host path,
as upstream).
4x RTX 3090, UD-IQ4_XS, --batch 16, split 12,24,36 + --trim-stage-weights: admission copy
65.6 -> 34.0 ms average (pure copy ~8 ms); tools/batch_test16.py 16/16 and
batch_interleave_test.py (incl. next turn from its slot) identical to solo.
Measured on 4x RTX 3090 (UD-IQ4_XS): 16 and 32 slots decode bit-identical to solo
(tools/batch_test16.py / batch_test32.py); 16 slots 603 tok/s batch decode vs 345 with 8.
Rebased on 0.1.42: the stages' batch row check (batch_rows_error, upstream 48f2c47d) bounds
hbase + S by the hand-off rows; set_handoff_rows passes the per-slot size there (only above 8 slots).
On a Mac the Monitor tab's GPU cards were empty: telemetry had NVML (NVIDIA) and the amdgpu
sysfs files (#301) and nothing else, so the Metal backend read nothing. The Apple GPU's load
and memory are in the IOKit registry's IOAccelerator nodes (ioreg -l -w0 -r -c
IOAccelerator, 22 ms), and the device's memory budget is the same Metal property the engine
reads (metal/src/mtl_rt.mm's recommended_working_set, the engine's "GPU working set" log line
- 26,800,603,136 bytes here, which is 25.0 GiB).
_Metal (a new reader beside _Nvml and _Amd): the index'th accelerator node's PerformanceStatistics
give Device Utilization % (older chips: the renderer or the tiler) and the in-use system
memory; the total is the working set, read through the Metal framework and objc_msgSend (no
PyObjC). The temperature and power sensors are not readable
without root (powermetrics), and an Apple Silicon GPU sits on the die, so there is no PCIe
link: the cards show a dash and static gpu_note says why, the same way #1380's note does for a
Windows AMD card. gpu_reader takes `amd` first, so the AMD trees still read on a Mac for the
tests, and a missing selector is asked for before it is sent (an unanswered one raises an
Objective-C exception no ctypes call can catch).
Service.free_vram_mib now goes through gpu_reader instead of _Nvml directly, so the Mac gets
that reading too (it used to return None there).
Measured on an M1 Max (macOS 26.6.2): through the server, GET /metrics during a request -
gpu_name "Apple M1 Max", gpu_vram_total_mib 25559, gpu_util 74-77 % while decoding (9 % once
idle), gpu_mem_used 10.5 GiB in use of 24.96 GiB, cpu 12.6 %; the plain-text
/prometheus-format metrics carry the same series. serve/test_server.py: MacTelemetry
(fixture registry: two accelerators, index selection, renderer/tiler fallback, a raising
runner, the note and the samples) plus AmdTelemetry, TelemetryNoDisks, Monitor and the full
serve/test_server.py (318 tests) pass on the same box.
What it leaves alone: the NVIDIA and AMD paths (a test-only gpu_reader order change on
macOS), and the web app - the cards read the series it already had.
Co-authored-by: Demetree Kallergis <[email protected]>
setup.py returns from mac_setup() before its own argparse runs, so a Mac never
sees the --source that sets STRATA_SOURCE, and the Mac parser did not define it
either: the flag the download failure recommends landed in parse_known_args'
leftovers and was dropped with "not used on a Mac".
Define it in the Mac parser and set STRATA_SOURCE from it, mirroring setup.py.
Recorded with the hardware they were measured on, per the measured-numbers rule, because
neither can be fixed here: mm_gu3's fault is a threadgroup-count threshold of the
cooperative-tensor path on this GPU (the fix shape would change the M5's register-operand
GEMM), and router_route's race has no acquire atomic to fix it with on this Metal
(device-scope atomics are relaxed-only, macOS 26.6). Both are opt-in and off by default;
the engine's M1 defaults are the v2 expert GEMMs and the separate gemv + moe_route, which
run on this box.
Measured on an M1 Max (macOS 26.6.2, Q2_0): mm_gu3 with 35 threadgroups passes and 40
faults (data-independent; the engine's tile 16x32x32 SG 4 and the v3 test's 190 tiles both
fault). router_route's logits are bitwise gemv's, but two identical runs give different
ids, weights and plan.
Co-authored-by: Demetree Kallergis <[email protected]>
Two stale defaults, both of which kept the test from covering what the engine ships.
The default tile was TM 16 TN 64 TK 16, but the v1 kernel set only instantiates TK 64: the
default run died at pipeline creation on any GPU (measured on an M1 Max:
"mtl: no function mm_gu_42_16_64_16_2"). TK is now 64, which both the v1 and the v2 sets
instantiate, so the default run and --v2 both work; the engine's v2 tile for Q2_0
(16,64,16,2) stays reachable with --tile.
--v3 bound the v1 kernels' argument list ({W, x, H, stok, tiles, ma}) to the v3 kernels:
tiles was read as the sorted-token buffer and MmArgs as raw int2 pairs, so every stride,
n_embd and expert id was garbage. On the M1 Max that is a deterministic GPU
command-buffer error in the GEMM pass; with the right arguments the v3 kernels run, and a
separate M1 limit shows (≥40 threadgroups of mm_gu3 fault, 35 pass, data-independent)
that the engine does not hit: gu_v3 = dn_v3 = coop_k4, which is false on the M1.
v3 now binds {W, XS, HS, tiles, ma} as the engine does, fills XS for v3 too (v3 is v2's
gather plus the register-operand GEMMs), and binds HS - not H - to down3 with DSG
simdgroups.
Measured on an M1 Max (macOS 26.6.2, Q2_0, T 256, 64 experts): default (v1) PASS, rel L2
2.53e-06 vs the fp16-operand reference and 2.37e-04 vs exact; --v2 PASS, 4.42e-06 and
2.37e-04, also with the engine's tile (16,64,16,2).
Leaves alone: no kernel, engine or other test change.
Co-authored-by: Demetree Kallergis <[email protected]>
qsa_attn_mm and qsa_flash (metal/kernels/qsa_prompt.metal) fill their single-simdgroup
matmul2d right inputs with
rk[i] = id >= 0 ? half(float(kq[row * QD + k]) * float(ksc[...])) : half(0.0f);
and that shape - a select with the device load in the arm - loses the threadgroup on an M1
Max: every dispatch of the two kernels fails with
kIOGPUCommandBufferCallbackErrorInnocentVictim, deterministically, and they run only under
MTL_SHADER_VALIDATION=1. qsa_prompt_parity dispatches qsa_attn_mm the way the file intends
(it is the matrix-unit attention of a prompt chunk) and faulted on its first dispatch before
this change. The engine reaches the same fills through qsa_flash, which it dispatches under
STRATA_METAL_PROMPT_QSA=flash; its default on an M1/M4 is the fp32 qsa_attn, which never
touches these kernels - that is why the default prompt path kept running while the parity
test could not, and why this is measured on the test rather than on a request.
The row is now clamped with max(id, 0), the load is unconditional, and the select is on the
value. For an id >= 0 the value is what it was; for an id < 0 it was discarded either way, so
the results do not change.
Measured on an M1 Max (macOS 26.6.2, Metal compiler 32023): qsa_prompt_parity ALL PASS -
qsa_attn_mm vs qsa_attn rel L2 3.09e-04 (pos0 0) and 3.56e-04 (pos0 30000), qsa_flash 3.72e-04
and 4.22e-04, qsa_score_mm worst rel 2.66e-06 (T 96, within budget and with 2051 selected
cells a token); before the change the test faulted on its first dispatch and reported nothing.
The compile failure that started this - qsa_flash's cooperative_tensor copy, "cannot
initialize a parameter of type 'void *' with an rvalue of type 'const void *'" - is already
fixed in main (#2323); this change is the runtime half of the same two kernels' M1 Max
bring-up.
What it leaves alone: the COOP_K4 path (a function constant: compiled out of the M1/M4
pipelines), qsa.metal, and every host file; no CUDA, HIP or SYCL backend builds these kernels.
docs/MAC.md records the M1 Max with the measured tok/s (the hardware's medians, from the
benchmark report: chat decode 10.6, 4K prompt 223.7, 16K prompt 228.6 tok/s).
Co-authored-by: Demetree Kallergis <[email protected]>
stream.cpp: the standalone-mmap fallback (the prompt's buffers as their own allocation) sat inside the chunk loop behind the slot-class check, so it only ran when a chunk was big enough for the prompt's 1.60 GB of buffers. When the arena shrank below that at the start (the 24 GB M2: chunks of 0.8-1.0 GB), every chunk was skipped and the prompt stopped with 'no arena region for the prompt buffers'. Both standalone sites - a fitting chunk whose slots a class cannot spare, and no fitting chunk at all - now share standalone_region(); the fitting-chunk carve and the slot-class rule are unchanged.
This is the half of #2323 that 0.1.44.1 could not see: the hotfix's reporter ran a 96 GB M2 Max, where the arena's chunks are several GB and the prompt region always fits. On a 16-24 GB Mac running --mmap-experts, 0.1.44.1 still stops at the first prompt with 'metal prompt: no arena region for the prompt buffers'; this makes the standalone allocation run there too.
Verified on a 24 GB M2 (macOS 26.6.2, Qwen3.8-Flash-Next GSQ-RCO-Q2_0 streaming): the fallback line appears where the error used to be, prompts run end-to-end (3.7-4.1 tok/s steady in web chat, no crashes). That run predates the standalone_region() refactor, which moves the same statements into one function both call sites use.
docs/MAC.md: the small-arena note says the prompt's buffers also get their own allocation when no chunk of the arena is big enough for them.
strata_bench starts the server on the config's `host` but polled readiness at
a hardcoded 127.0.0.1, so a LAN bind (192.168.1.10 in the report) printed
"still loading" while the server was answering on that address.
Use the configured host, mapping the wildcard binds to loopback and bracketing
IPv6 literals; an explicit --url still wins. Patch from the report, with the
mapping lifted into server_base so a test can reach it.
/metrics/prometheus exposes llamacpp:prompt_tokens_seconds and
llamacpp:predicted_tokens_seconds as averages of the cumulative totals, so an
idle server keeps advertising the speed of its last request.
llama.cpp reads those two names from a bucket it resets on every scrape ("the
gauges are averaged over the window between two scrapes", server_metrics::bucket
in tools/server/server-common.h) and reports 0 when the bucket is empty, so a
llama.cpp dashboard on Strata froze at the last value where llama.cpp decays to
0. Keep a bucket per scrape and expose its delta.
An earlier commit capped the accepted drafts at the end of an answer; that is an exact state, but it moved the hybrid mode's expert-slot
counts, so a sequence of requests with --mtp on a card that does not hold every expert differed from the base engine from the fourth
request on (found by comparing the 10 parity prompts against the base goldens on the 12 GB card). Now the speculative step commits what
the base engine committed; a stop seen before the state moves keeps only x (the server's hand-over of a stopped request to a batch slot then
reuses the held state: its text equals the run alone), and the held tokens list x and every accepted draft, so a conversation that continues
from them matches the state exactly. Setup says what parallel measured on the 12 GB card; docs/QWEN36.md updated.
spec_step takes no draft past an end-of-answer token or past the room max_new leaves, and a stop asked for before the state moves keeps
only x: every token emitted but the last is in the state, so a conversation that continues from the held tokens (the server's solo ->
slot -> solo hand-over, a chat's next turn) neither misses nor reads a token twice. Before, an answer ending inside accepted drafts left
the state ahead of the held tokens (found by tools/qwen35/q36_serve_handoff.py: the first request's text now equals its run alone).
STRATA_Q36_READ_WINDOWS sets the decode windows of the other slots between two chunks of a prompt read (default 1). The mtmd dump takes
several pictures; the engine logs each request's reuse; a 1,024-token test picture.
upload_f32 runs on the host and widens F16 with
__half2float(__ushort_as_half(...)), but __ushort_as_half is a __device__
intrinsic: nvcc 12.1 and 12.4 refuse the call ("calling a __device__ function
from a __host__ function is not allowed") and q36_engine.cu does not compile.
Widen the bits here instead, the way the neighbouring bf16_to_f32 already
does for BF16.
0.1.44 made IQ3_S the recommended Qwen size on a PC with 64 GB or more (IQ3_S: 9 to 15% slower decode than IQ3_XXS, more RAM).
SETUP.bat / --setup on a folder that already holds a model of the family now pre-selects the installed size (and --yes takes it);
the menu marks it "(installed)" next to "(recommended)", every size stays a choice, a first install takes the recommendation.
SETUP_ENV gained STRATA_MMQ_KQ, STRATA_HC_Q8 and STRATA_ROUTE_TAIL_SKIP, which setup writes for Unsloth's files only; a setup
run again dropped a user's own value of them in any other config (a hand-set STRATA_ROUTE_TAIL_SKIP=0). They are setup's only
in an Unsloth config.
/v1/responses (Codex) read only the Chat settings shared with apps, so the config's "reasoning_effort" (#1641) never applied
there (it always thought at xhigh); Service.effort_default() is now the one place that says which level a request that names
none gets, for the chat, Messages and Responses routes. openai_to_messages dropped "reasoning_effort": false (an `or` between
the two fields); effort_kwargs already treats false as thinking off.
The new test renders the prompt of every way a client sets the level (OpenAI reasoning_effort / reasoning.effort /
chat_template_kwargs, Messages API output_config.effort / thinking budgets at the 2048 and 8192 limits / disabled, Responses),
the config key, the shared Chat settings and their precedence, the web app's request shape, tools and a conversation.
Setup: the images question for qwen36 (GPU encoder on an NVIDIA card of 16 GB or more, the CPU encoder otherwise, AMD under Linux on the
CPU; --vision gpu kept as asked with a note), the pinned mmproj (size and SHA-256) beside the GGUF, the vision section and --vision in
the config; --parallel N is written as asked with what a slot costs, and the recommendation is said where every expert still fits.
docs/QWEN36.md: pictures and several requests at once with the measurements.
A slot's window carries its next token and, for the greedy slots that fit beside one row per slot, the proposal of its MTP block;
the proposals for the next window come right after the window from its own hidden states (the first from the state the prompt
left). Grouped GDN rows (a slot's rows in sequence on its own state), verify windows and their commit run per slot; every slot's
tokens equal its solo run. The engine also says what the sessions take and stops with the numbers when they cannot fit.
One conversation state per session; --batch N keeps N of them and decodes one token of each in a single window (rows of
several sessions in one forward, bit-equal to the solo rows), reads a prompt between the other slots' windows and takes a
conversation over from the session that holds its start. GENI / BGENI pictures: the encoder's rows replace the embeddings
of the <|image_pad|> cells, positions as llama.cpp's mtmd (interleaved M-RoPE 11/11/10, text after a picture continues at
p + max(nx, ny)). tools/qwen35/q36_mtmd_dump.cpp is llama.cpp's side of the picture check.
0.1.43's gate already had 'not on a layer split' as its first predicate; it printed the line only when the variable was set. A default 0.1.43 split ran without the overlap, so 0.1.43.1's opt-in default changes nothing on a split. Measured on 2x A4000 (IQ3_XXS, layer split, 5 interleaved pairs): v0.1.43, v0.1.43.1 and v0.1.43.1 + STRATA_ADAPT_OVERLAP=1 decode within 0.6% of each other.
On Windows cudaHostRegister of the whole arena fails (the WDDM shared budget is about half the RAM) and the
sliced fallback registers the first ~30 GiB: with Qwen3.8-Flash-Next IQ3_S layers 0-31, so layers 32-47 never
get a PCIe share, while part of the registered prefix holds GPU-resident experts that DMA never reads.
STRATA_PIN_BY_NEED=1 (the arena on one GPU):
- every blob gets a page-aligned stride (PadMap in pinned.hpp, ArenaExpertSource::arena_off, +0.15 % RAM), so any
set of blobs can be registered exactly; the loaders place each blob at its padded offset
- the expert profile's pairs past the estimated GPU-resident count are merged into ranges by rank and registered
before any page is touched, up to STRATA_ARENA_PIN_GIB (29 GiB by default on Windows) or the first refusal
- pinned() / device_alias() / pcie_layer() work on the ranges (a blob is pinned only if wholly inside one);
lock_resident_ranges() keeps the holes between them resident
- one start-up line: "pin by need: N ranges, X GiB pinned, Y % of the non-resident experts"
STRATA_PCIE_MIN1=1: the PCIe share (nmiss * pcie_num) >> 8 rounds to 0 at 1-2 misses per layer; with two or more
misses one goes over PCIe (when its blob is pinned).
RTX 3090 + Ryzen 5 5600X: 28.96 GiB registered = 92 % of the non-resident experts, all 48 layers with a PCIe
share; prompt read +5-7 % at 56K-117K tokens, decode level alone and +2-3 % with --adapt-async 1 and
--pcie-frac 0.6 (bench/results/2026-10-11-community-rtx3090-5600x-arena-pin-by-need, docs/DETAILS.md).
Both opt-in; without them the layout, the loaders and the sliced pin are those of v0.1.42.
In the arena mode the blocking adaptive tier creates and joins a thread every verify window and the next window
waits for its copies (#1863: the time outside the decode timing breakdown). --adapt-async 1 now also runs with the
arena on one GPU, not only in the resident RAM mode. The arena holds every expert, so a round is a copy-in only:
no copy back, no RAM move, no exchange buffers.
- gates for the arena; ArenaExpertSource::blob_at() reads a blob from the job thread without the read counter
- the evicted slot is marked non-resident and uploaded before the round's copies are posted; an expert becomes
resident only once its copy's event has completed
- STRATA_ADAPT_ASYNC_WAIT=1 (arena default): the next tick waits for the round, so the window an expert turns
GPU-resident does not depend on timing; STRATA_ADAPT_PICK_INLINE=1 (arena default): one round per window;
STRATA_ADAPT_ASYNC_BATCH=N caps the copies per tick (default 0 = all); arena rounds are not capped at 96
- adapt_quiesce(): every K/V grow, kv_quiesce and the VRAM command land the round in flight (they only called
apply_pending(true)); a failed round still drains its copies. This also covers the resident mode's async tier.
- STRATA_DECODE_TIMING: "adapt wait X join Y, Z swaps/window"; --adapt-async help text
RTX 3090 + Ryzen 5 5600X, cold prompts: x1.07-1.11 decode, faster on 12 of 12 prompts; 25.9 -> 21.6 ms per window
(bench/results/2026-10-11-community-rtx3090-5600x-arena-async, docs/DETAILS.md).
Opt-in (--adapt-async 1, default 0); the default path is that of v0.1.42. With the tier on, answers are not
bit-exact run to run (which window first computes a swapped-in expert on the GPU).
The GPU's own wiring of the arena ends seconds after its last command buffer, and a Metal residency set did not hold it either
(wired memory 75 -> 3 GB between requests, measured with vm_stat and a probe). Idle, the arena is ordinary anonymous memory, and a
20 GB file written beside it made macOS compress tens of GB of it: the third panic (wired 2.4 GB, file cache 50 GB, compressor 1.9M pages).
mlock keeps it wired (STRATA_METAL_WIRE_ARENA=0/1, default on from 64 GB; not when the fill stopped early). Same write, arena wired,
full speed: compressor +0.7M pages (the engine's non-arena memory), 408 pages swapped, nothing else.
docs/MAC.md: the footprint cap, the wiring and the new variables.
Both panics (watchdogd starved 92 s) had the streaming engine at 95.3 / 89.9 GiB resident, free memory 13 MB, the compressor at 2-4M pages.
macOS keeps a file-cache floor (vm.vm_page_filecache_min, 37.2 GB on the 128 GB M5 Max) and compresses anonymous memory instead of dropping
file cache below it; the arena is anonymous and the GPU wires it only while a command buffer runs, so during the fill and when idle it is
pageable. Measured with a 1 s memory watchdog: old build + 100 GB arena, or auto arena + 40 GB file write + a 16 GB program: compressor 21k -> 4.07M
pages in 10 s, 29,000 pages swapped, pressure warn. The 67% rule counted only cx.allocated() (7.5 GB) while the process held 11-15 GB more, and
67% was too much anyway.
- arena_budget.h: footprint_cap (anonymous memory: arena + Metal buffers + what the process holds, task_info phys_footprint) = 60% of the RAM on
a Mac of 64 GB or more (and at least the file-cache floor + 12 GB left); resident_cap (a resident pack's mapped file pages, measured: UD-Q4 resident,
wired 91.7 GB, 40 GB write + 16 GB program, nothing compressed) = 70%; STRATA_METAL_LEAVE_SHARE for measuring. The explicit
STRATA_METAL_ARENA_GB is kept and flagged when it is over the cap.
- window.cpp logs the planned total (arena + held + n-gram reserve against the cap) and the footprint after the fill.
- the engine refuses to start while a repack or setup's model preparation holds a lock beside the model (STRATA_METAL_IGNORE_REPACK=1 overrides).
- metal_repack.py: unbuffered write(), F_NOCACHE on the shards and the output, fsync every GiB, a pid lock (file cache stays flat; the old tool filled all 128 GB).
- setup_mac.py: no engine while preparing a model (stops setup), start() waits on a live repack lock, resident rule = the engine's resident_cap.
- tests: arena_budget_test.cpp (47 checks), tools/test_setup_mac.py (repack guard).
- CMake project version 0.1.44; setup MIN_ENGINE (0, 1, 44); NVFP4_ENGINE (0, 1, 44) (UD-Q5/Q6 were already 0.1.44).
- setup: a prebuilt zip without strata_kq leaves no stale library next to the new engine (kept in .previous); the
Dockerfile copies libstrata_kq.so into engine/.
- Model menu: families in the order Qwen (GSQ-RCO), Swift, Coder, Unsloth, NVFP4, Qwen3.6; NVFP4 is not listed on AMD
and Intel (the Unsloth line names UD-IQ4_XS there); on NVIDIA its line says native FP4 (RTX 50) or that UD-IQ4_XS is
faster on this card, with the download and RAM need. The Unsloth family lists its five sizes, images on.
- strata_mcp's built-in tables know the NVFP4 sizes and family. tools/test_setup_menu.py.
Variant 6 of gdn_recurrence_variant (sycl/include/strata/prefill/gdn_xe.hpp): the column kernel's four row groups
and FMA chains in the same order, a 16-lane sub-group holding four columns x four row groups, so the cross-row-group
sums are sub-group shuffles and a token needs one work-group barrier instead of five (256 GRFs a thread).
Arc A770 16 GB (xe, oneAPI 2026.1 JIT), same day, a0116b27 + #2190 + #2193 with and without this commit:
gdn_parity recurrence 51.0 -> 24.5 ms at 8,192 tokens, 204-236 -> 110-113 ms at 32,768 (48 heads), 0 mismatches
(state, FP32, FP16). Coder IQ1_M, 128K context, int8 KV, --resident-experts: prompt 386 -> 396 / 407 tok/s at 8K,
478 / 480 -> 511 / 518 tok/s at 32K (2 runs each), the same output ids in every run.
On by default on Alchemist only (strata::intel_gpu_gen); every other card keeps the column kernels, unchanged.
STRATA_GDN_XE=0 turns it off, =1 turns it on for any Intel GPU with 16-lane sub-groups. gdn_parity compares
variant 6 with variant 0 bit for bit (state, FP32 and FP16 outputs) on any Intel GPU and skips elsewhere;
ctest -R prefill_gdn_parity runs only that part.
On the A770 the multi-token GR read's kernels launch too few work-groups to fill the card. With
STRATA_GR_MORE_GROUPS on:
- sliced down kernel: one work-group per (slice, row block), 80 x 6 = 480 groups instead of 80;
every row's partial sum is computed exactly as before, only by a different group.
- gr_down_reduce: 8 slice loads in flight per step, then added one at a time, so x accumulates
slices 0, 1, ... 79 in the same sequence of float adds as the old loop (-fp-model=precise).
- up kernel: 8 columns per group, 320 groups instead of 160 (the kernel is now templated on the
column count; UPM_COLS stays 16 for every other user), and only group 0 writes the per-token lo
values, which every group computes identically.
Default on Alchemist only (strata::intel_gpu_gen); Battlemage and other devices launch the same
kernels as before. STRATA_GR_MORE_GROUPS=1 forces it on, =0 off; gr_bench runs either way.
Measured on an Arc A770 16 GB (xe, oneAPI 2026.1 JIT), same day, a0116b27 + #2190 + the previous
commit (GR_PICK, large GRF), with and without this one: gr_bench 1-6 tokens 118.9-183.5 -> 94.6-148.4 us,
checksums identical for every token count; gr_multi_parity 32 cases OK with STRATA_GR_MORE_GROUPS=0 and =1.
Decode, Coder IQ1_M, 128K context, int8 KV, --resident-experts, STRATA_VERIFY_STEPPED=1, 8K prompt, 256
tokens: 22.12 / 22.35 / 22.55 / 22.65 -> 23.24 / 23.61 / 23.62 tok/s (+3-5%), output ids identical.
Not built or run on Battlemage.
- GR_PICK: a runtime index into the by-value kernel argument (m.a[k]) made IGC copy the whole
GrMulti (~1.2 KB) into every work-item's private scratch (unitrace on an A770: ~40 KB private
per SIMD32 thread in gr_up_multi / gr_down_reduce / gr_norm_split). GR_PICK picks one field with
compile-time indices only (a chain of selects over the uniform kernel argument). Addressing
only: the same values, read and summed in the same order, so the output is bit-identical. This
part is on for every device; it is not measured on Battlemage.
- The sliced down kernel and the multi-token up kernel in large-GRF mode (256 registers/thread);
at 128 the up kernel still spills ~3.4 KB/thread on the A770 (unitrace). Large GRF halves the
threads resident per Xe core, so it is the default on Alchemist only (strata::intel_gpu_gen);
Battlemage and other devices launch the same kernels as before. STRATA_GR_LARGE_GRF=1 forces it
on, =0 off.
Measured on an Arc A770 16 GB (xe, Linux 7.0, oneAPI 2026.1 JIT), a0116b27 + #2190, same day:
gr_bench 1-6 tokens 205-258 us -> 119-184 us (GR_PICK alone, STRATA_GR_LARGE_GRF=0: 186-239 us),
checksums identical for every token count. Coder IQ1_M, 128K context, int8 KV, --resident-experts,
STRATA_VERIFY_STEPPED=1, 8K prompt, 256 tokens: decode 5.37 tok/s (5.88 / 5.93 with
STRATA_DOORBELL_CHECK=1 / STRATA_LEND_MIRROR=0) -> 22.12 / 22.55 / 15.60 (cold-cache run)
tok/s, 20.61 / 20.93 with STRATA_GR_LARGE_GRF=0; the window's pool phase 183-208 -> 34-35 ms
per round; output ids identical where the expert placement matched. Not built or run on
Battlemage or i915.
On an Arc A770 16 GB on the xe driver, Level Zero has no ext_intel_free_memory and get_memory_info reports 0 free
with the right total. device_free_bytes() kept the 0 (the #1440 fdinfo path only replaces a figure that is too
high), so --expert-cache auto sized the caches from 0 bytes. A 0 with a known total is now read as unknown: on
Linux the card's total less this process's DRM fdinfo figure is used, the same source the A750/i915 path uses.
Anything that reports a non-zero free figure is unchanged; off Linux the 0 stands.
When the main layers' experts fit but the MTP block's do not (UD-Q6_K_XL on
32 GB), the main layers are placed as in the all-in-VRAM mode (same text,
the decode graph kept) and only the MTP block's experts go to slots + the
CPU pool (its drafts). To get there when the fit is close: the packed Q8_0
decode copies are given back, the prompt chunk stays 512, and - said in the
log - the attention scores buffer shrinks to 64 MB and the VRAM reserve to
256 MiB. STRATA_Q36_NO_MTP_CPU=1 keeps the plain hybrid mode.
UD-Q6_K_XL on gfx1201: tg128 73-75 -> 80.6 (llama.cpp HIP 81.2).
Many small copies were slower than the one whole-layer copy on a 12 GB card
(RTX 3060, UD-Q4_K_XL at 37 of 256 resident: pp2048 1540 in runs, 1974
whole). gfx1201 UD-Q6_K_XL (205/256) and Q8_0 (176/256) keep the per-expert
copies: pp4096 3504-3588 and 3015-3079.
The server already keys its encoder cache by the normalized picture bytes (sha256, the .sve file
name); it now sends those keys with the request (img_keys=<hex>,..., in prompt order), and the
engine's conversation cache uses key + grid for a picture when the count matches. Without the key
(an older server) it hashes the embeddings as before. Old engines skip the unknown key.
Measured for #2228 (one 1600x1000 screenshot-like picture, strata-vision, fresh processes and
twice in one process): the CPU encoder gives the same bits on a TR PRO 3975WX at 8/15/16/32 threads
and on a Xeon E5-2643 v3 (e2357d8e...), the GPU encoder on an RTX A4000 is reproducible too
(92cf7818...), but CPU and GPU differ. So a picture's embeddings change when the encoder moves
between CPU and GPU, or after an encoder update, and no longer on its own; with the byte key the
cache's matching does not depend on that at all. CUDA/HIP and SYCL. Test: serve/test_server.py
test_pictures_are_keyed_by_their_bytes_for_the_engine.
The prompt's staging buffer took the whole layer from host memory over PCIe
(~19-28 GB per chunk); now the experts with a VRAM slot are copied device to
device and only the others come over PCIe, in runs of consecutive experts.
Same bytes in the staging buffer (greedy ids identical). UD-Q6_K_XL on
gfx1201 (205 of 256 experts per layer resident): pp4096 1935 -> 3511-3601.
Also: the packed Q8_0 decode copies are given back when that lets every
expert fit (STRATA_Q36_KEEP_PACKED=1 keeps them); STRATA_Q36_STREAM_ALL=1
keeps the whole-layer copy.
A checkpoint (or the live state) is reused only when the prompt starts with its tokens AND its
pictures, and a picture is its grid plus the hash of its embedding floats. In #2228 (screenshot-heavy
agent, CPU image encoder) the cache fell back to checkpoints a long way back although the client's
messages were byte-identical; one explanation is a picture encoded again with other bits. When the
deepest state whose tokens match is rejected for a picture, the engine now logs which picture and
where, once per request, so the next log tells this apart from a rewritten history. Log only; what
is reused does not change.
47Hunter47 measured </tool_response> asked verbatim at 0/15 on 0.1.43.1 (it reached the model as
the real closer). 62185aab already marks it; this pins the cases next to the exception that keeps
a wholly wrapped tool result's outer pair: the bare closer, a request that quotes a whole pair, a
message that only starts with the opener, two closers in one sentence.
Qwen3.6-35B-A3B-Q8_0.gguf stores the GDN alpha / beta projections as Q8_0;
the engine read only F32 / BF16 there and refused the file. They are now
widened to F32 at load (fp16 scale x int8, as ggml). q36_llama_dump:
STRATA_DUMP_CTX sets a smaller context / batch for files whose weights leave
no VRAM for 4096-token buffers.
The engine keeps every expert in RAM when they do not all fit in VRAM, so a
size's RAM need is its file plus room for the rest. The largest size this
PC's RAM holds is the default; any size can still be chosen (a bigger one is
asked about, as before, never refused).
- k_attn_mma (sm_80+): m16n8k16 f16 -> f32, 8 warps x 16 queries, 64-key K/V tiles double-buffered with
cp.async, ldmatrix (.trans for V), fp32 online softmax. Bench on the RTX 3060 at 4096 x 4150 keys x 32
heads: 23.7 TFLOPS vs ggml's flash_attn_ext_f16 15.4 (0.376 vs 0.577 s/step); CPU check 7e-5.
STRATA_QI_GGML_ATTN=1 keeps ggml's.
- q, k and v of a block with one weight type load into one [3C x C] tensor: one GEMM and one activation
quantize instead of three (STRATA_QI_NO_QKV_FUSE=1 keeps three).
- A float4 LayerNorm path with tanh(gate) once per step, off by default (STRATA_QI_FAST_LN=1): no measured gain.
RTX 3060, Q8_0 1024, seed 42, 3 interleaved rounds: steady step 2.09 s vs stock 2.61 (1.25x), denoise
17.93 s vs 22.13 (1.23x).
A start right after another engine exited sometimes fails before the engine writes a line: ROCm aborts with
"terminate called after throwing an instance of 'rocr::AMD::hsa_exception'" (or the process exits with an empty
log) while the previous process's GPU queues are still being released (r9700: 1 of ~10 rapid restarts in the A/Bs,
2 of ~40 for the quants runs). Such a start - within 30 s, no engine line other than the runtime's failure, not the
engine's own error exit 1 - is retried with a message saying why; STRATA_START_RETRIES (default 3, 0 = off).
0.1.41 (0391e478) scaled each stage's miss term by 20 GB/s / its probed link, for 2x 4090 D
with CUDA1 on a physical x1 link (1.6 GB/s: x12, K 22 -> 38, prompt ~350 -> ~1400 tok/s).
On 2x Tesla T10 the same rule moved auto from K=23 to K=14: CUDA1 runs 34 of 48 layers, and as
the prompt path is a pipeline the slower stage sets the rate - 256K prompt 1035 -> 795 tok/s,
decode 46.8 -> 39.5; --layer-split 23 on 0.1.43 restores both (xiake-1 in #2007).
split_rules::link_miss_scale: the scale is the fastest stage's link over this stage's, applied
only from a 4x gap (a riser or chipset x1/x4 slot beside an x16 one), capped at 64. A symmetric
rig keeps the fitted formula whatever its link speed; the x1 case still gets ~x15.
STRATA_SPLIT_LINK_ABS=1 restores the 0.1.41 rule. SYCL has no link term (unchanged).
Test: split_rules_test (the x1 case, equal gen3 cards, a 2x gap, caps, unprobed stages).
Not run on the reporter's T10s: the reply asks for the 'link ... -> miss cost' lines.
The native output head is needed only to generate, the batched prompt path's buffers only to read a prompt. With the flag the head is unloaded while a prompt is read (its VRAM counted into the auto cache and the prompt path's hybrid borrowed/owned layout) and read back in before the first decode window. On an 8 GB RTX PRO 2000 Laptop (Windows, IQ3_XXS, 590 MiB reserve) the prompt chunk went from 512 to 2,048 tokens and prefill from 292.5 to 667.1 tok/s on a 29,854-token prompt; reads of up to 512 fresh tokens cost about 0.6-0.75 s more. Off by default: every new path is behind the flag. docs/PREFILL_HEAD_ALIAS.md has the measurements and limits.
The table is read with F_NOCACHE through a bounded row cache (1M rows), so 2 GB was too much to set aside. M4 32 GB,
IQ3_XXS decode after the 6K document, 5 interleaved runs, auto arena (memory_pressure every 3 s, vm_stat swapouts):
keep 7 + 2 GB (54c0de70) 5.93 tok/s median, arena ~8.5 GB; keep 7 + 0.5 6.59 (+11%), ~10 GB; keep 5 + 0.5 7.47
(+26%), ~12 GB; the least free memory 43-57%, no swap-out in any run. The M5 Max still keeps 15% (20.6 GB).
arena_budget_test: 14 checks pass.
ROCm gives out hardware queues per priority. Sharing one with the window's stream, --pcie-mode dma's expert copies
and the host function that raises flag B queue behind the window graph that spins on that flag: on an R9700 every
dma-mode request timed out at layer 1. With its own queue dma mode runs (copies overlap the VRAM-hit compute).
The figure in "[strata] images: the image encoder started in N s, M MiB of VRAM" read the free VRAM right after the
cache's shrink, before the driver's counter showed it (amdgpu sysfs), so it came out low (286-362 MiB against the 1,339
measured otherwise). A 0.2 s wait before that reading, only on a start that shrank the cache.
Opt-in, with the config's "vram_elastic": the GPU encoder does not start with the server or a load. A picture's turn
makes the expert cache keep "elastic_mib" (default 2048) free on top of the reserve and starts the encoder; after
"elastic_idle_s" (default 120) without a picture a thread stops it between requests and the cache grows back to the
reserve (0 when no POST /v1/vram was made: the start's reserve left it a segment or more smaller). A POST /v1/vram
while the encoder runs stacks on its share; an unload stops it. Without "vram_elastic" the server says so and starts
the encoder as before. serve/test_vision_elastic.py pins each case; docs in VRAM_ELASTIC.md and DETAILS.md.
update_install refreshed the draft subset once per installed config, newest first, so a folder that two configs
name ended with the subset of the config used longest ago. A config without "draft_vocab" (cjk) then put the
default subset over the cyrillic one of the config in use. Now the first config that names a folder decides;
separate folders and a start (which installs its own config's subset) are as before.
Checked: tools/test_setup_draft_vocab.py (3 new tests, 10 pass), test_setup_config and test_setup_configs (18 pass).
Measured on the affected install, two alternating runs each, greedy 600-token Russian story: 54.0 / 54.0 tok/s
with the default subset installed by the update, 68.7 / 68.6 with the cyrillic one (1.39 -> 1.90 tokens per window).
A helper for the large-page arena (#1412): run as Administrator, it adds the account to
SeLockMemoryPrivilege with secedit, says that a new logon is needed, and prints the secpol.msc
steps when it cannot run. Changed from the PR: secedit /configure is limited to
/areas USER_RIGHTS (the exported policy is not re-applied as a whole), and the description no
longer claims a speed-up we have not measured. docs/DETAILS.md points to it. Not run on our side
(no Windows box for it).
Co-authored-by: dvasdekis <[email protected]>
Some amdhip64_7.dll builds (AMD TheRock wheels / ROCm 10.2 / 7.2.4 nightly) do not export
hipKernelNameRefByPtr although their import library has it, so strata.exe failed to load with
STATUS_ENTRYPOINT_NOT_FOUND before main. It is only used to name graph nodes under
STRATA_VERIFY_NODES; on Windows it is now looked up with GetProcAddress and a missing one
leaves the node unnamed.
From #2183, its verify.cpp part only. The PR's other part (Windows HIP --pcie-mode auto as
DMA) changes the default path of every Windows AMD user and was not measured on our side; it
is left out.
Co-authored-by: dvasdekis <[email protected]>
An Arc A770 under xe intermittently never started decoding: the main thread waits in Verifier::run's stepped serve
(queue::wait -> urQueueFinish) for a segment that never drains, xe logs nothing (#2194). The A750's same failure was
fixed by draining the other queues before each window (STRATA_WINDOW_SYNC, default 1 on i915 only). Reproduced on
the A750 by turning that off: STRATA_WINDOW_SYNC=0, STRATA_VERIFY_STEPPED=1, --resident-experts, IQ3_XXS, 4K prompt:
1 of 6 runs hung with #2194's stack; with the default drain 0 of 11 (5 unpinned, 6 with the pipelined helper pinned).
The default is now 1 on i915 or on an Alchemist device (intel_gpu_gen), 0 elsewhere; STRATA_WINDOW_SYNC overrides.
Unchanged on the A750 (i915: already 1) and on Battlemage (0). Not run on an A770 under xe (no such card here).
The first version compared only the extra loan with the RAM available at that point, but the
resident RAM copy is pinned after it and holds the lent slots' experts too: in a 30 GB scope on
an R9700 (IQ3_XXS, --resident-experts) 30 GiB were available before the 23.35 GiB copy, so the
rule never fired. The reporter's PC thrashed with 3.4 GB left at 16384 and ran with ~6.4 GB at
8192, hence 6 GiB.
Both NVFP4 packs keep Qwen's vision tower unquantized, so the image encoder every Flash-Next model uses reads pictures
for them; strata-vision opens the text model vocab-only, which is the pack's dense GGUF here. Setup asks about images as
it does for every model (default no; --vision gpu|cpu), the encoder is the file other installs already share
(mmproj-Qwen3.8-Flash-Next-BF16.gguf at ISTA's pinned revision, 907,543,008 B, sha256 b1a82259...).
Checked end to end through the server on an RTX 5070 (budget mode, GPU encoder, 1024 image tokens): a picture with
"CODE 4217", a red square on the left and a blue circle on the right - NVFP4 answered "4217" and described both shapes,
their colours and sides correctly, as IQ3_XXS did on the same picture. VRAM: the expert cache held 393 slots with
images (--vram-reserve-mib 2100 and the encoder) against 982 without (1500).
The gfx12 default (0.1.43) lets a bare --prefill auto reach 16384, which lends the prompt path
about twice the cache slots of 8192 (R9700, Swift 1.5 IQ3_XXS: 3,907 slots / 6.38 GiB against
2,068 / 3.37 GiB); the lent slots' experts are read from the RAM side during the read. On a
30 GB PC with --resident-experts 38-47K prompts went 20 GB into swap with 1.5M major faults
and the engine was killed; --prefill 8192 ran them at ~95 tok/s (bdwilton in #2090).
At start, when the engine's own 16384 ceiling is in effect, the extra slots of a 16384 loan
over an 8192 one are priced at the largest blob; if the RAM available (the cgroup or
--memory-limit-mib cap included) is below that plus 4 GiB, the ceiling stays at 8192 and a
line says why. STRATA_GFX12_CHUNK=1 keeps 16384; an explicit --prefill N / auto:N is never
touched (the 16384 default only applies to a bare auto).
The 'MiB of VRAM free with everything loaded' line and its LOW warning read CUDA0 alone. In
#2192 (2x RTX 2080 Ti 22 GB, Windows, IQ3_S, --batch 4, --layer-split auto, 0.1.43) CUDA0 had
740 MiB free while CUDA1 began its window captures at 153 MiB and fell to 52; the first batch
window then never finished. Both stall dumps show the host thread inside cudaGraphLaunch called
from Verifier::run_slot_rows (the stage still 'idle': it had not reached its per-layer wait), the
pool asleep and the watchdog firing after 60 s - a page-in under WDDM while the other card's
graph spins on a host flag.
Each later card now gets its own line, and below 512 MiB a LOW warning with the
--vram-reserve-later-mib value that leaves ~768 MiB. Said, never changed.
On Windows with only an AMD card neither NVML nor the amdgpu sysfs files answer, so
hardware_static.gpu_name was None and strata_bench (no nvidia-smi, no rocm-smi on Windows)
labelled the run "CPU only" although the engine ran on the card (ByteJuggler in #1587: RX 6900 XT).
telemetry.windows_display_adapters reads DriverDesc, ProviderName, DriverVersion and the memory
size from HKLM's display class (no subprocess, no vendor library); windows_amd_cards drops a
Ryzen's integrated Radeon (512 MiB here) when a real card sits beside it. hardware_static gets
the name and gpu_vram_total_mib; strata_bench uses the same list when nvidia-smi and rocm-smi
give nothing. Load, VRAM use, temperature and power stay unread on Windows AMD (that is the
reader of #1224 / #1434 / #1871). Checked against this PC's registry (RTX 5070 + the Ryzen 7600's
iGPU: both listed, the iGPU's 512 MiB read); tools/test_windows_amd_gpu.py with a fake registry.
The stock split shape (STRATA_STAGE_TRIM=1, 1024 MiB reserve, NO_HOST=1 from strata-sycl.sh) is
refused at the start on a 2x Arc Pro B60 24 GB pair with IQ3_XXS: the second stage is 2230-3731
experts short and the host mirror covers the first stage only. The same config with NO_HOST=0
runs at 39-50 tok/s with 0 faults (jds1-ai in #1980). When the model's experts take more than
80% of the split cards' VRAM, setup now writes NO_HOST=0 into the config (a value the user set
is kept) and prints why and how to turn it back. IQ2_XS on the same pair (74%) keeps NO_HOST=1.
Test: tools/test_setup_sycl.py test_split_short_rule.
- refuse_exit: after the REFUSED message the engine waits for the queued device work, flushes
and _Exit(2)s instead of returning from main. Returning ran the stages' destructors in an
order the UR loader did not survive: on a 2x Arc Pro B60 pair every REFUSED start segfaulted
in libur_loader, and a trimmed one also left an xe page fault (bcs CAT error) on the second
card that hung the next start (jds1-ai in #1980).
- resident_plan_set_mirror takes the table's length; resident_plan uses the mirror and the
usage counts only for a residency slice inside the table they were set for. A later stage
passes its own residency table, so the slice was taken as an offset across allocations and
read (and counted into) another buffer: the stage-1 xe faults with STRATA_VERIFY_NO_HOST=0
and no trim (10 of 10 requests, 0 tokens). Such a miss is now a plain miss, served by the
host. Patch shape as jds1-ai proposed it.
Compile-checked only (no two-card Intel box here).
Under WDDM a stream's work sits in the driver's command buffer until a driver call flushes it.
With the stream-ordered signal the verify window only spins on a pinned int, so the last
layers' copies and their 4-byte signal copies were never submitted: the first request stopped
with "the adaptive tier's copies for layer 45 never landed" (RTX 4070 SUPER, 0.1.43.1, opt-in
STRATA_ADAPT_OVERLAP=1). STRATA_ADAPT_SIGNAL=host worked.
- Windows: the host function is the default signal; STRATA_ADAPT_SIGNAL=copy asks for the copy.
- Every platform: one cudaStreamQuery on the copy stream after the swaps and their signals are
queued (a no-op for the results; it submits the batch under WDDM). Linux keeps the copy
signal as before.
Needs a Windows run with STRATA_ADAPT_OVERLAP=1 STRATA_ADAPT_SIGNAL=copy to confirm the flush.
bench/results/2026-10-12-p40-sm61-kernels: where a prompt and a decode window spend their time on a P40, the kernel
benches and parity runs, every request of the engine A/B (565-token prompt 205 -> 255 tok/s, decode 34-35 -> 37-38
tok/s; with STRATA_DENSE_F32=1 a 7K prompt 360-432 -> 464-474 tok/s), the byte comparison of the answers, and what
was not tested. docs/OLDER_GPUS.md: the sm_61 section and switches, and two traps when measuring on such a card.
As sm_60 got with STRATA_SM60_TABLE, measured on a Tesla P40 (native dp4a):
- native_expert_bench, 18 groups at 2 tokens on the IQ3_S pack's layers, mode 0 -> mode 8, bitwise: IQ3_S 390 ->
255 us, IQ3_XXS 345 -> 252, IQ2_S 290 -> 238, IQ4_XS 328 -> 261.
- kIlRows_61 / kIl1Rows_61 from three runs of mmvq_il_parity --bench --emit-table (identical but two Q3_K cells;
a cell is non-zero only where every run gained 3%).
For FP16 operands with FP32 sums cuBLAS 12.4 has two Maxwell kernels on a Tesla P40 and picks the slow one for most
chunk sizes: qkv (10240 x 2560) runs at 7.6 TFLOPS for T 7030-7040 and at 4.3 from T 7041 (49 -> 85 ms), so a
7K-token prompt reads in 16.3 or 19.5 s by its token count. cublasSgemm on FP32 copies runs at 8.4-9.8 TFLOPS from
T 512. For the five shapes and the sizes the route takes, the result is bitwise the FP16 call's: gemm_f32_parity,
5,201 chunk sizes through Gemm::f16. That is a property of the cuBLAS build (12.4.5.8 measured), hence opt-in.
dequant_kernel's thread reads its 32-value group a byte at a time and writes 32 two-byte values one by one, 64 bytes
from its neighbour's. On a Tesla P40 that is 10 GB/s for Q6_K, and the IQ3_S pack's dense weights cost every prompt
chunk ~0.5 s of it, whatever the chunk's length (a fifth of a 560-token prompt). The wide kernel takes 8 values a
thread: 16-bit loads, one 16-byte store, the same expression per value.
dequant_wide_parity: 360 cases bitwise (FP16 / BF16 / FP32 out, a padded stride, row slices, sane and raw random
blocks); --bench on the P40: Q6_K 10240 x 2560 7.25 -> 0.33 ms, Q4_K 5.2x, Q5_K 3.8x, IQ4_XS 3.6x.
On by default on compute capability 6.1 only (where it was measured); STRATA_DEQUANT_WIDE=0 / =1 for any card.
The router, the GDN alpha / beta and the shared expert's gate (n_out 1-256,
K 2048) ran on hipBLAS/cuBLAS SGEMM with 8x8 / 16x16 tiles (~10% of a
prompt). k_sgemm_nt: (16 TM) x 64 tiles, TM x 4 outputs per thread, K split
so about 128 blocks run, the partials summed in a fixed order
(deterministic). gfx1201 pp4096: 3290 -> 3455 tok/s. STRATA_Q36_BLAS_F32=1
keeps the BLAS SGEMM.
Warm-up: a throw-away prompt chunk, a decode step and (with drafts) a
speculative step at load make the code objects, BLAS kernels and graphs, so
the first request runs at full speed (pp4096 rep 0: ~1770 -> 3273 tok/s; a
34-token first prompt: 1032 -> 48 ms). --no-warmup skips it.
Hybrid mode with MTP exact by default: the MTP block is loaded whenever the
file has one (so the VRAM layout and the slot count do not depend on --mtp;
the prompt pass through it only runs with drafts); decode and verify rows'
routing is counted only for the rows kept; the slots are rebalanced exactly
at every 32nd position and a verify window never steps over one.
arena_budget.h: the rule as plain arithmetic, tested by metal/tests/arena_budget_test.cpp (13 checks, run on the
M4). The M5 Max froze (2026-10-13 02:08) with an 89 GB arena planned from 'available - 7 GB' beside a 50 GB n-gram
table: macOS now keeps 15% of the RAM (20.6 GB there), the table's mapped bytes (--ple-io ram) or 2 GB (read row by
row) come out too, and just before the arena is committed the free memory is asked again - less than planned: the
arena is laid out again smaller instead of failing. STRATA_METAL_KEEP_FREE_GB / STRATA_METAL_ARENA_GB override.
stream.cpp: arena buffers grow past 8 GB when 4 of 8 GB cannot hold the arena (STRATA_METAL_ARENA_CHUNK_GB).
The coordinator suggested max(12 GB, 15%); on the 32 GB M4 (Q2_0 streamed, 200 tokens, memory_pressure every 3 s)
12 GB kept cut the arena 9.4 -> 4.4 GB and decode 7.8 -> 4.8 tok/s, while 7 GB kept left macOS 55% free the whole
run (12 GB: 70%); same tokens. Hence 7 GB below ~47 GB of RAM, 15% above. Not yet run on the M5 (down).
iq_host.cpp (Metal's copy of the host helpers): Q6_K among the grouped gate/up formats, as mac-kern's moe_gu_14 /
mm_gu3_14 run them. Before, the engine refused UD-Q5_K_XL ('layer 2's experts are Q6_K/Q8_0 ... no GPU kernels').
strata reads the --native file's architecture at the top of main() and hands
a qwen35moe model to strata::qwen35::run (src/qwen35, now the strata_qwen35
library; strata-q36 wraps the same entry point) before any Flash-Next state
exists; every other file runs exactly as before. strata --families lists the
architectures a build runs. The Qwen3.6 path takes strata's flag names where
they mean the same (--native, --max-new, --pool-workers, --no-token-graph,
--gpu, --mtp N) and skips Flash-Next-only ones in serve mode. The SYCL build
keeps refusing the family, now saying CUDA and HIP run it.
setup: Qwen3.6-35B-A3B is a family in the menu (OWN_PATH_FAMILIES,
install_own_path): size, context, the engine (checked with --families), the
pinned GGUF, its tokenizer, a config with --native/--max-context/--mtp 2 and
the start script; no pack, profile, draft layer or tuning. The engine step is
a function shared with the Flash-Next path. strata_inspect says the family
runs on CUDA and HIP.
- dense_bench: the decode window's native_mmvq calls at the model's shapes and formats, 24 matrices per shape in one
graph (weights from DRAM), 1-4 columns, us / GB/s and an output hash.
- STRATA_MMVQ_WIDE_K_RPW=2|4: Q4_K / Q5_K / IQ4_XS wide kernel with 2 or 4 rows per sub-group sharing each column's
activation loads (apply split into act + dot, the same operations); every hash equal. B70, 3 columns: Q4_K
6144x2560 56.6 -> 27.7 us, Q5_K 2560x12288 89.8 -> 75.5 us, Q6_K unaffected.
- STRATA_MMVQ_WIDE_RPW>1 and STRATA_MMVQ_PF now take the aligned Q6_K loads (they were 2-4x slower than the default
without them); still slower than the default except Q6_K 6144x2560 at RPW 2 (50.2 -> 45.3 us).
- fused GR read: the up step's columns per block (STRATA_GR_UP_COLS 4/8/16/32/64, default 16 = before) and its T
butterflies interleaved (STRATA_GR_UP_ILP=1); gr_bench checksums equal for all. 32 columns: 3 tokens 50.7 -> 48.9 us,
1 token 27.5 -> 29.4; ILP slower.
Engine, B70, IQ3_S with data/expert-profile-sycl.bin, 5 interleaved pairs: STRATA_MMVQ_WIDE_K_RPW=2 STRATA_GR_UP_COLS=32
vs default: decode after a 4K prompt 54.31 -> 54.00 tok/s, story 38.41 -> 38.50, outputs identical. Defaults unchanged.
Found by the quant yardstick (--expert-cache 1) on the NVFP4 GPTQ pack: 'pre-filled 0 of 0 slots', and the verify windows
refused to run. Every other native format's blob is rows x row bytes with 1280 / 2560 rows, a multiple of 256, so its
budget is unchanged.
ROCm gives out its hardware queues per priority level; the draft stream shared one with the verifier's, so the
draft round ran only after the commit graph the verifier had just launched. With a queue of its own the commit
runs beside the draft. R9700, UD-Q4_K_XL: draft wall 5.8 -> 3.5 ms a window (STRATA_DRAFT_TIMING).
The per-layer commit was ~98 graph nodes; it runs between a window and the draft round, and on HIP the round
waits for it (R9700 UD-Q4_K_XL: 2.35 -> 0.65 ms GPU). Each layer's arithmetic is the per-layer call's:
gdn_parity (gdn_commit_layers vs the per-layer commit, n_keep 1-8) and qsa_parity (append_steps_layers vs
copy + append_steps, 3 scalings) are bitwise. STRATA_COMMIT_TIMING=1 prints the commit graph's GPU time.
On the RTX 5070 PC (64 GB, Windows) --mmap-experts let the experts it read stay in the engine's working set: free RAM
fell to 0.6-4.9 GB and the memory guard stopped the engine three times (2026-10-13). --resident-budget-gib 22 with the
same pack kept >= 22.8 GB free. The budget mode is what setup already writes for Unsloth's files (resident_budget_gib:
40 GiB on 64 GB); the pack's experts.bin is the file the rest are read from. test_setup_nvfp4: 64 GB -> budget 40.
Verifier::window_logprobs reads the head rows through window_copy_logits under STRATA_GPU_METAL (the CUDA/HIP path
unchanged: verify.cpp's object is byte-IDENTICAL on P1004 and p620). The prompt's windows sample nothing (head
sampling off), so with STRATA_LOGPOS the Metal window computes the head anyway.
tools/quant_quality.py on the M5 Max against the Q8_0 reference (q8_0.tsv 627558d1, set.ids.json 414634dd), 4,995
positions: UD-Q4_K_XL 92.77% top-1, KL 0.0548 (p99 0.855), PPL 4.043 vs 4.052 (CUDA/HIP: 93.09% / 0.056); IQ3_S
87.85% / 0.1422 (CUDA/HIP: 87.7% / 0.144). The Metal window keeps int8 K/V (the yardstick asks fp16).
tools/image/sdcpp_native: a runner that replaces QwenImage21Runner::compute only (sd.cpp keeps the text
encoder, VAE, sampler and RNG), chosen with --model-args qwen_image_2_1_native=1 in CUDA / HIP builds.
- The text prefix runs once per condition (causal, the t = 0 modulation row) and leaves each layer's
fp16 K/V; each step runs the 32 blocks on the image tokens only.
- GEMMs stay ggml graphs on the runner's backend (sd.cpp's MMQ / BLAS kernels and numerics); fused
LayerNorm+modulate, gate+residual(+next LayerNorm), q/k RMSNorm+RoPE+fp16 K/V, SwiGLU and, on gfx11, a
WMMA flash attention (8 waves x 16 queries, 64-key tiles, fp32 softmax) run on the backend's stream.
- apply.py copies the two files into the pinned checkout and makes two small edits (runner choice,
SD_QI_NATIVE for CUDA / HIP builds); anything not covered falls back to the stock runner.
780M, Q8_0, seed 42: 512 2.11 s/step (stock 2.85), 1024 9.58 s/step (stock 14.2), image 96.5 s (131.8).
A parked pool worker watches the epoch's cache line with UMONITOR and sleeps in UMWAIT (C0.1) until the host publishes or the cycles run out. On a laptop whose CPU and GPU share one power budget the spin's power goes back to the GPU. Unset, 0, no WAITPKG or a non-MSVC build: the spin, unchanged. STRATA_POOL_UMWAIT_C02=1 picks the deeper C0.2 state, STRATA_POOL_UMWAIT_ESC=<cycles> C0.1 first and C0.2 after.
Two places upstream builds QsaShapes by hand from the geometry (chunk_bytes, added since the geometry change was
written, and the conversation-state fingerprint, in both trees). They now call qsa_shapes(g) like layer, mtp,
session, verify and prefill; for every file the guard accepts the values are the ones they built before.
generate.cpp reads the geometry before anything else looks at the model, so a model of another architecture was
refused as "missing qwen4exp.block_count" where 0.1.40.2 said "architecture is 'qwen35', this engine requires
'qwen4exp'" (found running Qwen3.8-27B through --native). read_geometry now checks general.architecture first, and
check_architecture is read_geometry plus geometry_unsupported. gguf_geometry_test: a dense qwen35 file (Qwen3.8-27B's
keys) and a file without general.architecture.
Tested on gfx1151 (HIP build of this branch, Windows): the GGUF tests pass; Qwen3.8-27B UD-Q6_K_XL, Q8_0 and
HauhauCS Q4_K_P are refused by architecture in 0.5 s; Qwen3.8-Flash-Next UD-IQ4_XS, greedy, 256 tokens on three
prompts (39, 48 and 5,568 tokens), gives output identical to the installed 0.1.40.2.
The f32 and array members added for gguf_geometry_test left the braced Kv{...} initialisers short of the struct, which
clang's -Wextra reports as -Wmissing-field-initializers in every test that includes the fixture (MSVC /W4 does not).
make_kv() sets the key and type, and each helper sets its own value. Built with the HIP toolchain (gfx1151):
gguf_geometry_test, gguf_split_test, gguf_reader_test and native_dense_ple_key_test pass, with no warning from the
fixture.
read_geometry (gguf_reader.hpp) fills ModelGeometry from the qwen4exp.* keys, under llama.cpp's names: layers, widths,
GDN, attention, indexer (heads, key width, pooling block from compress_ratios, top-k), rope width, hyper-connections,
experts, RMS epsilon, and the vocabulary from tokenizer.ggml.tokens. The six keys the guard always required stay
required; any other absent key keeps the compiled value. Keys that state one thing twice must agree (the interval and
compress_ratios, key and value head widths, routed and shared expert widths, ssm.inner_size and state x v heads).
check_architecture no longer hard-codes 48 / 2560 / 24 / 2: it reads the geometry and refuses a file the kernels are not
compiled for, naming the key ("qwen4exp.block_count = 40, the kernels are compiled for 48"). Only the expert count,
the experts per token (1..64) and the vocabulary may differ. ModelGeometry moves to core/geometry.hpp (no
dependencies), with kCompiledGeometry; the kernels that fix a dimension at compile time static_assert it there, in the
CUDA/HIP tree and the SYCL copies (plus qsa_prompt_attn_xmx).
generate.cpp takes the geometry and K from read_geometry, and the native embedding takes n_embd / n_vocab from it
instead of a literal 248320. The QSA kernel shapes come from one qsa_shapes(g) (layout.hpp) in layer, mtp, session,
verify and prefill, so n_rot, idx_block and idx_top_k follow the geometry too. Mirrored in sycl/.
Tested: tests/core/gguf_geometry_test.cpp (new, synthetic GGUFs) 28/28, gguf_split_test and gguf_reader_test pass,
built with MSVC /W4 /WX on Windows. `gguf_geometry_test --real` on Unsloth's Qwen3.8-Flash-Next UD-IQ4_XS shard 1
reads exactly the compiled shape and the guard accepts it. Not built here: the engine (no CUDA, HIP or oneAPI compiler
on this PC), so generate/layer/mtp/session/verify/prefill and the kernel asserts are reviewed, not compiled.
The Debian fallback loop ran with check=False on Ubuntu too, so a failed 13.0 install (a
network or apt lock error) went on to 13-1..13-4 and then past the loop without a word. Ubuntu
is back to the single checked install of 0.1.43; Debian tries 13-0..13-9 and fails with
guidance when none installs. Checked against NVIDIA's repos today: debian12 and ubuntu2404
carry 13-0..13-3, debian13 starts at 13-1 (up to 13-4). Two tests added.
The CUDA apt repo keyring and cuda-toolkit-13-x packages exist for
debian12/debian13 as well as ubuntu2204/ubuntu2404, so derive the repo
name from /etc/os-release instead of hardcoding ubuntu. Debian 13's
repo carries no cuda-toolkit-13-0, so try 13-0..13-5 in order (Ubuntu
still gets 13-0 first, same as before).
Checked with tools/test_setup_cuda_repo.py (4 tests, no GPU or network
needed): repo mapping for ubuntu/debian/other and the minor-version
fallback.
opencode puts its <env> block (working directory, today's date) near the start of the system message, before ~40K characters that never change. The prefix cache only matches a prompt's start, so each new folder or day read the whole system turn again. With the switch the block goes to the front of the first user message (string or content parts), for OpenAI and Anthropic requests. Off by default; serve/test_env_to_user.py; docs/DETAILS.md.
The server sends BYIELD for a waiting request under half as long only when a PP line (or the 10 s heartbeat) arrives,
and the engine looks for it at its next boundary. Where a boundary's own PP line is the only line for a while - every
chunk of a --batch read on one GPU, and #2050's pipelined pieces - the BYIELD answers that line and reaches the engine
after it has looked: the read gives way one boundary later, or never when that was the last one it could take. It is in
time only where the engine saves a periodic checkpoint after the PP line (every 16,384 tokens by default).
_control() now also calls on_quiet() whenever the engine has been quiet for half a second (its existing poll), and the
solo and admission reads run the same check there as at a PP line. A BYIELD for a read that has ended is ignored by
the engine, as before. serve/test_parallel.py: the fake engine takes --pp-every K (a PP line and a boundary every K
chunks); a solo read and an admission must give way at their first boundary (both fail on main), and the rule (only
to a much shorter request) holds on the poll too.
Measured on one RTX 5090 (Swift 1.5 IQ3_S, --batch 4, the engine built from main), main's server and this one, two
engine starts each (M F F M): a one-line request sent 0.2-8.6 s into a cold ~33K read, with nothing else decoding and
with one stream decoding. Late (a boundary passed after the request waited): 28 of 40 runs on main, 12 of which never
gave way; 4 of 40 with this change, all requests that started waiting 0.02-0.10 s before that boundary's PP line.
Where main was late the short request's TTFT went 3.56 -> 1.70 s (nothing decoding) and 4.40 -> 1.57 s (one stream);
where main never gave way 4.60 -> 1.51 s and 7.59 -> 2.35 s. Where both gave way at the same boundary, both requests'
TTFTs were the same (medians within 0.02 s). On 2x RTX 5090 with #2050's STRATA_PIPED_READ_PIECE=8192: 6 of 12 late without
it, none with it.
On two equal cards every K whose caches hold all the profiled pairs predicts the same window
time, and the search kept the first one tried: the smallest K, which gives the last stage (it
also carries the MTP drafter) the most layers and lets its cache fill the card to the reserve.
2x Arc Pro B70 with IQ3_S: auto K=24, CUDA1 at 31.8 of 31.9 GiB, ~11 tok/s; --layer-split 30
~54 tok/s (agnaylor in #1440). A full xe card does not fail an allocation, it moves buffers to
system memory.
The SYCL search now scores every placement it tries with its smallest spare room (a stage's
room after its session minus every expert of its layers) and, among the placements within
STRATA_SPLIT_HEADROOM_TOL (default 0.2%) of the best predicted time, takes the one with the
most (split_rules::pick_with_headroom). A log line names both picks when it moves the split.
STRATA_SPLIT_HEADROOM=0 keeps the old pick. CUDA/HIP placement is unchanged.
Test: split_rules_test (a 48-layer two-card model of the report: plain K=24 with 664 MiB spare
on the last card, headroom K=25 with 1728 MiB; tolerance and tie rules).
The three are USER_DEFINED (type 4) tokens like </tool_call> (ids 248058, 248066, 248070), so
the tokenizer matched them inside a message's text as the real call / tool-result markers.
They join TOOL_LITERAL_TAGS: inside a user, system or tool message (and the tool list) they are
encoded as ordinary text; an assistant message replaying raw call text keeps its tokens, as
for </tool_call>.
One exception keeps the template's behaviour: a user message whose whole text is
<tool_response>...</tool_response> (a client that formats tool results itself) keeps that outer
pair as the markers, since the template tells such a turn from a real question by exactly that
pair (last_query_index); the text inside is still marked.
Found by 47Hunter47 in #2026. Tests: serve/test_frontend.py QuotedToolCallCloser (every tag
and role, the assistant replay, the outer pair as string or text parts).
M5 Max: ./setup.sh --model UD-Q4_K_XL --vision yes: shards checked by SHA-256, repacked and verified, the pack, the
image encoder; the config it writes answers the test picture ('STRATA 42', 'Red circle / Blue square / Green
triangle').
The routed experts are placed after the engine's buffers: all in VRAM when
they fit (unchanged), else every expert in host memory with the same number
of VRAM slots per layer (--expert-vram GiB, --expert-slots N). Batches of up
to --hybrid-rows rows run the resident experts on native_expert_grouped and
the others on the CPU ExpertPool (ggml-cpu arithmetic) side by side; bigger
prompt chunks stream each layer's experts into two staging buffers for MMQ
(prompt chunk 2048 in this mode). Slots follow the routing counts (after a
prompt, every 32 decode positions). ExpertPool's native phases split rows by
the layer's n_ff / n_embd instead of Flash-Next's constants (same values
there). STRATA_MMQ_KQ=1 by default for this family.
ple_conv_add reads the conv kernel as half: Unsloth's file stores it F32, so it is narrowed (round to nearest even)
into its own buffer; the GSQ packs' F16 tensor is read in place as before. A file whose HC matrices are Q8_0
already makes no copies (the copy buffer was allocated with 0 bytes and the Metal allocation failed).
M5 Max, UD-Q4_K_XL resident (the repacked 82.5 GB file): greedy prompt1 identical to the CUDA reference (z440),
story first differs at token 36; MTP output equal to no MTP; decode no MTP 60.6 / 60.0, MTP --spec 2 93.9 / 74.0
tok/s; prompts 4K 1,374 / 32K 1,288 tok/s, needles found (4K, 32K). llama.cpp Metal on the same shards: 48.4 /
47.3 decode, 821 / 739 prompt.
With STRATA_FS_SLOTS > 0 and the resident RAM mode's adaptive exchanges, a slot copy issued
from an expert's RAM place (note_miss / the router look-ahead's predict) could land after an
exchange rewrote that place with the outgoing expert's bytes; take() then served expert Y's
bytes for expert X (FS_VERIFY: 19 BAD on clean v0.1.43, reported in #2134).
commit_exchanges, commit_copies and commit_flip now hold every staged expert (in and out) in
the swap space while their bytes move: its slot is dropped (never served again) and no copy
of it is issued until the commit is done (commit_copies holds until commit_flip, so the
asynchronous tier's half-done exchange is covered, and the rotation path too). Unlike the
patch in the issue, a dropped slot is not handed out at once: it waits the same `depth`
finished windows as any slot a window may still read, since a window planned before the
exchange can be reading it. "N dropped by exchanges" joins the swap space's report.
Foresight is opt-in; with STRATA_FS_SLOTS unset nothing here runs. Test:
file_expert_source_test --rotation-gpu (both commit paths, the depth rule, the bytes after).
Based on the analysis and patch in #2134.
Co-authored-by: radoslawsz <[email protected]>
The stdin reader thread is detached and captured main's stop flag, mutex, condition variable,
line queue and EOF flag by reference. The server writes QUIT and closes stdin right after it,
so the reader often wakes with EOF while main is returning: it then locked a destroyed mutex
and wrote to a destroyed deque (undefined behaviour on every platform; the Metal port aborted
on it). The state now lives in a shared_ptr that the thread holds a copy of, so it outlives
main's frame for as long as the reader runs. CUDA/HIP and SYCL engines alike; no change to
what the engine reads or answers.
expert_source.cpp's Linux-only STRATA_TEST_UNMAP_GGUF check had a literal 0x00 byte as its
character constant (since 0.1.43; gcc warned "null character(s) preserved in literal"), and the
#2126 buddyinfo parser in pinned.cu got one the same way (a heredoc dropped the backslash).
Both are the escaped character now. Git took pinned.cu for a binary file because of the byte,
so it was committed with CRLF line endings; it is back to LF (no other change).
tools/test_source_nul_bytes.py fails when any tracked source file holds a NUL byte again.
Prompt chunks run the attention scores and P V as FP16 strided-batched GEMMs
with FP32 accumulation (K / V cells and Q converted per chunk, the causal
softmax writing FP16 probabilities), replacing the FP32 SGEMMs. The prompt's
delta rule splits each V head over 4 column groups (16 row groups of 8 rows,
the norm in its own kernel); decode, verify windows and commits keep one
kernel. pp4096 on gfx1201: 2557 -> 3290 tok/s.
The prefill hook hands the prompt's image rows (sp.embd_rows) to the Metal prompt paths (prompt_chain.inc, the
streaming prompt path, the 8-row windows), which take an image row in place of the token embedding.
mrope_table_set keeps the host view of the server's (t, h, w) table; every window, prompt chunk and draft round copies
the rows it reads into the window's buffer, bound at index 20 of qsa_prep with function constant 24 (mac-kern
29724e5e) - the window, both prompt paths and the draft layer. The cached window's QsaArgs are patched by the last
inline argument (the table is bound after it). setup_mac.py --vision yes: the pinned projector (SHA-256) and
strata-vision built with ggml-metal; the config gets --vision and the encoder.
missing.cpp: a crash prints its call stack and fault address.
M5 Max, Q2_0, ISTA's projector, a test picture (shapes and 'STRATA 42'): 'STRATA 42' and 'Red circle / Blue square /
Green triangle', the same answers as the HIP engine on the R9700 (p620) with the same picture and files, also
through the config setup writes. Text greedy unchanged with and without --vision (prompt1, story, the 1,662-token
chain prompt). prefill.cpp: the CUDA (P1004) and HIP (p620) objects are byte-IDENTICAL (preprocessor-only).
The greedy divergence seen with the fused single-token kernels is not a
kernel bug: fused_gdn_conv_l2 and k_conv_seq leave the same history and
differ by 1 ulp in h (FMA contraction of the 4-tap sum); the Q8_1
activation rounding turns that into a flipped near-tie (p01 token 4,
margin 0.43). KL with either is the same. Drop the switch.
Lane tid wrote block tid % 8 of its superblock, so a sub-group's 2-byte stores landed 64 bytes apart. Lane tid now
takes block tid / 4 (the same 32 (block, part) pairs, the same values) and writes its runs with store_run. UD-Q4_K_XL's
down projections are Q5_1 and UD-Q5/Q6/Q8_0's are Q8_0, so this is most of their FP16 prompt-path dequant.
B70, dequant_bench (32 experts, gate/up + down): Q5_1 7.18 -> 2.83 ms, Q8_0 6.77 -> 3.53 ms; Q4_0 3.21, Q4_1 2.76,
Q5_0 3.74 ms after; the FP16 output hash of every type unchanged. dequant_bench lists Q4_0 / Q4_1 / Q5_0 too.
The lane kernels of native_expert_grouped re-read and re-decoded a Q4_K / Q5_K / Q6_K / Q5_1 / Q8_0 expert row once
per entry of its group (the IQ formats already decode once, Multi). Each of these dots is now split into load (the
weight words and scales) and apply (the activations and the same float expression), and the group's entries go
through the existing multi-entry path in chunks of 4 and 2; every entry keeps its own k order and lane sum, so the
outputs are bitwise the per-entry kernels'. STRATA_KQ_SPLIT=0 keeps the old kernels.
B70 (bmg-g31), 2560 x 640 experts, GB/s of expert bytes, 2- / 4-token windows: Q4_K/Q5_1 362 -> 402 / 369 -> 425,
Q5_K/Q8_0 395 -> 440 / 404 -> 464, Q6_K/Q8_0 259 -> 307 / 256 -> 318, Q8_0 430 -> 479 / 429 -> 502; one token -1..2%.
Experts read from pinned host memory (the NO_HOST mirror) go from 18-19 to 23 GB/s on those windows.
Also: STRATA_KQ_GU_LANES (opt-in; the K-quant / Q8_0 gate/up lane split alone, other bits), native_grouped_parity
--kq (split vs per-entry bitwise, and a CPU float reference with ggml's dequantize_row_* for Q4_K/Q5_K/Q6_K/Q5_1/Q8_0)
and --kqbench (per format pair, device or pinned host memory), Q6_K blobs with a finite scale in that harness,
dequant_bench and kq_cpu_bench for the Unsloth formats in the SYCL build.
--mtp D (0..7) loads blk.<n_layers> (eh_proj, enorm/hnorm, attention + MoE
layer, shared_head_norm) and runs greedy requests as speculative steps:
D chained drafts, one verify window, commit of the accepted rows' GDN state
and conv history. Verify windows and plain decode share the per-token GDN
kernels, so the text is the plain greedy text (10 of 10 prompts identical).
Serve reports accepted/offered drafts in DONE.
- moe_mm.metal: W8<Q4_K / Q5_K / Q5_1 / Q6_K> (the lane's runs k and k + 16 share the sub-block's scale, min and
high bits; 4-aligned code runs read as one uint); mm_down3 takes the paired-run decode on the COOP_K4 layout (TK
32). Prompt per layer at T 2048: Q4_K gate/up 10.6 -> 6.0 ms, Q5_K 15.4 -> 9.0, Q6_K 17.2 -> 14.0, Q5_1 down
5.0 -> 3.3; the same values (moe_mm_parity v3 incl. Q2_0 and IQ3_XXS / IQ4_NL)
- qsa.metal: QSA_MROPE (function constant 24) - interleaved M-RoPE (ggml IMROPE, sections 11/11/10): buffer 20 of
qsa_prep = int32 [cells][3] (t, h, w); rotary pair i of the row at pos_base + cell rotates by the cos/sin row
tab[(pos_base + cell) * 3 + i % 3] (pooled indexer keys at pos_base + 4 b too). qsa_parity QSA_MROPE=1 (random
table vs the CPU reference) / QSA_MROPE=id (identity table: the same numbers as the plain kernel)
The HC read takes, per matrix, its load-time Q8_0 copy or the file's own tensor: Unsloth's Q8_0 down/up need no copy,
their F32 inject rows go to gr_down_q8f (mac-kern 945c0d24). router_route_<type> for BF16 and F32 (opt-in). The M4's
v2 prompt kernels: K-quant gate/up TK 128, Q8_0 / IQ4_XS TK 32, Q5_1 / Q8_0 / IQ4_NL down TK 32. GSQ packs unchanged:
Q2_0 prompt1/story, IQ3_XXS prompt1 and the 1,662-token chain prompt give the same tokens as before (M5 Max).
tools/vision/strata_vision.cpp: no posix_fadvise on macOS (it does not have it); strata-vision then builds there with
ggml-metal.
- unsloth_budget / UNSLOTH_RESIDENT_BESIDE_GB 16, ported from sergqwer's fork (sergqwer/strata-nvfp4 3f3546f6, MIT):
no --resident-budget-gib where the experts and 16 GiB fit the RAM (UD-IQ4_XS 71.4, UD-Q4_K_XL 87.7, UD-Q5_K_XL 107.6,
UD-Q6_K_XL 117.7, Q8_0 135.5 GiB), the budget below that, when asked for, and on a unified-memory APU (#1715).
The menu, --check and the install say which; the menu's default for the family is the largest file whose experts
all fit (UD-IQ4_XS where none does).
- UD-Q5_K_XL / UD-Q6_K_XL / Q8_0: six shards each, sizes and SHA-256 pinned at 38bb39ee; "ram_gb" is the all-in-RAM
need, below it setup asks first (default no) and says how many GiB are read from the SSD. Q5/Q6 need engine 0.1.44
(the Q6_K expert kernels in every build); Q8_0 0.1.38.
- UD-Q4_K_XL: ran on an R9700 (docs/QUANTS.md), so #429's AMD question is gone, and it is not experimental.
- tools/strata_mcp.py mirrors the table and the verdicts. Golden configs re-recorded: only UD-Q4_K_XL on 96 GB
(65536, --kv-resident) and 128 GB (131072) changed, as in the fork.
Tests: test_setup_unsloth Resident and BigFiles (the budget tests force the budget mode where their PC now fits every
expert), test_strata_mcp, every tools/test_setup_*.py.
Co-authored-by: sergiywith <[email protected]>
A driver of its own beside Flash-Next's (generate/verify/prefill untouched), built from Strata's kernels:
- weights from the GGUF: dense Q8_0 on the native MMVQ (with the packed Q8_0 copy), the routed experts
repacked at load into native expert blobs (stride in whole blocks for MMQ), F32 norms / projections;
- the plain pre-norm residual layer driver, the residual add fused with the next norm, the MoE end
(combine + shared-expert gate + add + next norm) in one kernel;
- GDN: the fused conv+L2 and SiLU step for a token, one-launch sequential kernels for a prompt chunk;
- attention without an indexer: split-K decode over 256-cell chunks (positions on the device, so the
decode step is one captured graph), and for a prompt chunk scores / values as FP32 GEMMs per KV head
with a causal softmax;
- prompt projections as FP16 GEMMs (prefill::Gemm::native), the F32 router / alpha / beta as SGEMM,
the prompt's experts on llama.cpp's MMQ (entries sorted by expert on the device);
- decode: native router 256/8, native grouped experts (Q5_K / Q6_K downs added to the grouped
kernels' down list for FF 512), router + shared gate in one product, alpha/beta in one launch.
- serve: READY / GEN (sampling keys) / T / PP / DONE / STOP, the held state reused when a request
extends it; --tokens / --force / --dump-full-logits for tools/qwen35/kl.py; --bench-pp / --bench-tg.
- hip_compat cublas_v2.h: cublasSgemm / cublasSgemmStridedBatched.
setup_mac.py: the Unsloth files by name and SHA-256 (setup.py's pinned revision), repacked into one GGUF for the GPU
with every tensor verified, the n-gram table found by name, --native-dense-gguf the repacked file; setup writes
STRATA_METAL_STREAM to the config so the engine follows its choice. UD-* are behind 'missing' (the Metal expert
kernels for Q4_K/Q5_K/Q6_K/Q5_1 are in progress) and only reachable with --model.
stream.cpp: auto streams when the mapped weights + 12 GiB do not fit the GPU working set (was: experts > 60% of
it, which would have streamed UD-Q4_K_XL on the 128 GB M5 Max). M5 Max: Q2_0 stays resident (same tokens and speed);
M4 32 GB: Q2_0 streams as before.
Shard 1's metadata (without split.*) and every tensor but the n-gram table, byte for byte: dense first, then each
layer's routed experts together; --verify hashes every tensor (SHA-256) against its source shard. For Unsloth's
UD-*_K_XL (4-6 shards cutting through layers). Checked on Q2_0 (M5 Max): 35.05 GiB, 1,223 tensors identical,
written and verified in 33 s; the engine on it (--native-dense-gguf the same file, --ple-gguf the original shard 2)
gives the same tokens as on the original shards, prompt1 and story, at the same speed (114.2 / 107.7 tok/s).
From sergqwer/strata-nvfp4 1688a344 (src/prefill/mmq_expert_bench.cpp), with UD-IQ4_XS's gate/up and down formats
added as comparison cases (an f16 d leads their blocks, as Q8_0's).
Co-authored-by: sergiywith <[email protected]>
row_dot_20_sub8: lane t takes k = t (+ k = t + 16 for t < 4, else + 0) and k = t + 8 (+ 0), then xor 4, 2, 1 - the
order warp_sum adds a K = 20 row in (lane i + 16, then + 8), so the result is the same bits; native_down_multi_kernel
routes TD 40 there (as IQ4_NL's K = 40 row_dot_40_sub8) and the down grid takes 32 rows a block for d_type 40.
nvfp4_decode_bench, RTX 3060, 64 distinct experts: down alone 0.260 -> 0.189 ms at 1 token (226 -> 312 GB/s, 87% of
360); the whole expert 0.626 -> 0.554 ms (89%), 2 tokens 0.686 -> 0.596, 3 0.786 -> 0.661, 4 0.910 -> 0.766 ms (from
2.261 before Split<40>). Output hashes unchanged, nvfp4_expert_gpu_parity unchanged.
The ported NVFP4 decode experts had no Split, so they took the per-entry kernels: every weight was decoded again for
every token of a verify window, and the kernel slowed almost linearly with the tokens per expert. Measured with
nvfp4_decode_bench on the RTX 3060 (64 distinct experts, sm_86, CUDA 13.4; best of 50):
| tokens per expert | before | after | |
|---|---:|---:|---:|
| 1 | 0.714 ms (248 GB/s) | 0.626 ms (283 GB/s) | 1.14x |
| 2 | 1.200 ms | 0.686 ms | 1.75x |
| 3 | 1.730 ms | 0.786 ms | 2.20x |
| 4 | 2.261 ms | 0.910 ms | 2.48x |
(UD-IQ4_XS's IQ3_S / IQ4_NL experts, 15% fewer bytes: 0.512 / 0.578 / - / 0.814 ms.)
- Split<40>: vec_dot_nvfp4_q8_1 split into load (the E2M1 codes through kvalues_fp4, the two sub-block scales) and
apply (the dp4a chain and the float expression in the same order), so the multi-entry kernels apply one decode to
up to four tokens. The multi kernels now apply the NVFP4 tail scales on their outputs as the per-entry kernels do
(s_gate / s_up on gate/up, s_down on down; x * 1.0f for every other format, which is exact).
- ue4m3_to_f32_half builds the value from its bits instead of two ldexpf and branches: equal for all 256 bytes
(nvfp4_decode_bench checks them).
- down: NVFP4's rows are K = 20 calls at n_ff 640, as Q2_0's, so they take row_dot_20_sub16 (2 rows a warp, 16 rows a
block) instead of one 20-of-32-lane row a warp: down 0.361 -> 0.260 ms at 1 token. Its addition tree is
warp_sum's (lane i + lane i+16 first), bitwise.
Bit for bit: the output hash of every case above equals the per-entry kernels' (STRATA_OLD_IQ_MMVQ=1);
nvfp4_expert_gpu_parity on real GPTQ blobs unchanged (1.088-1.168%). Other formats: only the store multiplies by a
1.0f scale, and the down launch grid changes for d_type 40 alone.
nvfp4_decode_bench [gu_type d_type] [groups] [tokens per group] [iters]: native_expert_grouped on distinct random
blobs (64 x 2.76 MB, far past L2), best/mean time, weight GB/s against the card's DRAM peak, and an FNV hash of the
output so a kernel change can be checked bit for bit. NVFP4_BENCH_PHASE=1|2 times gate/up or down alone. Also an
exhaustive check of the UE4M3 decode built from bits against the ldexpf form (all 256 bytes).
- fused_gdn_step_norm_silu: the fused GDN step with the SiLU output gate (Qwen3.6), a compile-time
template flag; fused_gdn_step_norm (Flash-Next, sigmoid) is the same kernel instantiated as before.
- native_router_topk(n_expert, k): 512/10 goes to the existing pinned top-10 kernels unchanged; 256/8
is a templated copy of the same topk-moe contract.
- strata-q36ref --kernels strata (default): native MMVQ (q8_1) for every quantized matmul, dense and
per routed expert (Q4_K/Q5_K/Q6_K/Q8_0), the fused GDN conv+L2 and SiLU step, the native router;
--kernels plain keeps the reference kernels.
- kl.py: answer-only statistics beside all steps, llama.cpp's logits cached between runs.
- quant.metal: QUANT_CTAB (function constant 23): the IQ3_XXS / IQ3_S / IQ2_XXS / IQ2_XS / IQ2_S grids and the sign
table read from ggml-common's constant arrays, no threadgroup copy (same values). Decode moe_gu: 3-7% faster
(IQ3_S-pack T2 window 14.96 -> 14.79 ms, 3 of 3); token_chain default (CHAIN_CTAB=0 off). Prompt mm_gu3: slower
(kept off)
- moe_mm.metal: mm_gu4 (decoded operand shared by MS 16-row token tiles): IQ3_S 9.5 -> 8.6 ms, IQ3_XXS equal, Q2_0
slower; mm_gu5 (weight blocks staged in threadgroup memory, coalesced): slower (12.9-20.6 ms); W8 templated on the
address space for IQ3_S / IQ3_XXS / IQ2_S; probes MM_PROBE 5 (loads only: as slow as the full decode)
- moe.metal MOE_PROBE (function constant 22; no table load)
- finding: the prompt i-quant decode is bound by its scattered small device loads (2-7 a 4-value run, 8 rows a load
instruction), not by the grid lookups or the ALU
--family nvfp4 --model NVFP4 | NVFP4-NVIDIA (MODEL-UPDATE Phase 4). The files come from the packs' Hugging Face
repositories at the commits the fork's setup pins (NVFP4_REPOS: Maximilian228/Qwen3.8-Flash-Next-NVFP4-GPTQ-Strata
640a1db9, ...-NVIDIA-NVFP4-Strata cca8f7fe), each checked against its size and SHA-256; nothing is converted on the
PC and no file is mirrored. The FP8 n-gram table and the BF16 embedding are the same file in both: a second install
links them. The MTP draft layer is setup's own, as for every model. The engine gets the fork's model flags
(--native and --native-dense-gguf on the dense GGUF, --ple-gguf, --embd-gguf); the rest of setup's choices (context,
KV, the low-RAM mode on the pack's experts.bin) are the usual ones: 96 GB keeps every expert in RAM, 64 GB with a
32 GB card the resident low-RAM mode. NVIDIA only: on AMD setup stops and names UD-IQ4_XS (the HIP engine has no
NVFP4 kernels). Images: off for these packs. A ready-made engine older than NVFP4_ENGINE (0.1.47, to be set at
release) is replaced by one compiled from this source.
Tests: tools/test_setup_nvfp4.py (fetch from a file:// mirror with SHA-256 checks, the shared file linked, a wrong
file refused, the config name read back, mocked installs on 128/96/64 GB, AMD refused); test_setup_pins covers the
new table. Every tools/test_setup_*.py passes.
Co-authored-by: sergiywith <[email protected]>
- sycl: iq_dequant_f16 / iq_dequant_gu_f16 / moe_combine accept the NVFP4 scale arguments the shared headers now
declare (refused if ever given: the SYCL port has no NVFP4 experts), and the no-MMQ stubs match the new mmq
signatures (the calls were ambiguous with the defaulted ones).
- CMake: list(TRANSFORM ... REPLACE) with \2 of an optional group failed on CMake 3.28 for a plain "120"; two
rewrites, one per form.
- src/qwen35/q36ref.cu (target strata-q36ref, CUDA and HIP): Qwen3.6-35B-A3B on one GPU, the whole
model resident, one token at a time with fp32 activations: plain pre-norm residual, GDN with the
SiLU output gate (32 V heads over 16 K heads, conv 8192), dense gated attention (16 Q / 2 KV x 256,
NEOX RoPE over 64 dims), softmax top-8 router over 256 experts with the sigmoid shared-expert gate,
every weight read in its GGUF form (F32/F16/BF16/Q8_0/Q4_K/Q5_K/Q6_K). --act q8 (default) rounds the
activations of quantized matmuls as llama.cpp's GPU path does; --force / --dump-full-logits for
teacher-forced KL; --dump-layers / --dump-logits for parity.
- tools/qwen35/: q36_llama_dump.cpp (llama.cpp's side through the eval callback), parity.py
(greedy answers + per-layer error), compare_dumps.py (two runs, e.g. the GPU-vs-CPU floor), kl.py
(teacher-forced KL against llama.cpp HIP with the CPU backend as the floor), parity_prompts.json.
The fork was built with MSVC only: on Linux its parity tool's F16C rounding helper failed to inline without the
target flag, and the SYCL tree's own list of CPU expert sources lacked nvfp4_avx512.cpp / nvfp4_avx2.cpp.
Port of sergqwer/strata-nvfp4 c83625c3 (the rule for NVFP4 experts in any layer) onto 0.1.43's default, with its
#1884 nearly-all-cached exception kept. On the fork's qwen-nvfp4-gptq pack the skip moved the answers by
teacher-forced KL 0.035 against it off, about the GPTQ quantization's own error against an all-Q8_0 reference
(0.038), for +11.5% decode (RTX 5090, the fork's measurement). Packs without NVFP4 experts keep the default
unchanged. The final default is the owner's, from our own 3060 A/B and KL against Q8_0.
Co-authored-by: sergiywith <[email protected]>
Without AVX-512 (Zen 2/3, Intel 12th-14th gen) the pool's NVFP4 rows went
through ggml-cpu's vec_dot, which re-decodes a row's blocks for every token of
a window. nvfp4_256_* decode a 64-value block once and apply it to up to 8
tokens. Each token's arithmetic is ggml_vec_dot_nvfp4_q8_0's AVX2 path step
for step (vpsignb, vpmaddubsw + vpmaddwd, the same FMA order, hsum_float_8),
so every row is bit-equal to it: an AVX2 CPU computes what it did before.
nvfp4_avx512_parity checks both kernels, whichever this CPU runs (the AVX2
rows must also be bit-equal; 0 differing rows of 1..9 tokens against the
portable and the native ggml-cpu builds), and is now a ctest
(nvfp4_cpu_parity). One expert's gate+up, Zen 5 core: 1.12x ggml at one
token, 2.00x at 4, 2.35x at 8; under DRAM load (--bw) with 6 threads 47.3
against 24.0 GB/s at 4 tokens, 26.6 against 12.0 at 8. Chat decode on the
RTX 5090 with STRATA_FORCE_AVX2=1 and 6 pool workers: 16.5-16.9 ms a round
against 17.3-17.5 (pool 6.6-6.8 against 7.2-7.3 ms).
STRATA_NO_NVFP4_256=1 falls back to ggml-cpu.
(cherry picked from commit 0c859e15f89054a376db7458bfb298e3e6c81f22 of sergqwer/strata-nvfp4)
Upstream's 04dbe561 allocates cpu_rows at its largest before the walk; the fork's allocation of cpu_ones (row_sd 1 for
the CPU's rows on an NVFP4 pack, b59f5dda) sat inside the reallocation that then never ran, so a staged chunk with the
CPU share copied from a null host pointer: "prefill moe_combine: invalid argument" (2K prompt, qwen-nvfp4-gptq).
(cherry picked from commit 52b272ebb92c9ef49029314da1a695d38f570d88 of sergqwer/strata-nvfp4)
Was 640df4f0, the fork's CPU share. Upstream 0.1.40.2 took it (978d3558, with 667f2eca's blobs taken on the
prefill thread and 4322e241's STRATA_DBG_NAN rows; opt-in there), so the port keeps upstream's code and adds only
what an NVFP4 pack needs: the CPU's rows carry s_down already, so their row_sd entries are 1 (cpu_ones, pinned
beside cpu_rows).
Port note: the release loop keeps 0.1.43's steps_up / tok_up / up_ring releases.
(cherry picked from commit a0c2ca233ac8fe8ec9d342bd319c93404791fa15 of sergqwer/strata-nvfp4)
- The blob keeps its {s_gate, s_up, s_down, 0} tail whenever any projection is NVFP4; the other projection's scale is
exactly 1.0, and every path (GPU decode, CPU pool, prompt path) already multiplies by the tail, so 1.0 is exact.
native_expert_layout (GPU) and native_fmt (CPU) no longer require NVFP4 in all three projections.
- tools/iq_pack.py writes such blobs (1.0 for a non-NVFP4 projection, .scale tensors only for the NVFP4 ones).
- native_expert_parity: the synthetic NVFP4 blob gets its tail (it was left zero, so every NVFP4 synthetic expert
computed 0 and failed), non-unit for NVFP4, 1.0 for the other projection, and the reference applies it.
nvfp4/q8_0, q8_0/nvfp4, nvfp4/nvfp4 and q8_0/q8_0 pass: CPU and GPU within 1e-7, ~1.2% from the float reference
(activation rounding).
(cherry picked from commit 473c0959141b36d28e7a2eb16005f8c8a4e3c8d0 of sergqwer/strata-nvfp4)
0.1.38 took the fork's group gather without the NVFP4 parts: each expert's 16-byte scale tail now goes to its
group slot and the group's last flush zeroes the MMQ tail, in the same launch (as #353 on 0.1.38).
(cherry picked from commit 8f2193f028940b9a6d270203b640562435937a7a of sergqwer/strata-nvfp4)
A ModelOpt NVFP4 checkpoint of Qwen3.8-Flash-Next (its routed experts NVFP4, group 16) runs on the native path.
Rebased on 0.1.31: NVFP4 is type 40 in the per-role format lists (STRATA_GU_FMTS / STRATA_D_FMTS / STRATA_MMVQ_FMTS)
beside Q4_K/Q5_K/Q5_1/Q8_0, and 0.1.31's dq_q8_0 serves both.
- tools/nvfp4_convert.py: llama.cpp's converter with the engine's type policy (BF16 control projections, Q8_0 native
tensors and token embedding, no PLE table); tools/nvfp4_verify.py / nvfp4_verify_gguf.py prove the experts bit for
bit.
- tools/iq_pack.py: expert_layout() gives NVFP4 blobs a 16-byte tail {s_gate, s_up, s_down, 0} (the per-expert
weight_scale_2), so every consumer that moves whole blobs moves the scales; an NVFP4 pack always writes experts.bin
(the GGUF has no tails, so the in-place GGUF read does not apply).
- decode: NVFP4 in the native expert kernels (q8_1, UE4M3/E2M1 decoded in software, bit-identical to ggml); each scale
on its own projection's FP32 output (folding s_down into up put the hidden into FP16's subnormals).
- CPU pool: AVX-512 NVFP4 rows (1.8-3.7x ggml-cpu's), ggml-cpu elsewhere.
- prompt path: llama.cpp MMQ - W4A8 by default (int8 MMQ with Blackwell's FP4 MMA hidden, mmq_nvfp4_w4a8.cu), W4A4
and FP16 through STRATA_PREFILL_NVFP4; the scales applied where they are read.
- CMake: the architectures stay as given. Only mmq_nvfp4_w4a4.cu (the FP4 x FP4 kernel and its FP4 activation
quantizer, symbols renamed) is built with 12x -> 12xa in strata_mmq_w4a4; without its image W4A4 falls back to W4A8.
strata.exe: 55 sm_120 cubins + 1 sm_120a.
- 0.25 of the missed experts over PCIe for NVFP4 packs (their blobs are 2.76 MB).
- Parity tools (mmq_nvfp4_parity, nvfp4_avx512_parity, nvfp4_expert_gpu_parity); HIP guards; docs/NVFP4.md.
Checked on this rebase (RTX 5090): the parity tools as before (decode experts 1.09-1.16%, MMQ W4A8 0.53%, W4A4 8.6%
- the same numbers as the 120a-everywhere build); IQ2_XS 128 greedy tokens identical to 0.1.31 main; the NVFP4 pack
against the 0.1.29-based build at the same expert-cache size: identical tokens on a 24-token and an 8K prompt, in W4A8
and in W4A4 (prefill 4,231 / 4,583 tok/s on 8K).
Port note: the PCIe share base for NVFP4 packs (0.25) sits beside 0.1.43's PcieAuto start values.
(cherry picked from commit 1f8a12858cc4ed80a977767afef6b6c3a754ac63 of sergqwer/strata-nvfp4)
- serve/images.py: OpenAI's images API (prompt, size, n, response_format b64_json|url) plus seed and steps; one
sd-cli process per request with the model card's arguments (8 steps, CFG 1, euler, Turbo's sigmas,
--diffusion-fa), the PNGs returned unchanged. The job takes the service's turn (one job at a time on the GPU)
and the config's gpu_policy says what the text engine does meanwhile: keep, shrink (--vram-elastic), unload
(its conversation saved to a session file and restored after the next start), auto.
- setup.py --images qwen-image (--image-quant, --image-backend, --image-gpu-policy): sd.cpp 2988060 + its ggml
as zips, built for CUDA / HIP / Vulkan (LunarG SDK into third_party when the PC has no Vulkan dev files) / CPU;
DiT quant by GPU memory (Q4_K_M <= 8 GB, Q6_K <= 12, Q8_0 above), VAE and the Qwen3-VL-8B UD-Q4_K_XL text
encoder at pinned revisions, each checked against the Hub's SHA-256. tools/image/install.py adds it to an
existing install.
- Web page: an Images tab when the server has image generation.
- docs/IMAGES.md; tests: serve/test_images.py, tools/test_qimage.py.
IQ3_S runs on Metal since mac-kern's IQ4_XS kernels: recommended from 110 GB (Apple's 128 GB Macs), IQ3_XXS where it
fits resident, then Q2_0. A pack that does not fit Metal's share of the memory streams its experts from the SSD (the
engine decides, f-mac-ssd): setup says so with the M4 numbers, writes --suffix-draft 0 and no --mtp (one token per
step, measured faster there) and does not fetch the draft layer.
session_zero and conversation_checkpoint_save/restore go to the Metal window under STRATA_GPU_METAL (the running
state lives there, not in the session's buffers): reset zeroes its GDN recurrences and conv histories, PLE history
and indexer tails (and the draft layer's); a checkpoint holds the same set, after the pending fold. Before this a
new request carried the last conversation's state (M5 Max, Q2_0: an unrelated request after a story answered
differently from a fresh engine; now the same), and on a streaming Mac the session buffers it zeroed or copied had
been given back (the engine stopped).
Checked on the M5 Max (resident) and the M4 (streaming): an unrelated request after another equals a fresh
engine's answer; a three-turn conversation reuses 35 / 73 of its tokens and its second answer equals a fresh
engine's. Shared files: session.cpp and conversation_state.cpp preprocess IDENTICAL and their objects are byte-
IDENTICAL on CUDA (P1004) and HIP (p620). Also: IQ4_XS's v2 prompt tile (_16_64_32_2), and a call stack when a
given-back allocation is touched.
- ModelFamily from general.architecture (qwen4exp, qwen35moe); the architecture guard accepts both, the
engine's guard (native dense/head, --native) refuses Qwen3.6 with "recognised, not run yet".
- ModelGeometry moved to core/geometry.hpp (+ family, shared-expert FFN, in-file MTP layers, vocab);
geometry_from_gguf reads every shape key, checked against the compiled kernels (Flash-Next files give
exactly the defaults; expert count and top-k stay run-time sizes).
- Per-family tensor layout tables (GDN / attention / nextn MTP block, hyper-connections or plain
pre-norm, indexer or none) and a GGUF directory check; MXFP4 block size in the reader.
- setup: --family qwen36 downloads unsloth/Qwen3.6-35B-A3B-MTP-GGUF (pinned 5bc3e238, size + SHA-256)
UD-Q4_K_XL / UD-Q6_K_XL / Q8_0 (+ mmproj) and says the engine does not run it yet; serve's default
model name per family; strata_inspect recognises qwen35moe.
- Tests: model_family_test (synthetic, --real for headers), test_setup_qwen36, serve/test_family_name.
Conflicts in metal/src/engine/window.cpp resolved: the stream's next-layer routing guess beside router_route
(opt-in); the cached window through the stream's segmented run, the kernel profile off while streaming; host reads of
the weights through W_ (the mapped file, or the dense weights in memory when streaming), also in prompt_chain.inc;
the drafter's prompt rows batched, from either prompt path. The prompt: resident -> prompt_chain.inc, streaming ->
the stream's prompt path (it streams each layer's experts for the chunk); STRATA_METAL_PROMPT=window: 8-row windows.
M5 Max: Q2_0 prompt1/story and IQ3_XXS prompt1 with MTP equal to the no-MTP references; the 1,662-token prompt
through prompt_chain.inc equal to before; Q2_0 streamed (STRATA_METAL_STREAM=1) equal to resident on prompt1. The
1,662-token prompt streamed (the stream's prompt kernels: chunks of 1024, flash QSA) first differs at token 21 of 60
from the resident prompt_chain.inc path (different kernels and chunking).
- gemv.metal: gemv_x<F,LPR,NR,SG> = gemv (KS 1) with the window's prepared x staged in threadgroup memory (bitwise
the same arithmetic); GEMV_PROBE 1/2/3 bench probes (skip later tokens' x loads / combine; rows-outer order)
- units.metal: X<F>::load templated on the pointer's address space (device or threadgroup), same arithmetic
- gemv_parity: GEMV_X=1 sweeps gemvx_*, GEMV_PROBE passes the probe
- M4 finding: a window token's cost is the fp32 combine (1 FMA a weight a token), at ~0.85 T FMA/s (near the M4's
peak); the x loads are ~6 us of the ~30-60 us a token, and staging x in threadgroup memory does not help
setup.py hands a darwin run to metal/setup_mac.py (nothing else changes on a PC); setup.sh fetches a pinned
standalone Python 3.12 into .python on a Mac without Python 3.10+ (no sudo, no Homebrew). The Mac setup checks the
chip, unified memory, macOS (15+) and the Command Line Tools; recommends the pack by memory (IQ3_XXS from ~96 GB,
Q2_0 below; IQ3_S only with --model: the Metal expert kernels lack its IQ4_XS experts); builds the engine with pip's
cmake/ninja, Apple's metal-cpp (pinned SHA-256) and llama.cpp's ggml at the pinned commit; prepares the native pack
and the MTP draft layer; writes strata-<model>.json (backend metal, --spec 2) and run-<model>.sh; starts
serve/server.py. tools/test_setup_mac.py: 10 tests with sysctl/sw_vers/xcode-select mocked.
On the M5 Max (m1002): ./setup.sh --model Q2_0 --gguf-dir ... --yes from a fresh copy: Python fetched, engine built,
pack and config written in 29 s (models already downloaded); the server answered /v1/models, the chat page,
streamed /v1/chat/completions and /v1/messages (Q2_0, 112-121 tok/s). python -m pytest serve -q on the Mac: 705
passed, 11 skipped.
The stdin reader (a detached thread) can take in_mu after the serve scope destroyed it, when the server closes the
pipe after QUIT; on macOS that lock throws and the engine ended with an abort. Inside #if STRATA_GPU_METAL: flush
and _Exit(0) after the QUIT profile save. HIP (p620): generate.cpp preprocesses IDENTICAL and its object is
byte-IDENTICAL against the gated tree (P2003 was unreachable for the CUDA compare; the change is preprocessor-only).
The serve loop asks cvec() every request: Metal returns the empty one (no --control-vector-scaled on Metal yet)
instead of the stub that stopped the engine; stubs.cpp regenerated without it. An uncaught exception now prints its
call stack (backtrace_symbols_fd) before the abort.
The BF16 router GEMV and moe_route as one dispatch beside the shared expert's GEMVs. Greedy output unchanged (Q2_0,
IQ3_XXS, both prompts, on and off). Not the default: in this window the router GEMV already runs beside the
shared GEMVs, and the T2 window measured 13.035 ms GPU with it vs 13.027 without (the fused kernel alone 0.615 ms
vs 0.566 for the two); end to end, --spec 2, 3 interleaved pairs, off -> on: Q2_0 113.61 -> 113.28 / 107.50 ->
106.97, IQ3_XXS 103.51 -> 103.07 / 96.19 -> 96.02 tok/s.
- router.metal (new): router_route<F> - the router GEMV rows as gemv's (LPR 32, KS 1) with 512-thread threadgroups,
the last threadgroup to finish (device counter, device-scope fences) runs moe_route's body and resets the counter;
logits / ids / w / plan bitwise gemv + moe_route. token_chain default (CHAIN_ROUTE=0: separate): T2 13.08 -> 12.93 ms
- gemv.metal gemv_cat: two same-format matrices over one x in one dispatch (ssm_alpha + ssm_beta), bitwise; no gain
in graph mode (CHAIN_ALBE opt-in)
- gr2.metal gr_norm_comb: the MoE combine folded into gr_norm, bitwise; +0.6 ms at T2 (CHAIN_COMB opt-in, not kept)
- units.metal Dec<Q3_K>: qs / hmask as packed_ushort4 loads (same values): T2 2560->10240 33.8 -> 32.2 us
- tests/fuse_parity: the three fusions bitwise for 1..4 tokens
With it a verify window of T > 1 commits its first row in place (commit = 0x100 | 1, mac-kern 56526aed): commit(1)
needs no fold, commit(n) folds rows 1..n-1; a prompt window commits all its rows in the step. Greedy output equal
to no MTP (Q2_0, IQ3_XXS, both prompts, --spec 2 and 4, the window prompt path).
Not the default: M5 Max, --spec 2, 3 interleaved pairs, off -> on: Q2_0 113.35 -> 113.24 / 106.98 -> 106.75,
IQ3_XXS 103.53 -> 102.71 / 96.18 -> 95.88 tok/s. The in-place state write (36 x 3 MB) costs every window; the
fold it saves is only the rounds with no accepted draft (24-36% at --spec 2).
- gdn.metal: with commit = 0x100 | k (1 <= k <= GDN_NTOK) the state is written in place and the conv history to
hist_out right after token k - 1, while every token's y is still computed; commit = 1 / 0 unchanged (the full
commit now writes at the last token inside the loop - same values, same speed)
- gdn_parity: the partial commits are bitwise a k-token commit's state and history, and y bitwise the verify window's
(k = 1 .. ntok - 1, ntok 2..4); gr_parity GR_DOWN=SG_KS picks gr_down's variant (sweep: 10.1-11.8 us at T 2, none
better than 8_8 by more than 7%)
Once, after the 20th cached window of T tokens with the head: the window's dispatches grouped by pipeline and weight
role, each group run by itself (7 runs, median). Profiling only: the reruns move the state, so the text after it
is not meant to be read.
Owner decision. Only the Options default under STRATA_GPU_METAL; CUDA/HIP/SYCL keep 0: on P2003 the CUDA
generate.cpp translation unit preprocesses IDENTICAL and its object is byte-IDENTICAL.
M5 Max, MTP, 200 tokens, greedy output equal to no MTP in every run: Q2_0 114.0 / 107.9 tok/s (prompt1 / story,
--suffix-draft 0; was 112.0 / 86.2 at --spec 4), 119.5 / 108.2 with the default suffix drafter; IQ3_XXS 103.5 /
96.9, 108.9 / 97.0 (was 110.2 / 87.4).
The GDN step writes the prepared y planes (no xprep dispatch per GDN layer); a window whose context is within the
2,051-cell budget skips the QSA score and top-k (qids_ holds the identity, restored after a selecting window; the
cached encode is keyed by it). STRATA_METAL_FUSE=0: off.
M5 Max, Q2_0, 200 tokens, 3 interleaved pairs: no MTP 85.1 -> 87.4 (prompt1) and 85.2 -> 87.3 (story); MTP 110.7 ->
112.0 and 85.3 -> 86.2 tok/s; tokens identical on and off and equal to the no-MTP reference.
prompt_chain.inc: tests/prompt_chain.cpp's chain (6f6c555b) into the decode window's own state - fp16 GEMMs on the
matrix units, grouped expert GEMMs, the GDN prompt scan, QSA K/V and indexer keys into the window's caches - plus
the PLE block after layer 0, every row's final residual for the drafter (its prompt rows in one command buffer),
the history parity, RoPE table and pending fold. Chunks of 2048 rows (STRATA_METAL_PROMPT_CHUNK); COOP_K4 picks
the register-operand kernels on the M5 and the older ones elsewhere. The 8-row-window path stays as the fallback
(STRATA_METAL_PROMPT=window, and prompts under STRATA_METAL_PROMPT_MIN = 32 rows).
M5 Max: Q2_0 4K/32K prompts 1,434/1,462 tok/s (window path 190/174), IQ3_XXS 1,260/1,240. Teacher-forced KL of
the chain's prompt vs the window path's, 28 prompts: 0.0004 (top-1 99.4-99.6%); needles 4K-32K x 10/50/90: the
same answers as the window path (12/12, both models).
- gdn_prompt.metal: gdn_cprep (per head and chunk: gates in log space, A, forward substitution for U and W),
gdn_cstate (per head and 32 state columns: the chunks in order), gdn_cout (the intra-chunk output)
- gdn_prompt_parity: CB -100 = the chunked form; y 3e-7, state 2e-7 vs the decode step; 7.4 ms vs 1.9 ms at T 2048
(cprep 1.5, cstate 4.7, cout 1.4) - it needs the matrix units to pay off
- moe_mm.metal: the paired-run (k, k + 16) decode is taken on the COOP_K4 layout unconditionally (coop_probe now
checks that pairing on both tiles used); the runtime per-element check cost ~30%. The MMA-only ceiling of mm_gu3
(MM_PROBE=1: no decode) is 2.45 ms a layer = 55 TFLOP/s useful, so the decode is what is left. Measured and not
kept: fp16-math Q2_0 decode (slower), a W8<Q2_0> sharing the block scale (slower than two W4)
- glue.metal slc_touch + token_chain CHAIN_PREFETCH: the next HC read's weights touched concurrently with the first
projection GEMV after a read; measured +0.83 ms a token (10.46 -> 11.29, 3 of 3 interleaved) - not kept, opt-in
A prompt of 64+ tokens runs in chunks (STRATA_METAL_PROMPT_CHUNK, default 1024 streaming / 2048 resident) through
mac-kern's prompt kernels (fp16 GEMMs, the GDN scan, QSA over the chunk, grouped expert GEMMs v2 on the M4, v3 on the
M5), the PLE block for T rows, on the decode window's GDN / K/V / PLE state; the draft layer's prompt K/V read the
chunk's final rows. Streaming: the expert GEMMs address arena slots (function constant 20 in moe_mm.metal too), a
chunk's handshake puts the layer's experts in place and streams the next layer's non-resident experts into the LRU
tail behind the compute (the decode-hot experts stay); slots in use by the running layer are pinned. The host
code's own device weight copies are given back on a streaming Mac (purge_device_allocations).
STRATA_METAL_PROMPT_GEMM=0 / _MIN, STRATA_METAL_PROMPT_EXPERTS=v2|v3, STRATA_METAL_PROMPT_QSA=v1|flash.
- qsa_attn_mm: the P V matmul in K = 32 cell runs (K = 64 runs computed garbage on the M4), the output through
threadgroup memory (a direct matmul2d store into 258-float rows lost the threadgroup on the M4); also faster on the
M5 (8.1 -> 7.4 ms at pos 0)
- qsa_flash: the same K = 32 runs (was a hang on the M4) and a fresh per-chunk accumulator folded into the running
one (on the M4 a run does not accumulate into elements rescaled in place); now about equal to attn_mm + merge on
the M5 (9.4 vs 9.5 ms at pos 0, 20.1 vs 20.3 at 30000) and the prompt_chain default there (PC_QSA=flash|mm|v1);
the M4 defaults to v1 attention (faster there) with the GEMM score
- qsa_attn_mm bisection probe (function constant 8, QSA_STOP), qsa_prompt_parity QSA_DEBUG / pipeline report;
coop_layout prints the destination layout too
The dense weights are read into one resident buffer; the routed experts live in an arena of slots filled from the
GGUF with F_NOCACHE preads (several pieces of an expert in flight); the window runs in 48 segments with a shared-event
handshake after each layer's router (mtl_rt run_graph_seg), where the host puts the missing experts in place (LRU)
and reads ahead the experts the next layer's router predicts from this layer's FFN input. The expert kernels address
slots through smap (function constant 20); the resident path is unchanged. STRATA_METAL_STREAM=auto|1|0.
- moe_mm.metal mm_gu3/mm_down3 and qsa_prompt.metal qsa_attn_mm/qsa_flash: with COOP_K4 = false every element is
decoded on its own (the M4 holds runs of 2 n at one k); the experts are correct on the M4 that way
- tests/coop_probe.h: probes the layout (COOP_K4=0/1 overrides); moe_mm_parity, qsa_prompt_parity, prompt_chain
set the constant; prompt_chain defaults to experts v2 and QSA v1 without it (faster / correct on the M4)
- qsa_prompt_parity: NaN-fills the partials before the matrix-unit run (a kernel that writes nothing shows),
line-buffered output, QSA_NOFLASH
STRATA_METAL_LOGPOS=<path>: per decode position of one-token windows, the log-probability of the token that came
next (with --spec-follow, the forced text) and the top 64, in phaseA-tests cuda_kl.py's row format.
STRATA_METAL_PROFILE=1: per window size, host time before the GPU, run_graph wall vs GPU time, commit and draft
round times, printed when the window goes away. Both off unless set.
commit(n) after a window of several tokens no longer submits its own command buffer: the fold of the kept prefix is
encoded first in the next window's (cached per pending count). Nothing reads the GDN state in between.
STRATA_METAL_LAZY_FOLD=0 folds at commit as before.
M5 Max, Q2_0, MTP --spec 4, 200 tokens, 3 interleaved pairs: commit 0.87 -> 0.05 ms a round; prompt1 109.25 ->
110.21 tok/s, story 83.99 -> 85.14 (medians); greedy output equal to no MTP, lazy and not.
0.1.42 let --batch-mtp run on a layer split and made the --batch-groups auto default give way to it: one serial group. On
2x RTX A4000 (IQ3_XXS, 3 pairs) four streams aggregated 62.2 tok/s against 92.7 on 0.1.41's pipelined groups (-33%, 0/3 pairs
faster), two streams 55.7 against 71.4 (-22%). With --batch-groups 2 (which turns --batch-mtp off) 88.3 and 71.1. The start-up
line says why --batch-mtp is off and how to keep it. --adapt-async 1 still gives one group. Solo decode and the answers are
untouched.
- qsa_prompt.metal: qsa_score_mm (fp32 MPP, relu-sum epilogue, the tail block scalar; 6.2 -> 1.1 ms at pos 30000,
T 2048), qsa_flash (whole attention a threadgroup with an online softmax, fp16 out; correct but 27.6 ms vs 20.9 for
attn_mm + merge - not used)
- moe_mm.metal: W8<F> decodes a lane's two runs of a 32-group with one read of the group's scale/sign fields;
IQ3_XXS gate/up 9.3 -> 6.7 ms a layer
- prompt_chain: PC_QSA=mm uses qsa_score_mm too; gate/up 16,32,32,4 for every format; breakdown sees the far
chunk's classes
msl/mtp_chain.metal: mtp_pick (the pick through the draft vocabulary and its softmax probability) and mtp_embed_F
(the picked token's embedding row through mac-kern's unit decoders), so every draft step of a round runs in one
command buffer. At load the GPU embedding is checked against the host's dequant (max |diff| 0 on Q3_K and IQ3_S;
any difference falls back to a command buffer per step). STRATA_METAL_MTP_CHAIN=0 keeps the per-step path.
Fix: with accepted drafts (a > 0) draft 0's embedding overwrote the catch-up's row 0 before the GPU read it; draft
0's row now goes to memb_ row a. The dense attention's cell list is per step.
M5 Max, 200 tokens, --spec 4, greedy output equal to no MTP in every run: Q2_0 109.5 / 84.4 tok/s (was 101.4 /
80.0), IQ3_XXS 107.9 / 86.0 (was 101.5 / 82.8).
- MTP (src/core/mtp.cpp hook under STRATA_GPU_METAL: load / bind / prefill / draft / draft_first): the draft layer
of mtp.hpp on mac-kern's kernels, from the MTP GGUF (tools/mtp_pack.py, beside --mtp's rt dir; the F32 norms
folded to 1 + w as tools/mtp_rt.py does): input branches (msl/mtp.metal: rms, strided xprep, stream combine),
the three HC reads, K/V and indexer keys through qsa_prep into its own cache, dense attention over the last
2,051 cells, the MoE (fused gate_up experts), the final mixer and the head over data/draft_vocab.bin's subset
(STRATA_METAL_DRAFT_VOCAB=full: the whole head). One command buffer per draft step (the next step's embedding
is dequantized on the host). Greedy output with MTP equals the output without it, token for token (Q2_0, two
prompts, BF16 and Q8_0 HC); acceptance equals the HIP engine's on the same prompts (124 of 228; 104 of 291 vs 285).
- Prefill (src/prefill/prefill.cpp hook): init brings up the decode window, run reads the prompt through windows
of 8 rows (no head) and feeds on_chunk (the MTP's K/V); draft_kv declines. Prompt windows of 8 rows give the
same tokens as one-token windows. generate.cpp's own Metal prompt loop is gone again (--prefill auto as on the
PC). One window per process (the prompt path may bring it up before the Verifier).
- msl/ple.metal: the PLE block's norms, gate, conv and residual update on the GPU (same tokens as the CPU version).
- The HC read in Q8_0 by default (the owner's decision; STRATA_HC_Q8=0: BF16): the down / up / inject matrices
quantized at load (290 matrices, 680 MB, 70 ms), read by gr_down_q8 / gr_up_q8.
- The encoded window is cached per (T, head, history parity) and only its positions are patched per window.
- f-mac-kern merged up to 4775d233.
M5 Max, Q2_0, greedy, 200 tokens: Q8_0 HC 85 tok/s without MTP; with MTP 101 tok/s (acceptance 0.54) and 79 tok/s
(acceptance 0.36, a prompt the drafter does badly on everywhere: HIP 71 tok/s there).
- moe_mm.metal: mm_gu3 / mm_down3 - weights decoded per lane straight into matmul2d's right-input cooperative tensor
(single-simdgroup scope, no threadgroup staging or barriers in the K loop); W4<F> 4-value decoders for Q2_0,
IQ3_XXS, IQ2_XXS, IQ2_XS, IQ2_S, IQ3_S, IQ4_NL (quant.metal's deq arithmetic)
- qsa_prompt.metal: qsa_attn_mm (S = Q K^T and P V as single-simdgroup matmuls, int8 K/V decoded into the right
input; same partials as qsa_attn, so qsa_merge is unchanged), qsa_q_f16
- tests: qsa_prompt_parity, coop_layout (the cooperative tensors' element layout), moe_mm_parity --v3;
prompt_chain PC_GU / PC_DN / PC_QSA switches (defaults v3 / v3 / mm)
#1240 stopped creating the drafter's branch stream and its two events on HIP: nothing uses them there, and an R9700
lost prompt speed with them. On gfx103x the stream's existence alone makes prompts read faster. RX 6800, Windows,
STRATA_HIP_PROMPT_F16=1, IQ3_XXS, two runs each: a 7.9K-token prompt at 798-808 tok/s with the stream and 584-596
without, a 30.4K-token one at 855-863 against 553-600; all of it is in the prompt path's "wait copy" phase. The two
events change nothing, so they stay out.
gfx103x creates the stream; other HIP cards do not, as before. STRATA_MTP_SIDE_STREAM=1 / =0 forces either. CUDA is
unchanged. Measured on one card under Windows only.
- token_chain: the HC read defaults to the Q8_0 copies through the 3-dispatch read (q8_3); STRATA_HC_Q8=0 selects
the exact BF16 path (--hc still overrides)
- prompt.metal: route_topk, gr_epi_down/up, swiglu_h, moe_out, gr_norm_h (fp16 xn), gemm_hc_up (the HC up GEMM with
the mixing epilogue; w_up rows interleaved by dq_f16's new perm field)
- gdn_prompt.metal: gdn_pre (parallel conv/norms/gates) -> gdn_scan_r (state scan, one barrier a token) -> gdn_post
- dense_mm.metal: DqArgs gains `perm` (4th field)
- moe_mm.metal: mm_scan zero-fills the unused simdgroup slots; indirect grids for gather/gate-up/down in meta;
MM_PROBE bench constant (function constant 5: pipelines of mm_gu2 need it set)
- tests: prompt_chain (whole prompt in chunks, class breakdown), prompt_parity, gdn_prompt_parity; mm_bench tiles
- window.cpp: a window of 2..8 tokens runs the GDN steps without committing; the projections it feeds them are
kept per layer, and commit(n) folds the kept prefix into the state (gdn_step over n tokens with commit, the same
kernel: bitwise the window's trajectory) and turns the conv-history ping-pong. K/V and indexer cells past the
prefix are rewritten when those positions run again, before anything reads them; the PLE history keeps the n
rows. Checked: greedy output with suffix drafts (windows up to 4, 50 of 59 drafts kept) is the same token for
token as one-token windows.
- generate.cpp (Metal only): --mmap-experts by default (the GGUF shards in place; the window reads them itself):
the 31.6 GB expert arena is not loaded any more (RSS 74 -> 40 GB, the rest is the mapped shard)
- stubs.cpp regenerated (256 entry points left as stubs)
IQ3_XXS vs the CUDA engine (P1004, f-mac, same flags, --kv int8): 200 of 200 tokens equal on the capital
prompt, 124 on a story; following CUDA's tokens 3 of 200 picks differ, at margins 0.07-0.23 (near ties).
The Metal build of `strata` is the shared host code (src/, unchanged except preprocessor-only Metal/arm64 branches)
over two Metal-only parts in metal/src/engine:
- window.cpp: the decode window, the one backend hook (Verifier::init/run/commit/copy_logits under
STRATA_GPU_METAL). It is mac-kern's chain on the model GGUF itself (shard 1 mmapped and wrapped no-copy, every
weight read in place: no upload, no expert cache): HC reads, GDN/QSA mixers with their own state (recurrence,
conv history ping-pong, int8 K/V, indexer), router + plan, routed and shared experts, combine, the final mixer,
the LM head and the greedy pick, encoded as one dependency graph. The embedding is dequantized on the CPU into
the shared buffer; the PLE rows come from the shared table reader, their projections run on the GPU and the
block's norms / gate / conv on the CPU between two command buffers. One-token windows (--mtp-max-t 1) for now.
- host helpers copied from the CUDA kernel files the host code reads while it starts (format tables, sizes, path
switches, the RoPE table/config); every other kernel entry point is a generated stub that stops with its name
(stubs.cpp, tools/gen_stubs.py, from clang's AST of our headers).
Shared host code, all inside #if STRATA_GPU_METAL / __APPLE__ / __aarch64__ (the PC objects do not change):
- verify.hpp/.cpp: the hook (an opaque window pointer; init/run/commit/wait_commit/copy_logits hand over)
- generate.cpp: no x86 ISA refusal; a native pack without --prefill reads its prompt through one-token windows;
the model shard handed to the window
- device.cu: the poison fill through the allocation's CPU view; pool_affinity_linux.hpp: a macOS branch (no
pinning); expert_layout.cpp: no xgetbv asm on arm64
metal/include/metal_compat: cpuid.h (all zero on arm64), linux/mman.h (no hugetlb/THP: the fallback taken).
M5 Max, Q2_0, greedy, "What is the capital of France?": "The capital of France is Paris." - the same 36 first
tokens as the HIP engine (R9700, same flags) on that prompt and 39 on a story prompt, then near-tie flips
(margins 0.08-0.41 when following the HIP tokens); decode 74-75 tok/s.
- kernels/mm_bench.metal + tests/mm_bench: M5 Max MetalPerformancePrimitives matmul2d fp16/bf16 62-65 TFLOP/s
(64x64 tiles), fp32 15, simdgroup_matrix half 14.
- kernels/moe_mm.metal: deterministic GPU plan (block histograms, scan, stable placement; padded per-expert tiles),
activation gather to fp16, mm_gu2 (gate+up+SwiGLU) / mm_down2 with weights dequantized (ref_quant arithmetic)
into threadgroup memory and matmul2d in multiply_accumulate mode, mm_combine. tests/moe_mm_parity: Q2_0 and the
IQ3_XXS-pack formats vs a reference with the same fp16 operands (1e-6..4e-5; vs exact float 2e-4..5e-4).
T=2048, 512 experts, per layer: Q2_0 8.0 ms = 25 TFLOP/s = 3.9 us a token (~5,300 tok/s experts only);
IQ3_XXS gate/up + IQ4_NL down 14.5 ms. The tile idea (dequantized weights from threadgroup memory under matmul2d)
follows yibie/strata-mlx's tile_matmul (MIT); the code is ours.
- kernels/dense_mm.metal: dq_f16 (any dense format to fp16 once a chunk), act_f16, gemm_f16 (MPP): 64-65 TFLOP/s
on the GDN/QSA shapes for 2048 tokens, dequantization ~4% of it; tests/dense_mm_parity (12 formats).
metal/CMakeLists.txt holds both: mac-kern's runtime face, kernels and parity programs as they are, and the device
runtime + the engine build (taken from ../src through include/metal_compat) when METAL_CPP_DIR is given.
runtime_hello is kept out of the kernel tree's test glob.
- gr.metal: gr_down_f<SG, KS, F> / gr_up_f<F> for BF16 (the old names) and Q8_0 (gr_down_q8_*, gr_up_q8);
tests/gr_parity GR_Q8=1: vs double on the dequantized Q8_0 weights, 1/3/8 tokens, M5 Max and M4 pass.
- token_chain --hc q8_3. M5 Max, --fuse --graph, interleaved, 2 rounds: Q2_0 1 token 11.62 (BF16) / 10.59 (Q8_0,
2 dispatches) / 10.38 ms (Q8_0, 3); 3 tokens 16.78 / 17.21 / 16.24-16.31. IQ3_XXS 1 token 12.85 / 11.87 / 11.68;
3 tokens 17.88 / 18.59 / 17.47-17.51. The 2-dispatch read's gain was all Q8_0; its kernels are slower than the
3-dispatch ones at windows. M4 HC read: Q8_0 85 us vs BF16 141-144 us.
- gemv.metal: gemv_pair<F1, F2> for the packs' gate/up format pairs (tests/pair_parity: 8 pairs x 1/3/8 tokens vs
double, 1.4e-7..2.6e-7) and GV_SET(T_Q2_0R).
- token_chain CHAIN_PAIR=1: interleaved A/B, 3 pairs, Q8_0 HC --fuse --graph: 10.60-10.61 ms without vs 10.71-10.72
with (3 tokens 17.23-17.25 vs 17.31-17.33): two concurrent GEMVs and the SwiGLU win; opt-in only.
kernels/gridbar.metal + tests/gridbar_bench: phases of a streaming read separated by an atomic grid barrier vs by
dispatch boundaries (M5 Max). Without atomic_thread_fence(device, seq_cst) the phases read other threadgroups'
stale writes; with it 0 stale reads. Speed: 4.5 MiB phases 10.3 us (10 threadgroups) vs 9.9 us a dispatch, 30-34 us
with 20-40 threadgroups; 0.5 MiB phases 1.5 vs 2.2 us. Not pursued.
All under __APPLE__ / STRATA_GPU_METAL, so the Linux and Windows code is unchanged:
- direct_file.cpp: no O_DIRECT on macOS; F_NOCACHE after open keeps the reads out of the buffer cache
- memory.cpp: F_RDADVISE is the read-ahead hint where posix_fadvise(WILLNEED) is used
- expert_source.cpp: available memory from host_statistics64 (free + inactive + purgeable); the posix_fadvise of a
released expert layer is skipped (the madvise before it already released the mapping)
- generate.cpp: <sys/sysmacros.h> is not on macOS (major/minor come from <sys/types.h>); the CUDA toolkit mismatch
check is CUDA's
- vmm.cpp: the cuMem* virtual memory stays CUDA's (Metal takes the HIP branch: elastic K/V off)
- prefill.cpp: std::max<size_t> where size_t and uint64_t meet (the same type on Linux, distinct on macOS)
- gdn.metal: gdn_step_t<IB>; gdn_step8 (1024 threads, 16 state rows a thread) passes and is not faster than
gdn_step (16.3 vs 16.4 us commit, n=1): the state write after the compute is what a commit adds.
- token_chain: CHAIN_GU / CHAIN_DN / CHAIN_DN4 select the routed-expert tiles; measured in the chain (Q8_0 HC,
--fuse, --graph): gate/up 32,1,4 stays; the Q2_0 down 8,1,4 over 8,2,4 (10.63 vs 10.67 ms, 3 tokens 17.23 vs
17.50 ms), now the default.
- kernels/qsa2.metal: qsa_topk2 - the same selection (ids exact at contexts 700..33000): the radix digit found by a
parallel suffix scan of the 256 bins, cells emitted from contiguous block ranges after one scan. 23 -> 8.2 us at
8K, 54 -> 14.5 us at 32K; token_chain (Q8_0 HC, --fuse, --graph, interleaved): 8K 11.40 -> 11.00 ms,
32K 11.90 -> 11.27 ms.
- qsa_attn2 (chunks of 32, head groups) and qsa_attn3 (lane per cell for q.k): both pass, neither beats qsa_attn at
the full budget (21-24 us, nearly independent of the cell count: a latency chain); QSA_ATTN selects, default 1.
- Read yibie/strata-mlx's OPTIMIZATION_RECORD/ROADMAP (MIT) before this round: its finding that merging kernels cut
the kernel count 43% for 15% of the time matches ours.
kernels/mma.metal: 8-row tiles dequantized with ref_quant's arithmetic into threadgroup memory, x loaded transposed
from device memory, simdgroup_multiply_accumulate; tests/mma_parity: 12 formats pass vs double, every token of a
window bitwise a one-token call. Speed (M5 Max): 60-190 GB/s for 1, 3 and 8 tokens alike, against the per-lane
GEMV's 400-595 GB/s at 1 token and 70-230 at 8: the dequantization through threadgroup memory (~8 B a weight) is
the limit. Only Q2_0 at 8 tokens is faster (115 vs 126 us). Not used by token_chain.
- gdn.metal: GdnArgs.plane: the step writes y as the raw plane plus the other planes and the per-16 sums, so the
out projection needs no prep dispatch.
- gr2.metal: gr_down3 can form bo as the MoE combine itself (GrArgs3.comb).
- token_chain --fuse: the GDN prep in the step, no QSA score/top-k dispatch while the context fits the 2051 budget.
Q8_0 HC, --graph: 10.71 -> 10.49 ms (1 token), 17.13 -> 16.86 ms (3); BF16 2-dispatch 11.91 -> 11.65 ms.
--fold-comb (the combine in the next HC read) is slower: 10.49 -> 11.22 ms; off.
- kernels/gr2.metal: gr_down3 (grid row blocks x streams: each threadgroup forms its stream's R', norm and xn in
threadgroup memory, writes per-stream partial dots; R' ping-pong, never in place) and gr_up3 (lo from the
partials, xn on the fly); BF16 or Q8_0 weights. tests/gr2_parity: both formats vs double on their own weights,
prepared planes bitwise = xprep; Q8_0 moves the read's output by 1.2e-3 relative.
- ref_quant: quantize_q8_0 (ggml's reference rounding), used for the load-time Q8_0 copies (STRATA_HC_Q8's form).
- token_chain --hc 3|2|q8. M5 Max, Q2_0, --graph: 1 token 12.00 / 11.91 / 10.71 ms; 3 tokens 17.21 / 18.02 /
17.13 ms.
MAC-PLAN Phase 1, compat design (the alternative to renaming the host calls, f-mac-design S1-S4).
metal/include/metal_compat goes first on the Metal build's include path, as include/strata/hip_compat does on HIP:
its cuda_runtime.h gives the ~70 CUDA runtime names of the Phase 1 inventory (types, constants, calls) on Metal,
each forwarding to metal/src/gpu/runtime.cpp (metal-cpp: gpuAddress device pointers in one residency set, streams =
queue + command buffer + shared-event timeline, graphs = recorded dispatch lists), and immintrin.h gives _mm_pause /
_mm_sfence on arm64. The CUDA / HIP / SYCL builds never see metal/.
Shared files: four preprocessor conditions also exclude STRATA_GPU_METAL from CUDA-only blocks (the cuMem*
segmented expert cache, the STRATA_VERIFY_NODES graph listing, the MTP stream priority, cudaInitDevice of the remote
experts); nothing else in src/ changes, so the PC objects are byte-identical.
On the M5 Max: runtime_hello passes with the unchanged src/core/graph.cpp, and graph, expert_cache, weights,
native_head, native_dense, layer, verify, mtp and session compile for Metal as they are (-fsyntax-only); prefill,
expert_source and generate stop only at macOS platform items (std::max of size_t vs uint64_t, posix_fadvise /
_SC_AVPHYS_PAGES, sys/sysmacros.h).
- gdn.metal: a q/k conv channel is read at the start of three threadgroups (value heads h, h + 16, h + 32) while
its owner wrote it back in place at its end: on the 10-core M4 the threadgroups run in waves and the later ones
read the new history (gdn_parity failed there, passed on the M5 Max where all 48 start together). The committed
history now goes to hist_out (the caller swaps); gdn_parity checks determinism too. Passes on both Macs.
- gemv_parity --gguf: one real tensor per format of a GGUF (experts: expert 0) vs double; Q2_0 file: 11 formats
pass, rel L2 8e-8..2.7e-7.
- src/gguf_lite: a minimal GGUF v3 reader; the file is mmapped and wrapped no-copy as one MTLBuffer.
- moe.metal addresses experts by gate/up/down offsets and strides (a pack blob or the GGUF's three tensors).
- kernels/glue.metal: the shared expert's SwiGLU (prepared activations) and the combine with its sigmoid gate.
- tests/token_chain: every layer of the model with its own formats, all kernels in one command buffer.
M5 Max, Q2_0, context 1000: 12.83 ms a token (serial encoder), 12.01 ms with --graph (a concurrent encoder,
barriers only at true dependencies); a 3-token window 18.42 / 17.23 ms. Bound: 7.27 ms.
- mtl_rt: run_graph (concurrent encoder, Dispatch::barrier).
- kernels/qsa.metal: q/k RMSNorm + NEOX RoPE (host table), int8 KV append (fp16 scale per 64), the indexer's raw
tail / pooled block keys / dead key, ReLU-sum block scores with the +1e9 tail, a cell-level radix top-2051
(ties to the lowest cell, ascending), split-K GQA attention over the selection, log-sum-exp merge * sigmoid(gate)
written as prepared activations; tests/qsa_parity stage by stage (ids exact; contexts 700..9001).
- kernels/sample.metal: two-stage argmax, ties to the lowest id; tests/sample_parity.
kernels/gdn.metal: one threadgroup per value head, the head's state in registers across the window's tokens;
commit writes state + conv history back, verify does not; tests/gdn_parity vs double (1.7e-7), verify leaves the
state untouched, the first m outputs of a longer window are bitwise a window of m (the fold).
- kernels/gr.metal: gr_norm (folds the previous write, per-stream RMSNorm), gr_down (down + inject rows, silu/hc
epilogue, K split), gr_up (gated mean over the streams, writes the prepared activations); tests/gr_parity vs double.
- moe.metal: moe_route = native_router.cu's softmax/top-10/renormalisation and the plan in one threadgroup
(tests/route_parity: ids exact incl. ties, weights 2e-7); moe_gu/moe_down switch once per threadgroup into a body
specialised for the group's entry count (the predicated 8-way loop cost 2x); Q2_0 down reads raw activations
(T_Q2_0R) so no prep dispatch is needed.
- mtl_rt: warm() before timings (the GPU starts at idle clocks).
- kernels/units.metal: per-format unit decoders (Q2_0, Q3_K..Q6_K with the mask trick, IQ4 via a byte->half2
codebook table, BF16, generic for the rest) and one combine for all; xprep writes the pre-scaled activations
and per-16 sums once per input vector.
- kernels/gemv.metal: y = W x for 1..8 tokens in one pass over the weights, NTOK a function constant, optional
K split across simdgroups; tests/gemv_parity: parity vs double for 12 formats x 20 tile shapes, every token of
a window bitwise equal to a one-token call, and the bench on the packs' projection shapes.
- moe.metal on the same decoders; mtl_rt compiles with safe math by default (no reassociation; measured free).
- tests/alu_bench: instruction rates (fp32/fp16 FMA, int, convert, threadgroup/device/constant gathers).
The dpct migration had dropped the cudaMemcpyAsync D2H and the stream sync, so the resident RAM mode with adaptive swaps committed
whatever the exchange buffer held as the evicted expert (lost expert, wrong weights on a later CPU read). STRATA_CHECK_SWAPS=1 is a
test aid: it compares each staged buffer with the model file and audits the RAM copy at the end of a run.
metal/ is its own CMake project (macOS only), nothing in src/ or include/ changes. MSL is compiled at run time
(the Command Line Tools have no offline metal compiler) with ggml-common.h prepended for the i-quant grids.
- src/mtl_rt.*: a C++ face over Metal (buffers, pipelines with function constants, serial/concurrent encoders,
indirect dispatch, indirect command buffers, GPU timing).
- src/ref_quant.*: CPU reference dequantizers for Q2_0, Q4_0, Q5_0, Q8_0, Q3_K..Q6_K, IQ2_XXS/XS/S, IQ3_XXS/S,
IQ4_NL/XS, BF16, F16; checked bitwise against gguf-py (tests/ref_dump + tools/check_ref.py: 17/17 equal).
- kernels/quant.metal: the formats as per-lane units (decode once, dot with every token of the window).
- kernels/moe.metal: moe_plan (top-k -> groups by expert, deterministic, writes the indirect grids), moe_gu
(gate+up+SwiGLU, grouped), moe_down, moe_combine; tests/expert_parity: parity vs double and the bench.
- tests/rt_bench: M5 Max streaming read ~585 GB/s, ~0.85 us per dependent dispatch, ~170 us commit+wait.
Linux CUDA had it on by default since 0.1.43, measured on an RTX A4000 (+5.5..8.3% decode) and a Tesla P100 (+4.4%). An RTX 5090 Laptop with a native IQ3_XXS pack and --expert-cache-per-layer decodes 21% slower with it, and the overlap is the default only there. The switch is now =1 turns it on everywhere, and the startup log names the state (with the reason when it cannot run), so a decode-rate drop is not the only way to see it.
Unchanged: the copy path, the gate predicates and the tokens (same tokens with or without it).
Fixes#2010
On 2x/4x R9700 the drafter's chain and the last stage's window shared a card and slowed each other (window 13 -> 27 ms) in most engine starts; the draft layer now loads on the first card (STRATA_MTP_STAGE0=0 keeps the old place). Exact flags, tokens identical: 2 cards code +21%, 3 cards +19%, 4 cards +22% over the serial loop. A split reads prompts in 8192 chunks (STRATA_SPLIT_CHUNK16K=1 keeps 16384): 16K prompt +36..53%, 32K +18..29%, same tokens. STRATA_PCIE_AUTO_SPLIT=1 now also runs in the pipelined loop (opt-in).
gfx1103 was missing from the gfx11 matrix-core lists, so the 780M ran the portable verify-window and prompt kernels. Added to the
compile-time lists and gfx_arch_is_gfx11_wmma(). Radeon 780M, Q2_0, 5 interleaved pairs: decode (code) 11.96 -> 18.25 tok/s,
same token ids; prompts unchanged. hip_prefill_wmma_gemm_parity passes on gfx1103 (3520 cases). Identity gate (defaults off,
mg_norepeat 10/10 against the portable build, exact_pp 4K/20K residual dumps identical).
The serve path counts every routed expert on the device when a save path and a mirror exist; the saved ranking mixes the counts with the start ranking (prior worth 800k routed entries, STRATA_PROFILE_PRIOR_ENTRIES). Exact: placement is fixed under NO_HOST, the file applies at the next start. B70, IQ3_S: code +13..19% vs the xe default profile, prose -18%.
0.1.43 moved the QSA step records and the batched token ids to a pinned buffer (#1712). A 4-hour multi-slot load on 2x RX 7900 XTX (parallel 4, vision plus long
prompts, layer split) still faulted after 31 minutes, with the picture rows, the draft tokens and bounds and the non-mapped routing tables still going up from
caller memory. PinnedRing (include/strata/core/pinned_ring.hpp) is a ring of four pinned slots with one event each: the bytes are copied into the next slot after
the copy that last read it is done, the DMA runs from the slot, and the caller may change its source on return. No pinned RAM (or STRATA_TEST_PAGEABLE=1): the
copy is made from the caller's memory and the stream is waited for. A failed upload is returned and reported.
Moved onto it: the picture's embedding rows (gathered per run into one slot), the draft K/V step's tokens and bounds, the PLE rows' pageable fallback, the
non-mapped branch's routed-id download and slot/src/bounds uploads, the peer device's src and bounds tables, and the drafter's prompt records (mtp.cpp).
Copies from pinned arenas and cudaHostAlloc buffers are unchanged. Same bytes reach the GPU, so answers are identical.
The analysis of which copies are still pageable, and the bounce-slot-per-event idea, are mantovaniluca91's (patch C on #1712). This is our own form of it.
collect_machine listed every nvidia-smi card. It now takes the bench's --gpu, else the config's gpu (or --gpu in its args); with none given it names the one card the server reports instead of all of them.
ensure_loaded logged 'loading anyway' for a before_load command that failed and co-loaded two models onto one GPU. A non-zero exit or a timeout now leaves the model unloaded and answers 503 with Retry-After (before_load_ignore_failure restores the old behaviour); before_load_timeout_s sets the 120 s limit; POST /unload?wait=1 (or wait=<s>) waits for the running turn instead of answering busy.
The engine answered 'ERR BGEN: no such free slot' when its view of a slot differed from the server's; the server turned the ERR into an HTTP 400 and the conversation, drawn to the same slot by its held prefix, got it again on every turn. Now the refused slot is parked (busy, no owner) for SLOT_PARK_S, the request waits for / takes another slot, and after BGEN_REFUSALS_MAX refusals it ends with a retryable 503.
The host mirror and the unmirrored count cover the first stage only; a later stage whose cache lacks experts has no mirror entry and, under NO_HOST, no host answers for them (device lost at the first verify window on 2x Arc Pro B60). The start now refuses with the existing wording, or logs the count when NO_HOST is off.
The mapped gather_batch took 16 serial demand faults per token from a cold table with no progress mark; the first chunk of a prompt only has layer 0 to hide it behind, so the stall watchdog stopped the engine. Rows are now hinted 4096 at a time (PrefetchVirtualMemory / MADV_WILLNEED). Answers are unchanged. The 0.1.42 auto-switch to mmap on Windows is opt-in until it is measured there.
The per-layer host function made the overlap depend on the driver's callback thread being scheduled: with the pool's
workers spinning on every other CPU it still cost 7 ms per window on a 20-core desktop even with the thread on all CPUs
(RTX 3060, Core Ultra 7 265: 46.5 tok/s with the warmed callbacks against 56.5 with the overlap off). The copy stream now
ends each layer's copies with a 4-byte device-to-host copy of the layer number into a pinned int the gate polls; same stream
order, no host thread. STRATA_ADAPT_SIGNAL=host brings the host functions back. Swaps, residency and answers are unchanged.
On Linux the CUDA (and HIP) runtime creates its host-callback thread lazily, at the first cudaLaunchHostFunc, and a new
thread inherits the creator's CPU mask. The decoding host pins itself to one CPU (SessionLoopScratch::init), so a thread
created after that - which the adaptive overlap's per-layer callbacks did - ran on the host's CPU and fought the host's
spin-wait: ~1000 us instead of ~4 us per callback, decode 2-4x slower with STRATA_ADAPT_OVERLAP on (#1964, #1975).
oal's warm-up (one no-op callback + sync before ExpertPool) is now unconditional on Linux; the answers, swaps and
residency are untouched. Windows CPU Sets are per thread and not inherited, so Windows is unchanged.
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).
description:Share measurements from your machine, or report a problem with the benchmark tools
title:"[Benchmark] "
labels:["benchmark"]
body:
- type:markdown
value:|
The title must start with **[Benchmark]**. Keep it, and add the GPU and model after it.
To share a full report, follow the [community benchmark guide](https://github.com/Niko1221/Strata/blob/main/docs/COMMUNITY_BENCHMARKS.md) and open a results-only pull request. The standard protocol is a [draft](https://github.com/Niko1221/Strata/blob/main/docs/BENCHMARK_PROTOCOL.md); existing tools do not implement it yet. Include poor results and failed requests.
- type:textarea
id:what
attributes:
label:What did you measure, or what went wrong
description:Hardware (GPU and VRAM, CPU, RAM, OS), Strata version, model and quantization, the exact command, and the numbers. Paste the report or link the pull request.
description:Something in Strata doesn't work as documented
title:"[Bug]: "
title:"[Area] "
labels:["bug"]
body:
- type:markdown
value:|
Thanks for the report.
Thanks for the report. Please read [CONTRIBUTING.md](https://github.com/Niko1221/Strata/blob/main/CONTRIBUTING.md) first.
Please search the [open issues](https://github.com/Niko1221/Strata/issues?q=is%3Aissue+state%3Aopen) first — this may already be tracked.
Search the [open and closed issues](https://github.com/Niko1221/Strata/issues?q=is%3Aissue) first: this may already be tracked, and new evidence belongs in that thread.
Write only what you saw. Do not guess at steps, errors, hardware or severity.
**The title must start with its area:** [Engine], [GUI], [TUI], [Benchmark], [Docs] or [Other]. Choose the same one below.
- type:dropdown
id:area
attributes:
label:Area
description:Start the issue title with the same area in square brackets, for example "[Engine] decode stops after 2 minutes". Pick Other if none fits.
options:
- "[Engine] (speed, kernels, models, the server and API)"
- "[GUI] (the desktop app)"
- "[TUI] (the Terminal: strata launch, connect, monitor)"
- "[Benchmark] (measurements and reports)"
- "[Docs] (documentation)"
- "[Other] (anything else, including setup and installation)"
validations:
required:true
- type:dropdown
id:how
attributes:
label:Desktop app, Terminal or source?
options:
- Desktop app (Windows, Linux or Mac)
- Terminal (the `strata` command)
- Source (START-HERE.bat, setup.sh or a build)
- Docker
- Other
validations:
required:true
- 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.
description:What you did, what you got, and what you expected instead. Paste the exact error text if there is one.
validations:
required:true
- type:textarea
id:reproduce
attributes:
label:Smallest steps or command that reproduce it
description:The shortest list of steps that shows the problem. If you cannot make it happen again, say so here.
validations:
required:false
- type:dropdown
id:reproduction-status
attributes:
label:Reproduction status
options:
- Reproduced locally
- Unconfirmed static analysis (I read code or ran a scanner; I did not see it fail)
validations:
required:true
- type:input
id:version
attributes:
label:Strata version
description:"In the app: Settings > About. In the Terminal: `strata --version`. From source: the About tab of the web page, or `engine` in `GET /v1/status`, or the git commit."
placeholder:"Strata 0.2.3 · engine 0.1.48"
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"
label:Graphics card, memory and system
description:The card and its VRAM, the RAM of the PC, and the operating system.
placeholder:"RTX 5070 12GB · 64 GB RAM · Windows 11"
validations:
required:true
- type:input
id:model
attributes:
label:Model, size and settings
description:The model and size you started, the context, and whether pictures were on. Write what is relevant.
description:Drag in `strata-<model>.log` from your Strata folder. Drag and drop works directly into this box.
label:Logs
description:|
The app: Settings > Engine > Diagnostics > View logs > Save logs, then drag the file in. The Terminal: paste the output of `strata doctor`. From source: drag in `strata-<model>.log` from your Strata folder.
Remove API keys, passwords and private text first.
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.
- Check the [troubleshooting page](https://github.com/Niko1221/Strata/blob/main/docs/TROUBLESHOOTING.md) first, many reports are already solved there.
description:Suggest something new or an improvement
title:"[Feature]: "
title:"[Area] "
labels:["enhancement"]
body:
- type:markdown
value:|
Explain what you want in plain language.
Explain what you want in plain language. For a large feature, a change to a default or a refactor, please agree the scope here first. Read [CONTRIBUTING.md](https://github.com/Niko1221/Strata/blob/main/CONTRIBUTING.md).
**The title must start with its area:** [Engine], [GUI], [TUI], [Benchmark], [Docs] or [Other]. Choose the same one below.
- type:dropdown
id:area
attributes:
label:Area
description:Start the issue title with the same area in square brackets, for example "[Engine] decode stops after 2 minutes". Pick Other if none fits.
options:
- "[Engine] (speed, kernels, models, the server and API)"
- "[GUI] (the desktop app)"
- "[TUI] (the Terminal: strata launch, connect, monitor)"
- "[Benchmark] (measurements and reports)"
- "[Docs] (documentation)"
- "[Other] (anything else, including setup and installation)"
validations:
required:true
- 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.
description:Describe the goal rather than the implementation. Say who needs it and why.
validations:
required:true
- type:textarea
id:workaround
attributes:
label:What do you do today instead?
description:The workaround you use now, if there is one.
validations:
required:false
- 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.
All of this counts: model and architecture support, the server and API, the app and the Terminal, multi-GPU and layer split, images and vision, MCP servers, setup and installation, and performance.
description:Ask how to use Strata, or share a result
title:"Question: "
title:"[Area] "
labels:["question"]
body:
- type:markdown
value:|
Not sure how to do something, or want to share numbers from your machine? Ask here.
Not sure how to do something, or want to share numbers from your machine? Ask here. To share a full benchmark, see [CONTRIBUTING.md](https://github.com/Niko1221/Strata/blob/main/CONTRIBUTING.md#share-a-benchmark).
**The title must start with its area:** [Engine], [GUI], [TUI], [Benchmark], [Docs] or [Other]. Choose the same one below.
- type:dropdown
id:area
attributes:
label:Area
description:Start the issue title with the same area in square brackets, for example "[Engine] decode stops after 2 minutes". Pick Other if none fits.
options:
- "[Engine] (speed, kernels, models, the server and API)"
- "[GUI] (the desktop app)"
- "[TUI] (the Terminal: strata launch, connect, monitor)"
- "[Benchmark] (measurements and reports)"
- "[Docs] (documentation)"
- "[Other] (anything else, including setup and installation)"
validations:
required:true
- type:textarea
id:question
attributes:
@@ -13,10 +29,17 @@ body:
description:What you're trying to do. Include your version, GPU, and OS if it affects the answer.
validations:
required:true
- type:textarea
id:tried
attributes:
label:What you tried
description:What you already did and where you got stuck.
validations:
required:false
- type:textarea
id:numbers
attributes:
label:Results (optional)
description:Only if you ran a benchmark or measured speed. Model, quant, context, and the tok/s you get.
<!-- If Applicable, reference the GitHub issue -->
Issue: Resolves #
**The title must start with its area: [Engine], [GUI], [TUI], [Benchmark], [Docs] or [Other], for example "[Engine] fix decode stop on long prompts" or "[GUI] keep the model list after a tab switch".**
## Summary
<!-- Quick Summary of changes -->
<!--
Read CONTRIBUTING.md first. Lead with the problem and the result, not a file-by-file list.
Placeholders below are not test results: replace them with what you observed, or write "none".
-->
## What changed
<!-- Specifics on files changed, and what changes were made there -->
## Problem and result
<!-- What triggered this change. How it behaves before and after. -->
## Extra Notes
<!-- Any extra notes, delete if there are none -->
## Related discussion
<!-- The issue URL and the maintainer's agreement on scope, when the change needs one (features, refactors, defaults). Write "none" if not applicable. -->
## Approach
<!-- Why this fix. Compatibility effects. Keep unrelated cleanup out of the PR. -->
## Validation
<!-- Exact commands, the revision and the environment tested, what you saw. Keep the three lists apart. -->
missing.push('Start the title with its area: [Engine], [GUI], [TUI], [Benchmark], [Docs] or [Other], then a short description. Example: "[Engine] decode stops after 2 minutes".');
missing.push('Re-test on Strata 0.2.0-beta and put that version in "'+form.needsCurrentVersion+'". If it no longer happens, say so in a comment instead.');
missing.push('Describe it in the issue body: what happened, what you expected, and the exact error if there is one. The issue forms (New issue > Bug report) list what to include.');
}
if(!lenient){
if(!ANY_VERSION_RE.test(text))missing.push('Add the Strata version (Settings > About, or `strata --version`).');
elseif(needCurrent&&!CURRENT_VERSION_RE.test(text))missing.push('Re-test on Strata 0.2.0-beta and name that version in the issue. If it no longer happens, say so in a comment instead.');
if(!HARDWARE_RE.test(text))missing.push('Add your graphics card with its VRAM, your RAM and your operating system.');
missing.push('Start the title with its area: [Engine], [GUI], [TUI], [Benchmark], [Docs] or [Other], then a short description. Example: "[Engine] fix decode stop on long prompts".');
if(!problem||realText(problem.value).length<20)missing.push('Fill in "Problem and result": what triggered the change, and how it behaves before and after.');
missing.push('Fill in "Validation": the exact commands, the revision and environment tested, what you saw. Keep Passed, Failed and Not run apart.');
}else{
valText=validation.value;
}
}else{
constplain=realText(text);
if(plain.length<150){
missing.push('Describe the pull request: the problem and the result. The pull request template shows the parts (Problem and result, Related discussion, Approach, Validation, Limits).');
}
if(!/(problem|result|before|after|fix|because|bug)/i.test(plain))missing.push('Add "Problem and result": what triggered the change, and how it behaves before and after.');
if(!/(validat|tested|passed|ran |built|build|not run)/i.test(plain))missing.push('Add "Validation": the exact commands, the revision and environment tested, and what you saw.');
'Thanks for the '+(isPull?'pull request':'issue')+'. To keep the tracker readable, every item needs an area prefix in the title and the parts of the '+(isPull?'pull request template':'issue form')+' filled in. This one is missing:',
'',
checklist(missing),
'',
'Edit the '+(isPull?'pull request':'issue')+' (title and description) and this note goes away on its own. If it is still missing parts in 5 days it will be closed, which is not a rejection: fix it and comment `/reopen`.',
'',
'Details: [CONTRIBUTING.md]('+CONTRIB+')',
].join('\n');
}
functionreopenMissingComment(isPull,missing){
return[
'Not reopened yet. Still missing:',
'',
checklist(missing),
'',
'Edit the '+(isPull?'pull request':'issue')+' and comment `/reopen` again. Details: [CONTRIBUTING.md]('+CONTRIB+')',
].join('\n');
}
functionreopenOkComment(isPull){
return'Reopened, thank you. A maintainer will pick it up from here.'+(isPull?' If it conflicts with main, rebase it first; that speeds up review.':'');
}
functionstaleComment(isPull){
return[
'Closing this '+(isPull?'pull request':'issue')+' because it still did not follow the format after 5 days. That is housekeeping, not a rejection.',
'',
'To bring it back: fix the title (area prefix) and the missing parts, then comment `/reopen`. A check runs and reopens it, or tells you what is still missing. See [CONTRIBUTING.md]('+CONTRIB+').',
assert.ok(!c.checkIssue({title:'[GUI] pin models',body:'### Area\n\n[GUI]\n\n### What do you want to do?\n\n_No response_'}).ok);
constq='### Area\n\n[Other]\n\n### Your question\n\nHow do I move the model folder to another drive?';
assert.ok(c.checkIssue({title:'[Other] model folder',body:q}).ok);
});
test('free-form issue',()=>{
constgood='Strata 0.2.0-beta on an RTX 3060 12 GB with 32 GB RAM on Linux: the server hangs when I send two requests at once. Expected both to finish.';
assert.ok(c.checkIssue({title:'[Engine] hang on two requests',body:good,mode:'reopen'}).ok);
assert.ok(c.checkIssue({title:'[Other] idea',body:'It would help if the app remembered my last model between runs, today I pick it again.',labels:['enhancement'],mode:'reopen'}).ok);
});
constprBody=(over={})=>{
constp={
problem:'Decode stopped on prompts above 8k because the stop check read the wrong length. Now it continues.',
validation:'**Passed:** `ctest -R decode`, commit abcdef1, RTX 3060, 12 of 12.\n\n**Failed:** none observed\n\n**Not run:** HIP build',
...over,
};
return`## Problem and result\n<!-- hint -->\n${p.problem}\n\n## Related discussion\nnone\n\n## Approach\nFix the length.\n\n## Validation\n${p.validation}\n\n## Limits\nnone`;
constbody=prBody({validation:'**Passed:** I tried it and it worked for me on my machine, looked fine.'});
assert.ok(c.checkPull({title:'[Engine] x y',body,mode:'intake'}).ok);
assert.ok(!c.checkPull({title:'[Engine] x y',body,mode:'reopen'}).ok);
});
test('free-form PR',()=>{
constbody='This fixes a crash when the context is above 8k, because the length was read from the wrong field. Before it aborted, after it continues. I built it on CUDA and ran the decode test, passed.';
awaitsay('This one was closed on purpose ('+labels.filter((l)=>BLOCK_LABELS.has(l.toLowerCase())).join(', ')+'). Please add your new evidence in a comment and a maintainer will decide.');
return;
}
if(wasOpen&&!labels.includes(LABEL)){
core.info('already open, nothing to do');
return;
}
// The bot reads the live item, not the possibly stale event copy.
awaitsay('The check passed, but GitHub would not reopen it ('+(e.message||e.status)+'). '+(isPull?'If the branch was deleted, restore it or open a new pull request and link this one.':'Please open a new issue and link this one.'));
echo "::error::The title must start with its area and then a short description. Allowed areas: [Engine], [GUI], [TUI], [Benchmark], [Docs], [Other]. Example: \"[Engine] fix decode stop on long prompts\". Your title: $TITLE"
`--output json`. `strata launch --status` shows what is serving, `strata launch --stop` stops it, and
`strata launch --show-key` prints the API key.
4. Connect the user's apps: `strata connect` lists them, `strata connect <app> --check` sends one prompt and one tool
call the way that app does.
Keep the server on `127.0.0.1` unless the user asks for more. Beyond it an API key is recommended: `--access lan` makes one for you, and `--no-api-key` serves without one (a warning, never a refusal; on a public tunnel the warning is stronger). The old route (clone
the repository and run `START-HERE.bat --yes` or `./setup.sh --yes`) still works and is the fallback when the
Terminal cannot run. Strata's MCP server ([docs/MCP_SERVER.md](docs/MCP_SERVER.md)) offers the same steps as tools.
## Working on the code
- How the engine works, every measured number, the API and all settings: [docs/DETAILS.md](docs/DETAILS.md) and
the [paper](docs/paper/Strata-Paper.pdf).
- AMD (HIP) build and validation: [docs/AMD_HIP.md](docs/AMD_HIP.md); multi-GPU: [docs/MULTI_GPU.md](docs/MULTI_GPU.md).
- Setup's own tests run without a GPU or downloads: `python tools/test_setup_<name>.py` (for example
rem Benchmark this Strata install the way it is used and write a report you can share (docs/COMMUNITY_BENCHMARKS.md).
rem It starts the model with your settings (or uses the one already running), takes about 10 minutes and changes nothing.
rem Options: --clients 4 (several requests at once), --long (a 32K prompt), --compare <older results folder>.
rem This is the quick speed check. The standard benchmark (docs/BENCHMARK_PROTOCOL.md), whose results other people can compare with, is `strata bench run`.
setlocal
title Strata - benchmark
cd /d "%~dp0"
ifnotexist".venv\Scripts\python.exe"(
echo Strata is not set up here yet: run START-HERE.bat first.
pause
exit /b 1
)
echo Quick speed check. For results other people can compare with (the standard benchmark, a few minutes), run: .venv\Scripts\python.exe strata.py bench run
<p align="center"><b>Ein KI-Modell mit 125 Milliarden Parametern auf deinem eigenen Gaming-PC</b><br>
NVIDIA- oder AMD-Grafikkarte (ab 12 GB) · Windows oder Linux · kostenlos und Open Source</p>
<h3 align="center">Ein Spitzenmodell auf deinem eigenen Gaming-PC</h3>
<p align="center"><a href="https://github.com/Niko1221/Strata/releases/download/v0.1.10/Pagoda.mp4"><img src="docs/media/pagoda-preview.webp" width="720" alt="Ein Voxel-Pagodengarten, den das Modell in Strata geschrieben hat, läuft im Browser"></a><br>
<p align="center">Unterstützt NVIDIA, AMD, Intel · Windows, Linux, Mac · kostenlos und Open Source</p>
<p align="center"><a href="https://github.com/Niko1221/Strata/releases/download/v0.1.10/Pagoda.mp4"><img src="docs/media/pagoda-preview.webp" width="720" alt="A voxel pagoda garden that Strata's model wrote, running in the browser"></a><br>
<sub>Ein Voxel-Pagodengarten aus einem einzigen Prompt, auf einer RTX 5070 mit Strata (IQ3_S, 128K Kontext) ·
<a href="https://github.com/Niko1221/Strata/releases/download/v0.1.10/Pagoda.mp4">ganzes Video (49 s)</a></sub></p>
Strata lässt **[Qwen3.8-Flash-Next](https://huggingface.co/Qwen/Qwen3.8-Flash-Next)** auf einem normalen PC laufen.
Das ist ein großes, kluges KI-Modell, das sonst meist einen Server braucht. Es chattet, schreibt Code, versteht
Bilder und arbeitet mit deinen Apps und Coding-Agents zusammen. Nichts verlässt deinen PC.
Strata lässt **[Qwen3.8-Flash-Next](https://huggingface.co/Qwen/Qwen3.8-Flash-Next)** laufen, ein Modell mit 125
Milliarden Parametern, für das man sonst einen Server braucht, auf einem normalen PC. Es chattet, schreibt Code, liest
Bilder und arbeitet mit deinen Apps und Coding-Agents zusammen. Deine Chats und Dateien bleiben auf deinem PC.
## Wie schnell ist es?
**Neu hier?** Sieh dir an, [was du brauchst](#was-du-brauchst), und [installiere dann die App](#option-a-die-strata-app).
Wir haben es auf zwei ganz normalen Gaming-PCs gemessen. Ein Token ist etwa ¾ eines Wortes.
- **Läuft auf einem Gaming-PC.** 32 GB Arbeitsspeicher (RAM) und Grafikspeicher zusammen reichen für den Anfang.
- **Passt zu deinen Werkzeugen.** Strata spricht die OpenAI- und die Anthropic-API, deshalb verbinden sich Claude Code, Codex CLI, OpenCode und andere Agents damit.
- **Liest Bilder.** Das ist standardmäßig eingeschaltet.
- **Kostenlos.** MIT-Lizenz, kein Konto, keine Cloud.
- **Privat.** Keine Telemetrie. Was Strata herunterlädt, wann es das Internet nutzt und wie es Downloads prüft, steht in [docs/SAFETY.md](docs/SAFETY.md).
- **Schreibt Antworten:** wie schnell die Antwort in einem kurzen Chat erscheint. 60 Tokens pro Sekunde sind
schneller, als du lesen kannst.
- **Liest deinen Prompt:** wie schnell es aufnimmt, was du schickst (hier ein Dokument, Code oder Chatverlauf mit
NVIDIA: Q2_0 mit Engine 0.1.36, die anderen Zeilen mit 0.1.26 (4K Antworten, 32K Prompts). Die vollständigen
Tabellen stehen in [DETAILS.md](docs/DETAILS.md#speed-measured). Eine Karte mit mehr VRAM ist schneller: Eine
RTX 3090 (24 GB) sollte etwa 100-140 Tokens pro Sekunde schreiben. Lange Chats und andere Karten:
[Tempo jedes Modells](docs/MODELS.md#how-fast-is-each-size), [Ergebnisse aus der Community](docs/COMMUNITY_BENCHMARKS.md).
<p align="center"><a href="https://buymeacoffee.com/strataengine"><img src="https://cdn.buymeacoffee.com/buttons/v2/default-yellow.png" alt="Buy Me A Coffee" height="50"></a><br>
<sub>Strata ist kostenlos. Wenn es auf deinem PC gut läuft, hält ein Kaffee die Arbeit daran am Laufen.</sub></p>
Version 0.2.3 · Engine 0.1.48
## Was du brauchst
| | |
**Du brauchst mindestens 32 GB Arbeitsspeicher (RAM) und Grafikspeicher (VRAM) zusammen.** Bei einem Mac oder einem Ryzen AI Max
ist das der gemeinsame Speicher. Mit mehr Speicher läuft ein größeres oder ein schnelleres Modell.
- Ein PC mit 32 GB und einer Grafikkarte mit 12-16 GB führt den Coder aus.
- Mit 64 GB RAM laufen alle Größen.
| Dein Computer | Details |
| --- | --- |
| **Grafikkarte**| **NVIDIA** GeForce RTX 20, 30, 40 oder 50 Serie, oder **AMD** Radeon RX 7900 XT / XTX, RX 7800 XT / 7700 XT, RX 9060 XT, RX 9070 / 9070 XT, Radeon AI PRO R9700 oder RX 6800 / 6900 Serie. Sie braucht **12 GB VRAM oder mehr**. |
| **RAM**| 32 GB oder mehr. Dein RAM entscheidet, [welches Modell](#welches-modell-soll-ich-nehmen) passt. Mit 64 GB laufen alle Größen. |
| **Festplatte**| Etwa 80 GB frei. Nimm wenn möglich eine SSD: Der erste Start geht dann viel schneller. |
| **System** | Windows 10 / 11 oder Linux und ein aktueller Grafiktreiber von NVIDIA oder AMD. |
| **NVIDIA** GeForce RTX 20, 30, 40, 50 Serie (12 GB oder mehr) | Installiere die App oder das Terminal. |
| **AMD**Radeon RX 6800 und neuer, Radeon AI PRO R9700 (12 GB oder mehr) | [AMD-Karten](docs/AMD_HIP.md) |
| | NVIDIA, ältere Karte oder Treiber älter als 580 | [`Strata-0.2.3-windows-cuda12.exe`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.3-windows-cuda12.exe) |
| | AMD Radeon RX 9000 Serie oder Radeon AI PRO R9700 | [`Strata-0.2.3-windows-hip.exe`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.3-windows-hip.exe) |
| | AMD Radeon RX 7000 Serie oder Ryzen AI Max (Strix Halo) | [`Strata-0.2.3-windows-hip-rdna3.exe`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.3-windows-hip-rdna3.exe) |
| | NVIDIA, ältere Karte oder Treiber älter als 580 | [`Strata-0.2.3-linux-nvidia-cuda12.AppImage`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.3-linux-nvidia-cuda12.AppImage) |
| | AMD Radeon RX 9000 Serie oder Radeon AI PRO R9700 | [`Strata-0.2.3-linux-amd-rx9070.AppImage`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.3-linux-amd-rx9070.AppImage) |
| | AMD Radeon RX 7000 Serie | [`Strata-0.2.3-linux-amd-rx7900.AppImage`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.3-linux-amd-rx7900.AppImage) |
| | AMD Ryzen AI Max (Strix Halo) | [`Strata-0.2.3-linux-amd-strixhalo.AppImage`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.3-linux-amd-strixhalo.AppImage) |
Das Skript prüft deinen PC, lädt das Terminal herunter, kontrolliert die SHA-256-Prüfsumme und entpackt es in deinen
Home-Ordner. Es braucht keine Administratorrechte. Öffne unter Windows danach ein neues Terminalfenster. Dann startest du
Strata:
```sh
strata launch
```
Es stellt dir eine Frage nach der anderen: Modell, Kontext, Bilder, wer sich verbinden darf. Danach lädt es die Engine für
deine Grafikkarte herunter (signiert und vor der Nutzung probeweise gestartet), dazu das Modell, und beginnt zu laufen.
Für Skripte und KI-Agents gibt es Flags:
```sh
strata launch --model q2_0 --yes --no-tui
```
Wenn du das Installationsskript vor dem Ausführen lesen willst, lade es zuerst mit `curl -fsSL <address> -o install.sh`
herunter, lies es und führe dann `sh install.sh` aus. Alle Bildschirme und Flags: [docs/TERMINAL_LAUNCH.md](docs/TERMINAL_LAUNCH.md).
### Option C: deine KI richtet es ein
Füge das in Claude Code, Cursor, Codex, GitHub Copilot oder einen anderen Coding-Assistenten ein:
```text
Set up Strata on this PC for me: https://github.com/Niko1221/Strata - follow docs/AI_SETUP.md in that repository.
```
Er prüft deine Grafikkarte, deinen RAM und deine Festplatte und wählt das passende Modell. Dann installiert und
startet er es und sagt dir, wie du deine Apps verbindest. KI-Tools können Strata auch über seinen
Der Assistent prüft deine Grafikkarte, deinen Arbeitsspeicher und den Speicherplatz, wählt das passende Modell, installiert
und startet es und sagt dir, wie du deine Apps verbindest. KI-Werkzeuge können Strata auch über den
[MCP-Server](docs/MCP_SERVER.md) installieren, starten und stoppen.
### Oder mach es selbst
Du willst es lieber selbst bauen? Siehe [Aus dem Quellcode bauen](#aus-dem-quellcode-bauen).
[Lade Strata herunter](https://github.com/Niko1221/Strata/archive/refs/heads/main.zip) und entpacke es (oder nutze
`git clone`). **Windows:** Doppelklick auf **`START-HERE.bat`**. **Linux:** Führe im Strata-Ordner **`./setup.sh`** aus.
## Modelle
Die Schritte sind für NVIDIA und AMD gleich. Der Installer erkennt deine Karte und richtet die passende Engine
dafür ein. Er stellt dir ein paar Fragen:
Die App und `strata launch` empfehlen ein Modell passend zu deinem Speicher. Es ist ein einziges Modell in mehreren
Größen, mehr oder weniger stark komprimiert. Kleinere Größen sind schneller. Größere Größen sind etwas klüger.
- welches Modell und welche Größe,
- wie viel Kontext (wie viel Text sich das Modell merkt),
- ob es Bilder lesen soll.
### Wenig Speicher? Probier Gyro
Drück jedes Mal Enter, um die empfohlene Antwort zu nehmen. Dann lädt er das Modell herunter (etwa 70 GB) und
startet es. Bricht der Download ab, starte ihn einfach neu: Er macht dort weiter, wo er aufgehört hat. Dein Browser
öffnet die Strata-App unter `http://127.0.0.1:8080`.
[Gyro](https://huggingface.co/agentionai/Qwen3.8-Flash-Next-Gyro-GGUF) (von agentionai) ist die kleinste Möglichkeit, dieses Modell auszuführen.
> **Während das Modell startet, kann dein PC 1-3 Minuten langsam sein oder nicht reagieren** (beim ersten Mal am
> längsten). Strata lädt 35-55 GB in deinen RAM und reserviert einen Teil davon für die Grafikkarte. Das ist normal.
> Warte und schließ das Fenster nicht. Im Fenster siehst du, was Strata gerade tut.
| Größe | Benötigter Speicher (RAM + VRAM) | Download | Was es ist |
| --- | ---: | ---: | --- |
| **Gyro-S** | 27,6 GB | 58,5 GB | Für PCs, auf denen nichts anderes läuft. Die Qualität ist geringer als bei IQ2_XS. |
| **Gyro-M** | 35 GB | 92,0 GB | Ungefähr die Qualität von IQ3_XXS bei etwa 20 % weniger GPU-Speicher. Es schreibt Antworten langsamer als Q2_0: 16 % auf einer RTX 3060 mit 12 GB, 21 % auf einer R9700. |
**Beim nächsten Mal** startest du wieder `START-HERE.bat` (oder `./setup.sh`). Es startet sofort und lädt nichts
doppelt herunter. Schließ das Fenster, um das Modell zu stoppen. `UPDATE.bat` (`./update.sh`) aktualisiert Strata,
ohne es zu starten. Updates, Docker, mehrere Karten, wo die Dateien landen und alle Optionen:
[docs/INSTALL.md](docs/INSTALL.md).
Gyro läuft auf NVIDIA- und AMD-Karten (bei AMD experimentell) auf Intel-Arc-Karten unter Linux (getestet auf einer Arc A750) und ab 0.2.2 auf Macs mit Apple Silicon (Gyro-S auf einem Mac mini mit 32 GB: 9 Tokens/s, die Experten werden von der SSD gelesen). Details und Messwerte:
[docs/GYRO.md](docs/GYRO.md). Starte es mit `strata launch --model gyro-s`.
## Welches Modell soll ich nehmen?
### Die anderen Größen
Der Installer empfiehlt eins passend zu deinem RAM. Dasselbe Modell gibt es in mehreren Größen, mehr oder weniger
stark komprimiert. Kleinere Größen sind schneller. Größere sind etwas klüger.
| Dein RAM | Nimm | Warum |
| Dein Speicher (RAM + VRAM) | Nimm | Hinweise |
| --- | --- | --- |
| **32 GB** | **Coder** | passt in 32 GB und ist für Code gemacht (mit einer 24-GB-Karte laufen auch Q2_0 und IQ2_XS) |
| **48 GB** | **IQ2_XS** (oder Q2_0, das schnellste) | die größeren Größen passen nicht |
| **64 GB** | **IQ2_XS** (empfohlen) oder IQ3_XXS / IQ3_S | alle Größen passen; IQ3_S ist das beste und das langsamste |
| **96 GB oder mehr** | **IQ3_S**oder Unsloths UD-IQ4_XS (~4 Bit) | Platz für die größten Größen, auch wenn alles andere offen ist |
| **32 GB** | **Gyro-S**, oder den **[Coder](docs/MODELS.md#coder)** für Code | Der Coder braucht 32 GB RAM und behält die Hälfte der Experten, deshalb ist er außerhalb von Code schwächer |
| **40 GB** | **[Swift 1.5](docs/MODELS.md#swift-15)**IQ2_XS (39,2 GB), oder **Gyro-M** (35 GB) | Swift denkt kürzer nach, bevor es antwortet. Gyro-M kommt der Qualität von IQ3_XXS nahe, schreibt Antworten aber 16-21 % langsamer als Q2_0 |
| **64 GB** | **IQ3_S**(54,8 GB), das Original oder Swift 1.5 | Am langsamsten, am besten. Es erreicht in den veröffentlichten Tests das volle Modell |
| **96 GB oder mehr** | **UD-IQ4_XS** von Unsloth (etwa 72 GB RAM) bis **Q8_0** (etwa 136 GB), 4 bis 8 Bit | Näher am vollen Modell. Bei weniger RAM liest Strata einen Teil von der SSD, das ist viel langsamer |
- **[Coder](docs/MODELS.md#coder):** eine Version für Code, bei der die Hälfte der Experten entfernt wurde. Sie
erreicht 91 % des SWE-bench-Verified-Werts des vollen Modells (von ihren Autorengemessen) und passt in 32 GB RAM.
Außerhalb von Code ist sie schwächer, auch bei chinesischem und anderem CJK-Text (#438). Nimm dafür Q2_0, IQ2_XS
oder IQ3_S, die alle Experten behalten.
- **[Swift 1.5](docs/MODELS.md#swift-15):** ein Fine-Tune, das viel kürzer nachdenkt, bevor es antwortet. Du
bekommst die Antwort früher, bei etwa gleicher Qualität.
- **[Unsloth UD-IQ4_XS](docs/MODELS.md#unsloth-ud-iq4_xs):** die ~4-Bit-Version von Unsloth, in der Qualität
zwischen IQ3_S und UD-Q4_K_XL. 94 GB Download. Mit weniger als ~80 GB RAM liest Strata einen Teil davon während
der Antwort von der SSD, dort ist es also langsamer (eine NVMe-SSD hilft).
- **[Unsloth UD-Q4_K_XL](docs/MODELS.md#unsloth-ud-q4_k_xl-experimental)** (experimentell): am nächsten am vollen
Modell. Aber Strata liest das meiste davon während der Antwort von der SSD, deshalb schreibt es auf einem PC mit
64 GB nur 7-8.5 Tokens/s.
- **[OrcaRouter's Uncensored IQ3_XXS](docs/MODELS.md#orcarouter-uncensored-iq3_xxs):** Das richtest du von Hand
ein. Es steht nicht im Menü des Installers.
Auch mit weniger RAM, als eine Zeile nennt, kann es funktionieren: Strata wechselt von selbst in seinen
[Modus für wenig RAM](docs/MODELS.md#a-big-graphics-card-and-little-ram). Rechne mit weniger Tempo, mit einer kleinen
Grafikkarte mit deutlich weniger.
Größen, Downloads und was wohin passt: [docs/MODELS.md](docs/MODELS.md). Um später ein weiteres Modell
- **[Swift 1.5](docs/MODELS.md#swift-15)** ist eine Feinabstimmung, die kürzer nachdenkt, bevor sie antwortet, bei etwa gleicher Qualität und gleichem RAM-Bedarf.
- **[NVFP4](docs/NVFP4.md)** ist NVIDIAs 4-Bit-Version für NVIDIA-Karten. Sie braucht etwa 80 GB RAM, um alle Experten im RAM zu halten.
- **[Qwen3.6-35B-A3B](docs/QWEN36.md)** ist ein kleineres, anderes Modell, das Bilder liest und mehrere Anfragen gleichzeitig beantworten kann.
- **[OrcaRouters Uncensored IQ3_XXS](docs/MODELS.md#orcarouter-uncensored-iq3_xxs)** richtest du von Hand ein.
<p align="center"><img src="docs/media/runpagoda.png" width="900" alt="Der Monitor-Tab der Strata-App neben einem Coding-Agent"><br>
<sub>Der <b>Monitor</b> der Strata-App (links), während ein Coding-Agent den Pagodengarten aus dem Video schreibt (rechts)</sub></p>
Alle Größen, Downloads und was wo läuft: [docs/MODELS.md](docs/MODELS.md).
- **Im Browser:** Öffne `http://127.0.0.1:8080`. Dort gibt es **Chat**, einen Live-**Monitor** für das Modell und
deine GPU/CPU/RAM und **About** mit den Einstellungen und Adressen.
- **Deine Apps und Coding-Agents:** Füge einen „OpenAI-kompatiblen“ Anbieter mit der Basis-URL
**`http://127.0.0.1:8080/v1`** hinzu. Jeder API-Key und jeder Modellname funktioniert.
- Apps, die die API von Anthropic nutzen: `http://127.0.0.1:8080/v1/messages` (Claude Code:
`ANTHROPIC_BASE_URL=http://127.0.0.1:8080`).
- Codex CLI und andere Apps, die die OpenAI Responses API nutzen: `/v1/responses`
[DeepSeek V4.1-Flash](https://huggingface.co/ISTA-DASLab/DeepSeek-V4.1-Flash-1017-GSQ-RCO-GGUF) ist ein anderes, deutlich größeres Modell. ISTA-DASLab hat die drei Sets unten gebaut. Strata startet sie auf einem PC mit einer 12-GB-Grafikkarte und 46 bis 64 GB RAM und liest den Rest von der SSD. Es läuft auf NVIDIA-RTX-Karten der Serien 30 bis 50 und auf der Radeon AI PRO R9700. Es nutzt ein eigenes Engine-Paket, `strata-dsv41`, das `strata launch` lädt, wenn du DeepSeek wählst. Die Desktop-App bietet es noch nicht an.
## Etwas ist schiefgelaufen?
| Set | Download | Qualität im Vergleich zum Originalmodell | Antwortet mit |
| --- | ---: | --- | --- |
| **IQ1_M** (1,75 Bit) | 239 GB | KL 0,217, Perplexität 3,80 (+21 %); Code +8,9 %, Agent-Aufgaben +10,2 %; schwächer bei einfacher englischer Prosa. Laut Modellkarte von ISTA-DASLab behält es 98,7 % des Aufgabendurchschnitts des Basismodells (AIME25, GPQA-Diamond, LiveCodeBench v6). | noch nicht gemessen |
| **IQ2_XXS** (2.00 Bit) | 257 GB | KL 0,170, Perplexität 3,63 (+15,5 %); Code +8,6 %, Agent-Aufgaben +5,3 % | 6 bis 9 Tokens/s auf einer RTX 3060, etwa 5 auf einer RTX 5070 |
| **IQ2_XS** (2.25 Bit) | 274 GB | KL 0,107, Perplexität 3,37 (+7,3 %); Code +3,2 %, Agent-Aufgaben +2,2 % | etwa 11 Tokens/s auf einer Radeon AI PRO R9700 |
| **IQ4_NL** (Engram-Tabelle, kein drittes Modell) | 111 GB, schon in allen drei Downloads oben enthalten | Die 4-Bit-Engram-Nachschlagetabelle, die das Modell aus RAM oder SSD liest. Alle drei Sets nutzen diese eine Datei (`DeepSeek-V4.1-Flash-Engram-IQ4_NL.gguf`). | n/a |
- **Mein PC ist beim ersten Start von Strata eingefroren.** Das ist normal, während das Modell lädt. Warte und
schließ das Fenster nicht. Nach 10 Minuten immer noch eingefroren? Starte den PC neu, schließ andere Programme und
versuch es noch einmal, oder nimm eine kleinere Größe.
- **Es hat beim Herunterladen oder Installieren aufgehört.** Starte `START-HERE.bat` (oder `./setup.sh`) noch
einmal. Es macht dort weiter, wo es aufgehört hat.
- **Es ist sehr langsam und die Festplattenlampe blinkt ständig, oder es meldet „the engine stopped
unexpectedly“.** Dein PC hat nicht genug freien RAM. Schließ andere Programme (Browser brauchen viel) oder nimm
eine kleinere Größe (Q2_0 oder IQ2_XS).
- **Es meldet, dass Port 8080 schon belegt ist.** Strata läuft bereits. Such nach seinem Fenster.
4x Radeon AI PRO R9700: etwa 31 Tokens/s, 55 bis 69 mit DSpark.
Weitere Probleme und ihre Lösungen: [docs/TROUBLESHOOTING.md](docs/TROUBLESHOOTING.md). Kommst du nicht weiter?
Öffne ein [Issue](https://github.com/Niko1221/Strata/issues) und hänge `strata-<model>.log` aus dem Strata-Ordner
an. Du hast ein Sicherheitsproblem gefunden? Melde es vertraulich: [SECURITY.md](SECURITY.md).
Starte es mit `strata launch --model ist-das-2.00bpw` (oder `ist-das-1.75bpw`, `ist-das-2.25bpw`). Details, Geschwindigkeiten je Karte und die manuelle Installation: [docs/DEEPSEEK41.md](docs/DEEPSEEK41.md).
## Wie funktioniert es?
## Wie schnell ist es?
Modelle wie dieses laufen normalerweise auf Servern mit Hunderten Gigabyte Grafikspeicher. Deine Grafikkarte hat
12-24 GB. Strata bringt das Modell trotzdem unter, indem es **die Arbeit auf deinen ganzen PC verteilt**. Stell dir
eine Küche vor: Was du ständig brauchst, bleibt auf der Arbeitsfläche, der Rest wartet in der Vorratskammer.
Gemessen mit einer Grafikkarte mit 12 GB (RTX 5070, Ryzen 5 7600, 64 GB RAM). Ergebnisse anderer Karten findest du hier:
[Tempo jeder Größe](docs/MODELS.md#how-fast-is-each-size), [Ergebnisse der Community](docs/COMMUNITY_BENCHMARKS.md).
<p align="center"><img src="docs/media/how-it-works.svg" width="860" alt="Die 24,576 Experten des Modells: die meistgenutzten auf der Grafikkarte, alle im RAM, eine Nachschlagetabelle auf der SSD"></p>
| Modell | Antworten erscheinen mit | Liest deinen Prompt mit |
- **Das Modell ist ein Team aus 24,576 kleinen Spezialisten („Experten“).** Jedes Wort braucht nur 10 davon.
- **Deine Grafikkarte** hält die paar tausend Experten, die am häufigsten gebraucht werden. **Dein RAM** hält alle,
und **dein Prozessor** rechnet gleichzeitig am Rest. **Deine SSD** hält eine große Nachschlagetabelle.
Ein Token ist etwa ¾ eines Wortes, 60 Tokens pro Sekunde sind also schneller, als du lesen kannst. "Liest deinen Prompt"
beschreibt, wie schnell Strata ein langes Dokument, Code oder einen Chatverlauf aufnimmt.
<p align="center"><img src="docs/media/guess-and-check.svg" width="860" alt="Ein kleiner Helfer rät die nächsten Wörter; das große Modell prüft alle auf einmal und behält die richtigen"></p>
Gemessen mit einer AMD-Karte mit 16 GB (RX 9070 XT, Ryzen 9 3900X, 47 GB RAM):
- **Raten, dann prüfen:** Ein kleiner Helfer rät die nächsten paar Wörter. Das große Modell prüft sie alle auf
einmal. Du bekommst dieselbe Antwort, nur 1.6-1.8x schneller.
- **Lange Texte werden in großen Stücken gelesen** (bis zu 8,192 Tokens auf einmal), mit über 1,000 Tokens pro
Sekunde.
| Modell | Antworten erscheinen mit | Liest deinen Prompt mit |
Die längere Erklärung: [docs/HOW_IT_WORKS.md](docs/HOW_IT_WORKS.md). Jeder Teil und seine Zahlen:
Die Balken haben in beiden Tabellen denselben Maßstab. Eine Karte mit mehr VRAM ist schneller. Eine RTX 3090 (24 GB) sollte
etwa 100-140 Tokens pro Sekunde schreiben. Die Engine-Version hinter jeder Zahl steht in [DETAILS.md](docs/DETAILS.md#speed-measured).
## Benutzung
### In der App
Die App hat einen **Chat**, einen **Monitor** für das Modell sowie für GPU, CPU und RAM, eine Seite **Modelle** und die
**Einstellungen**. Der Chat kann in einem Ordner auf deinem PC arbeiten. Im Modus **Manuell** fragt er nach, bevor er eine
Datei ändert oder einen Befehl ausführt. Mit `/loop 10m <what to do>` wiederholst du eine Nachricht alle 10 Minuten, solange
der Chat geöffnet ist.
Der Chat kann das Web mit DuckDuckGo (oder Bing) durchsuchen, ohne Konto. Unter **Einstellungen, Tools** kannst du einen
Suchschlüssel hinzufügen. Die Suche ist die einzige Funktion, die etwas von deinem PC sendet (die Suchbegriffe).
Um zwei Modelle auszuführen, schalte **Mehrere Modelle ausführen** ein und gib jedem Modell eine eigene Karte desselben
Herstellers.
<p align="center"><img src="docs/media/app-chat.png" width="900" alt="The Strata app: a chat where the agent built a small voxel pagoda garden, with the page open in the preview on the right"><br>
<sub>Ein Chat in der Strata-App: Der Agent hat einen kleinen Voxel-Pagodengarten als eine HTML-Datei geschrieben, und die Seite öffnet sich in der Vorschau neben dem Gespräch</sub></p>
### Im Terminal
<p align="center"><img src="docs/media/terminal-monitor.png" width="900" alt="The Strata Terminal monitor overview while the model writes a reply"><br>
<sub><code>strata monitor</code>, während das Modell antwortet: Geschwindigkeit, GPU, Speicher und die letzten Anfragen</sub></p>
strata update --check # is there a new engine? (strata update installs it)
strata doctor # check the installed engine files
```
`strata --help` listet alles auf. Starten und Bereitstellen laden nichts herunter. Nur die Installation, `strata launch`
(wenn du ein Modell wählst) und `strata update` tun das. Um Strata von deinem Handy oder einem anderen PC aus zu erreichen,
nutze `strata launch --access lan`. Der Befehl erzeugt einen API-Schlüssel und startet in einem Netzwerk nicht ohne einen.
Ein Browser öffnet sich nur, wenn du `--open` angibst. Anleitung: [docs/TERMINAL_LAUNCH.md](docs/TERMINAL_LAUNCH.md). Der ganze Client: [docs/TERMINAL.md](docs/TERMINAL.md).
### Mit KI-Agents
Ein Agent installiert Strata mit dem Einzeiler aus Option B und führt
`strata launch --model NAME --yes --no-tui` aus. Die Schritte, Flags und Exit-Codes stehen in [docs/AI_SETUP.md](docs/AI_SETUP.md),
die Regeln für das Repository in [AGENTS.md](AGENTS.md).
### Andere Apps verbinden
| App-Typ | Basis-URL |
| --- | --- |
| OpenAI-kompatibel (die meisten Apps und Agents) | `http://127.0.0.1:8080/v1` (beliebiger API-Schlüssel, beliebiger Modellname) |
| Anthropic-API (Claude Code und andere) | `http://127.0.0.1:8080`, ohne `/v1`. Diese Apps hängen `/v1/messages` selbst an. Claude Code: `ANTHROPIC_BASE_URL=http://127.0.0.1:8080` |
| OpenAI Responses API (Codex CLI) | `http://127.0.0.1:8080/v1/responses` ([Einrichtung](docs/DETAILS.md#the-responses-api-and-codex-cli)) |
`strata connect` gibt die genauen Einstellungen für OpenCode, Codex CLI, Claude Code, oh-my-pi, pi, Hermes Agent und OpenClaw aus.
`strata connect opencode --write` schreibt die Datei nach einer Rückfrage und legt eine Sicherung an.
### Bilder
Bilder sind für Modelle, die sie lesen können, standardmäßig eingeschaltet. Hänge ein Bild im Chat an oder füge es ein, oder
sende es aus deiner App. Der Bildleser läuft auf dem Prozessor, wenn deine Grafikkarte weniger als 24 GB hat (einige Sekunden
pro Bild, ohne Speicher der Karte zu belegen), und ab 24 GB auf der Grafikkarte. Die Regel gilt für NVIDIA und AMD gleich.
Du kannst Bilder pro Modell ausschalten oder den Leser mit `--vision off`, `--vision gpu` oder `--vision cpu` wählen.
Die Zahlen hinter der Regel: Auf einer Radeon AI PRO R9700 brauchte der Bildleser der Karte 57 ms für ein Bild mit 260 Tokens,
der des Prozessors 3,1 s. Auf Karten mit 16 GB (Tesla P100 und RTX A4000, Linux) kostete der Bildleser der Karte 3-5 % des
Texttempos, weil er etwa 1,4 GB belegt, die sonst das Modell nutzen würde. Auf dem Mac liest die GPU, egal wie viel Speicher er hat (0,45 s für ein Bild mit 260 Tokens auf einem M4 mit 32 GB, etwa 1,2 GB des gemeinsamen
Speichers); der Prozessor braucht dort etwa 50 s pro Bild und liest deshalb nur mit `--vision cpu`.
### Gut zu wissen
- Das Denkniveau (**aus, niedrig, mittel oder hoch**) stellst du im Chat-Menü oder unter "reasoning effort" in deiner App ein. Aus ist am schnellsten.
- Strata beantwortet standardmäßig eine Anfrage nach der anderen. Für mehrere gleichzeitig nutze `--parallel N` in `strata launch` oder
`"parallel": 2` in der Konfiguration ([BATCHING.md](docs/BATCHING.md)). Auf einer Karte mit 12 GB wird dadurch jede Antwort langsamer.
- Strata liest die erste Nachricht eines Chats komplett, etwa 1 Minute pro 30.000 Tokens. Folgenachrichten starten in Sekunden.
- Wo deine Chats gespeichert werden und die ganze API: [docs/INSTALL.md](docs/INSTALL.md#where-things-are-stored), [docs/DETAILS.md](docs/DETAILS.md#using-it).
## Aus dem Quellcode bauen
<details>
<summary>Windows, Linux und Mac aus dem Repository</summary>
[Lade Strata herunter](https://github.com/Niko1221/Strata/archive/refs/heads/main.zip) und entpacke es, oder nutze `git clone`.
**Windows:** Doppelklick auf `START-HERE.bat`. **Linux:** Führe `./setup.sh` im Strata-Ordner aus.
**Mac (Apple Silicon, macOS 15 oder neuer):** Installiere einmal die Command Line Tools von Apple mit `xcode-select --install`
(Xcode selbst brauchst du nicht). Öffne das Terminal im Strata-Ordner und führe `./setup.sh` aus. Das Skript baut die
Mac-Engine auf deinem Mac (etwa 7 Minuten), stellt dieselben Fragen, lädt das Modell herunter und startet es. Beim nächsten
Mal startet `./setup.sh` sofort. Welche Größe zu deinem Mac passt: [docs/MAC.md](docs/MAC.md).
Der Installer erkennt deine Karte und richtet die passende Engine ein. Er fragt, welches Modell und welche Größe du willst,
wie viel Kontext (wie viel Text sich das Modell merkt) und ob es Bilder lesen soll. Drück Enter für die empfohlene Antwort.
Dann lädt er das Modell herunter (etwa 70 GB) und startet es. Wenn der Download abbricht, führe das Skript erneut aus, dann
geht es weiter. Dein Browser öffnet die Strata-Seite unter `http://127.0.0.1:8080`.
> Während das Modell startet, kann dein PC 1-3 Minuten langsam sein oder nicht reagieren (beim ersten Mal am längsten). Strata
> lädt 35-55 GB in deinen RAM und sperrt einen Teil davon für die Grafikkarte. Warte ab und schließe das Fenster nicht.
Starte beim nächsten Mal wieder `START-HERE.bat` (oder `./setup.sh`). Es startet sofort und lädt nichts doppelt herunter.
Schließ sein Fenster, um das Modell zu beenden. `UPDATE.bat` (`./update.sh`) aktualisiert Strata, ohne es zu starten.
Aktualisieren, Docker, mehrere Karten, wohin die Dateien kommen und alle Optionen: [docs/INSTALL.md](docs/INSTALL.md).
Um deinen PC zu messen, führe `BENCH.bat` (`./bench.sh`) aus. Das dauert 5-10 Minuten und schreibt einen Bericht, den du teilen
**Optional: mehr Tempo auf einer Karte, wenn das ganze Modell in den RAM passt.** Zwei Einstellungen zum Einschalten von
RubenTorney (#1904, #1905). Sie helfen, wenn du eine Grafikkarte nutzt und dein RAM alle Experten aufnimmt, zum Beispiel bei
Q2_0 oder IQ3_XXS auf einem PC mit 64 GB. Öffne `strata-<model>.json` im Strata-Datenordner ([wo](docs/INSTALL.md#where-things-are-stored)) und ergänze:
-`"--adapt-async", "1"` in der Liste `"args"`. Der Expertencache wird dann in einem Hilfsthread aktualisiert, statt die
Antwort anzuhalten. Gemessen auf einer RTX 5070 (Windows, Q2_0): +4-5 % beim Decodieren, +7-10 % bei Code und langen
Dokumenten. Die Antworten können sich leicht von der Standardeinstellung unterscheiden und bleiben von Lauf zu Lauf gleich.
-`"STRATA_PIN_BY_NEED": "1"` im Block `"env"` (Windows). Windows kann nur etwa die Hälfte des RAM für die Grafikkarte sperren,
und diese Einstellung sperrt zuerst die Experten, die die Karte am dringendsten braucht. Gemessen auf der RTX 5070: +4,4 % beim Lesen des Prompts.
```json
"args":[...,"--adapt-async","1"],
"env":{"STRATA_PIN_BY_NEED":"1"}
```
Starte Strata nach der Änderung neu. Zum Rückgängigmachen entfernst du die beiden Einträge.
</details>
## Wie funktioniert das?
Modelle wie dieses laufen normalerweise auf Servern mit Hunderten Gigabyte Grafikspeicher. Deine Grafikkarte hat 12-24 GB.
Strata bringt das Modell auf deinen PC, indem es die Arbeit auf den ganzen Rechner verteilt. Stell dir eine Bibliothek vor.
- Das Modell ist ein Team aus 24.576 kleinen Spezialisten ("Experten"), wie 24.576 Nachschlagewerke. Für jedes Wort braucht es nur 10 davon.
- **Deine Grafikkarte ist der Schreibtisch.** Er ist klein und schnell. Die etwa 4.000 am häufigsten genutzten Bücher liegen
dort aufgeschlagen.
- **Dein RAM sind die Regale im Lesesaal.** Alle 24.576 Bücher stehen dort. Braucht ein Wort ein Buch, das nicht auf dem
Schreibtisch liegt, holt dein Prozessor es sofort aus dem Regal, während die Karte weiterarbeitet.
- **Deine SSD ist das Archiv.** Sie enthält eine Nachschlagetabelle von 29 GB. Das Modell liest pro Wort nur wenige Zeilen
daraus, deshalb muss sie nicht im RAM liegen.
<p align="center"><img src="docs/media/how-it-works.svg" width="860" alt="The model's 24,576 experts: the busiest on the graphics card, all of them in RAM, a lookup table on the SSD"></p>
Ein kleiner Helfer rät außerdem die nächsten Wörter. Das große Modell prüft sie alle auf einmal und behält die richtigen. Du
bekommst dieselbe Antwort, 1,6-1,8-mal früher. Lange Texte werden in großen Stücken gelesen, bis zu 8.192 Tokens auf einmal,
mit über 1.000 Tokens pro Sekunde.
<p align="center"><img src="docs/media/guess-and-check.svg" width="860" alt="A small helper guesses the next words; the big model checks them all at once and keeps the right ones"></p>
Die ausführliche Erklärung: [docs/HOW_IT_WORKS.md](docs/HOW_IT_WORKS.md). Jeder Teil mit seinen Zahlen:
[die Details](docs/DETAILS.md#how-it-works) und das [Paper](docs/paper/Strata-Paper.pdf).
## Etwas ist schiefgegangen?
- **Mein PC ist beim ersten Start von Strata eingefroren.** Strata lädt das Modell in den RAM. Warte ab und schließe das
Fenster nicht. Nach 10 Minuten immer noch eingefroren? Starte den PC neu, schließe andere Programme und versuch es noch
einmal, oder wähle eine kleinere Größe.
- **Es ist beim Herunterladen oder Installieren stehen geblieben.** Starte die App oder `strata launch` noch einmal, oder
führe `START-HERE.bat` (oder `./setup.sh`) erneut aus. Es macht dort weiter, wo es aufgehört hat.
- **Es ist sehr langsam, die Festplattenanzeige blinkt ständig, oder es meldet "the engine stopped unexpectedly".** Dein PC
hat nicht genug freien RAM. Schließe andere Programme (Browser brauchen viel) oder wähle eine kleinere Größe wie Q2_0 oder IQ2_XS.
- **Es meldet, dass es keine Engine für diesen PC gibt.** Öffne in der App **Einstellungen, Engine** und drücke **Engine
aktualisieren**. Führe im Terminal `strata update` aus. `strata doctor` zeigt, welche Engine-Dateien installiert sind.
- **Es meldet, dass Port 8080 schon belegt ist.** Strata läuft bereits. Such das Fenster.
Weitere Probleme und Lösungen: [docs/TROUBLESHOOTING.md](docs/TROUBLESHOOTING.md). Du kommst nicht weiter? Öffne ein
[Issue](https://github.com/Niko1221/Strata/issues). Hänge in der App die Datei aus **Einstellungen, Engine, Diagnose, Protokolle anzeigen, Protokolle speichern** an. Füge im Terminal die Ausgabe von `strata doctor` ein. Hänge beim Start aus dem Quellcode die
Datei `strata-<model>.log` aus dem Strata-Ordner an. Ein Sicherheitsproblem meldest du bitte privat: [SECURITY.md](SECURITY.md).
## Danksagung und Lizenz
Das Modell ist [Qwen3.8-Flash-Next](https://huggingface.co/Qwen/Qwen3.8-Flash-Next) vom Qwen-Team. Komprimiert
wurde es von [ISTA-DASLab](https://huggingface.co/ISTA-DASLab/Qwen3.8-Flash-Next-GSQ-RCO-GGUF), UkisAI (Swift 1.5)
Das Modell ist [Qwen3.8-Flash-Next](https://huggingface.co/Qwen/Qwen3.8-Flash-Next) vom Qwen-Team. Komprimiert wurde es von
und Unsloth. Strata nutzt Teile von [llama.cpp / ggml](https://github.com/ggml-org/llama.cpp). Alle Danksagungen:
[docs/HOW_IT_WORKS.md](docs/HOW_IT_WORKS.md#credits). Strata ist Open Source unter der [MIT-Lizenz](LICENSE). Ein
paar Teile und alle Modelle haben eigene Lizenzen ([welche](docs/HOW_IT_WORKS.md#license)).
[docs/HOW_IT_WORKS.md](docs/HOW_IT_WORKS.md#credits). Strata ist Open Source unter der [MIT-Lizenz](LICENSE). Einige Teile und
alle Modelle haben eigene Lizenzen ([welche](docs/HOW_IT_WORKS.md#license)).
## Mitmachen
Du hast einen Fehler gefunden oder willst eine Änderung oder einen Benchmark einsenden? Lies zuerst [CONTRIBUTING.md](CONTRIBUTING.md).
Dort steht, was dazugehört, damit andere deine Arbeit nachprüfen können.
## Strata unterstützen
Strata ist kostenlos und Open Source. Wenn es dir nützt, kannst du die Entwicklung unterstützen:
Strata ist kostenlos und steht unter der MIT-Lizenz. Ich baue es und teste es auf jeder Hardware, die ich bekommen kann. Wenn es auf deinem PC gut läuft, hilft mir ein Kaffee,
weiter neue Karten zu testen und das zu beheben, was ihr meldet. Es besteht kein Druck. Strata zu nutzen und zu melden, was dir auffällt, hilft genauso viel.
<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>
<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>
Du möchtest Strata sponsern? Schreib mir über [GitHub](https://github.com/Niko1221).
<p align="center"><b>Ejecuta un modelo de IA de 125 mil millones de parámetros en tu propio PC gaming</b><br>
Tarjeta gráfica NVIDIA o AMD (12 GB o más) · Windows o Linux · libre y de código abierto</p>
<h3 align="center">Ejecuta un modelo de última generación en tu propio PC gaming</h3>
<p align="center"><a href="https://github.com/Niko1221/Strata/releases/download/v0.1.10/Pagoda.mp4"><img src="docs/media/pagoda-preview.webp" width="720" alt="Un jardín con una pagoda de vóxeles que escribió el modelo de Strata, funcionando en el navegador"></a><br>
<p align="center">Compatible con NVIDIA, AMD, Intel · Windows, Linux, Mac · libre y de código abierto</p>
<p align="center"><a href="https://github.com/Niko1221/Strata/releases/download/v0.1.10/Pagoda.mp4"><img src="docs/media/pagoda-preview.webp" width="720" alt="A voxel pagoda garden that Strata's model wrote, running in the browser"></a><br>
<sub>Un jardín con una pagoda de vóxeles, hecho con un solo prompt en una RTX 5070 con Strata (IQ3_S, contexto de 128K) ·
Strata ejecuta **[Qwen3.8-Flash-Next](https://huggingface.co/Qwen/Qwen3.8-Flash-Next)** en un PC normal. Es un
modelo de IA grande e inteligente que normalmente necesita un servidor. Conversa, escribe código, entiende imágenes
y funciona con tus aplicaciones y agentes de programación. Nada sale de tu PC.
Strata ejecuta **[Qwen3.8-Flash-Next](https://huggingface.co/Qwen/Qwen3.8-Flash-Next)**, un modelo de 125 mil millones
de parámetros que normalmente necesita un servidor, en un PC normal. Conversa, escribe código, lee imágenes y funciona
con tus aplicaciones y tus agentes de programación. Tus chats y tus archivos se quedan en tu PC.
## ¿Qué velocidad tiene?
**¿Eres nuevo?** Mira [lo que necesitas](#lo-que-necesitas) y luego [instala la aplicación](#opción-a-la-aplicación-strata).
Lo medimos en dos PC gaming normales. Un token equivale más o menos a ¾ de una palabra.
- **Funciona en un PC gaming.** Con 32 GB entre RAM y memoria gráfica ya puedes empezar.
- **Funciona con tus herramientas.** Habla las API de OpenAI y de Anthropic, así que Claude Code, Codex CLI, OpenCode y otros agentes se conectan a él.
- **Lee imágenes.** Viene activado por defecto.
- **Gratis.** Licencia MIT, sin cuenta, sin nube.
- **Privado.** Sin telemetría. Lo que Strata descarga, cuándo usa internet y cómo comprueba las descargas está en [docs/SAFETY.md](docs/SAFETY.md).
- **Escribe respuestas:** lo rápido que aparece la respuesta en un chat corto. 60 tokens por segundo es más rápido de lo que puedes leer.
- **Lee tu prompt:** lo rápido que procesa lo que le envías (aquí, un documento, código o historial de chat de 32K tokens).
**Necesitas al menos 32 GB entre RAM y memoria gráfica (VRAM).** En un Mac o un Ryzen AI Max, es la memoria unificada.
Con más memoria puedes usar un modelo más grande o más rápido.
</td><td>
- Un PC de 32 GB con una tarjeta de 12 a 16 GB ejecuta el Coder.
- Con 64 GB de RAM funcionan todos los tamaños.
| Tamaño | Escribe respuestas | Lee tu prompt |
| --- | ---: | ---: |
| **Q2_0** | 60 tokens/s | 1,160 tokens/s |
| **IQ2_XS** | 52 tokens/s | 1,110 tokens/s |
| **Coder** | 44 tokens/s | 1,420 tokens/s |
</td></tr>
</table>
NVIDIA: Q2_0 con el motor 0.1.36, las demás filas con 0.1.26 (respuestas de 4K, prompts de 32K). Las tablas
completas están en [DETAILS.md](docs/DETAILS.md#speed-measured). Una tarjeta con más VRAM es más rápida: una
RTX 3090 (24 GB) debería escribir unos 100-140 tokens por segundo. Chats largos y otras tarjetas:
[velocidad de cada modelo](docs/MODELS.md#how-fast-is-each-size), [resultados de la comunidad](docs/COMMUNITY_BENCHMARKS.md).
<p align="center"><a href="https://buymeacoffee.com/strataengine"><img src="https://cdn.buymeacoffee.com/buttons/v2/default-yellow.png" alt="Buy Me A Coffee" height="50"></a><br>
<sub>Strata es gratis. Si funciona bien en tu PC, un café ayuda a seguir trabajando en él.</sub></p>
## Qué necesitas
| | |
| Tu ordenador | Detalles |
| --- | --- |
| **Tarjeta gráfica**| **NVIDIA** GeForce serie RTX 20, 30, 40 o 50, o **AMD** Radeon RX 7900 XT / XTX, RX 7800 XT / 7700 XT, RX 9060 XT, RX 9070 / 9070 XT, Radeon AI PRO R9700 o serie RX 6800 / 6900. Necesita **12 GB de VRAM o más**. |
| **RAM**| 32 GB o más. Tu RAM decide [qué modelo](#qué-modelo-elijo) cabe. Con 64 GB funcionan todos los tamaños. |
| **Disco**| Unos 80 GB libres. Usa un SSD si puedes: el primer arranque es mucho más rápido. |
| **Sistema** | Windows 10 / 11 o Linux, y un controlador gráfico actual de NVIDIA o AMD. |
| **NVIDIA** GeForce RTX serie 20, 30, 40, 50 (12 GB o más) | Instala la aplicación o la Terminal. |
| **AMD**Radeon RX 6800 y posteriores, Radeon AI PRO R9700 (12 GB o más) | [Tarjetas AMD](docs/AMD_HIP.md) |
| | NVIDIA, tarjeta más antigua o controlador anterior a 580 | [`Strata-0.2.3-windows-cuda12.exe`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.3-windows-cuda12.exe) |
| | AMD Radeon serie RX 9000 o Radeon AI PRO R9700 | [`Strata-0.2.3-windows-hip.exe`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.3-windows-hip.exe) |
| | AMD Radeon serie RX 7000 o Ryzen AI Max (Strix Halo) | [`Strata-0.2.3-windows-hip-rdna3.exe`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.3-windows-hip-rdna3.exe) |
| | NVIDIA, tarjeta más antigua o controlador anterior a 580 | [`Strata-0.2.3-linux-nvidia-cuda12.AppImage`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.3-linux-nvidia-cuda12.AppImage) |
| | AMD Radeon serie RX 9000 o Radeon AI PRO R9700 | [`Strata-0.2.3-linux-amd-rx9070.AppImage`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.3-linux-amd-rx9070.AppImage) |
| | AMD Radeon serie RX 7000 | [`Strata-0.2.3-linux-amd-rx7900.AppImage`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.3-linux-amd-rx7900.AppImage) |
| | AMD Ryzen AI Max (Strix Halo) | [`Strata-0.2.3-linux-amd-strixhalo.AppImage`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.3-linux-amd-strixhalo.AppImage) |
El script analiza tu PC, descarga la Terminal, verifica su SHA-256 y la descomprime en tu carpeta personal. No necesita
permisos de administrador. En Windows, abre una ventana de terminal nueva después. Luego arranca Strata:
```sh
strata launch
```
Te hace una pregunta cada vez: modelo, contexto, imágenes, quién puede conectarse. Después descarga el motor para tu tarjeta
gráfica (firmado y arrancado como prueba antes de usarlo) y el modelo, y empieza a servir. Para scripts y agentes de IA, usa
opciones:
```sh
strata launch --model q2_0 --yes --no-tui
```
Si quieres leer el script de instalación antes de ejecutarlo, descárgalo primero con `curl -fsSL <address> -o install.sh`,
léelo y luego ejecuta `sh install.sh`. Todas las pantallas y opciones: [docs/TERMINAL_LAUNCH.md](docs/TERMINAL_LAUNCH.md).
### Opción C: que lo instale tu IA
Pega esto en Claude Code, Cursor, Codex, GitHub Copilot u otro asistente de programación:
```text
Set up Strata on this PC for me: https://github.com/Niko1221/Strata - follow docs/AI_SETUP.md in that repository.
```
Revisa tu tarjeta gráfica, tu RAM y tu disco, y elige el modelo que cabe. Luego lo instala, lo arranca y te explica
cómo conectar tus aplicaciones. Las herramientas de IA también pueden instalar, arrancar y detener Strata con su
El asistente revisa tu tarjeta gráfica, tu RAM y tu disco, elige el modelo adecuado, lo instala y lo arranca, y te explica
cómo conectar tus aplicaciones. Las herramientas de IA también pueden instalar, arrancar y detener Strata mediante su
[servidor MCP](docs/MCP_SERVER.md).
### O hazlo tú mismo
¿Prefieres compilarlo tú? Mira [Compilar desde el código fuente](#compilar-desde-el-código-fuente).
[Descarga Strata](https://github.com/Niko1221/Strata/archive/refs/heads/main.zip) y descomprímelo (o usa `git clone`).
**Windows:** haz doble clic en **`START-HERE.bat`**. **Linux:** ejecuta **`./setup.sh`** en la carpeta de Strata.
## Modelos
Los pasos son los mismos para NVIDIA y AMD. El instalador detecta tu tarjeta y prepara el motor adecuado para ella.
Te hace unas pocas preguntas:
La aplicación y `strata launch` recomiendan un modelo según tu memoria. Es un solo modelo en varios tamaños, más o menos
comprimido. Los tamaños pequeños son más rápidos. Los grandes son algo más inteligentes.
- qué modelo y qué tamaño,
- cuánto contexto (cuánto texto tiene presente el modelo),
- si debe entender imágenes.
### ¿Poca memoria? Prueba Gyro
Pulsa Enter cada vez para usar la respuesta recomendada. Después descarga el modelo (unos 70 GB) y lo arranca. Si la
descarga se detiene, vuelve a ejecutarlo: sigue donde lo dejó. Tu navegador abre la aplicación de Strata en
`http://127.0.0.1:8080`.
[Gyro](https://huggingface.co/agentionai/Qwen3.8-Flash-Next-Gyro-GGUF) (de agentionai) es la forma más pequeña de ejecutar este modelo.
> **Mientras el modelo arranca, tu PC puede ir lento o dejar de responder durante 1-3 minutos** (más la primera vez).
> Strata carga 35-55 GB en tu RAM y reserva una parte para la tarjeta gráfica. Es normal. Espera y no cierres la
> ventana. La ventana muestra lo que está haciendo Strata.
| Tamaño | Memoria que necesita (RAM + VRAM) | Descarga | Qué es |
| --- | ---: | ---: | --- |
| **Gyro-S** | 27,6 GB | 58,5 GB | Para PC donde no cabe nada más. La calidad es menor que la de IQ2_XS. |
| **Gyro-M** | 35 GB | 92,0 GB | Calidad parecida a IQ3_XXS con un 20 % menos de memoria de GPU. Escribe las respuestas un 16 % más despacio que Q2_0 en una RTX 3060 de 12 GB y un 21 % más despacio en una R9700. |
**La próxima vez**, vuelve a ejecutar `START-HERE.bat` (o `./setup.sh`). Arranca enseguida y no descarga nada dos
veces. Cierra su ventana para detener el modelo. `UPDATE.bat` (`./update.sh`) actualiza Strata sin arrancarlo.
Actualizaciones, Docker, varias tarjetas, dónde se guardan los archivos y todas las opciones: [docs/INSTALL.md](docs/INSTALL.md).
Gyro funciona en tarjetas NVIDIA y AMD (en AMD es experimental) en tarjetas Intel Arc con Linux (probado en una Arc A750) y, desde 0.2.2, en Mac con Apple Silicon (Gyro-S en un Mac mini de 32 GB: 9 tokens/s, con los expertos leídos del SSD). Detalles y mediciones:
[docs/GYRO.md](docs/GYRO.md). Arráncalo con `strata launch --model gyro-s`.
## ¿Qué modelo elijo?
### Los demás tamaños
El instalador te recomienda uno según tu RAM. El mismo modelo viene en varios tamaños, más o menos comprimidos. Los
tamaños pequeños son más rápidos. Los grandes son algo más inteligentes.
| Tu RAM | Elige | Por qué |
| Tu memoria (RAM + VRAM) | Elige | Notas |
| --- | --- | --- |
| **32 GB** | **Coder** | cabe en 32 GB y está hecho para programar (con una tarjeta de 24 GB también funcionan Q2_0 e IQ2_XS) |
| **48 GB** | **IQ2_XS** (o Q2_0, el más rápido) | los tamaños más grandes no caben |
| **64 GB** | **IQ2_XS** (recomendado), o IQ3_XXS / IQ3_S | caben todos los tamaños; IQ3_S es el mejor y el más lento |
| **96 GB o más** | **IQ3_S**, o el UD-IQ4_XS de Unsloth (~4 bits) | espacio para los tamaños más grandes con todo lo demás abierto |
| **32 GB** | **Gyro-S**, o el **[Coder](docs/MODELS.md#coder)** para código | El Coder necesita 32 GB de RAM y conserva la mitad de los expertos, así que es más flojo fuera del código |
| **40 GB** | **[Swift 1.5](docs/MODELS.md#swift-15)**IQ2_XS (39,2 GB), o **Gyro-M** (35 GB) | Swift piensa menos tiempo antes de responder. Gyro-M se acerca a la calidad de IQ3_XXS, pero escribe las respuestas un 16-21 % más despacio que Q2_0 |
| **48 GB** | **IQ3_XXS** (47,0 GB) | Más lento, mejor |
| **64 GB** | **IQ3_S** (54,8 GB), el original o Swift 1.5 | El más lento, el mejor. Iguala al modelo completo en las pruebas publicadas |
| **96 GB o más** | **UD-IQ4_XS** de Unsloth (unos 72 GB de RAM) hasta **Q8_0** (unos 136 GB), de 4 a 8 bits | Más cerca del modelo completo. Con menos RAM, Strata lee una parte desde el SSD, que es mucho más lento |
- **[Coder](docs/MODELS.md#coder):** una versión para programar a la que se le quitó la mitad de los expertos. Alcanza
el 91% de la puntuación SWE-bench Verified del modelo completo (medido por sus autores) y cabe en 32 GB de RAM. Es
más flojo fuera del código, también con chino y otros textos CJK (#438). Para eso, elige Q2_0, IQ2_XS o IQ3_S, que
conservan todos los expertos.
- **[Swift 1.5](docs/MODELS.md#swift-15):** un ajuste fino que piensa mucho menos tiempo antes de responder. Recibes
la respuesta antes, con más o menos la misma calidad.
- **[Unsloth UD-IQ4_XS](docs/MODELS.md#unsloth-ud-iq4_xs):** la versión de ~4 bits de Unsloth, entre IQ3_S y
UD-Q4_K_XL en calidad. Una descarga de 94 GB. Con menos de ~80 GB de RAM, Strata lee una parte desde el SSD
mientras responde, así que ahí es más lento (un SSD NVMe ayuda).
- **[Unsloth UD-Q4_K_XL](docs/MODELS.md#unsloth-ud-q4_k_xl-experimental)** (experimental): el más parecido al modelo
completo. Pero Strata lee la mayor parte desde el SSD mientras responde, así que solo escribe 7-8.5 tokens/s en un
PC con 64 GB.
- **[OrcaRouter's Uncensored IQ3_XXS](docs/MODELS.md#orcarouter-uncensored-iq3_xxs):** se instala a mano. No está en
el menú del instalador.
Con menos RAM de la que indica una fila también puede funcionar: Strata pasa solo a su
[modo de poca RAM](docs/MODELS.md#a-big-graphics-card-and-little-ram). Espera que sea más lento, y mucho más lento con una
tarjeta gráfica pequeña.
Tamaños, descargas y qué cabe dónde: [docs/MODELS.md](docs/MODELS.md). Para añadir otro modelo más adelante, ejecuta
`SETUP.bat` (Linux: `./setup.sh --setup`).
Más opciones:
## Cómo usarlo
- **[Swift 1.5](docs/MODELS.md#swift-15)** es un ajuste fino que piensa menos tiempo antes de responder, con una calidad y un uso de RAM casi iguales.
- **[NVFP4](docs/NVFP4.md)** es la versión de 4 bits de NVIDIA para tarjetas NVIDIA. Necesita unos 80 GB de RAM para tener todos los expertos en RAM.
- **[Qwen3.6-35B-A3B](docs/QWEN36.md)** es un modelo más pequeño y distinto que lee imágenes y puede atender varias peticiones a la vez.
- **[El Uncensored IQ3_XXS de OrcaRouter](docs/MODELS.md#orcarouter-uncensored-iq3_xxs)** se configura a mano.
<p align="center"><img src="docs/media/runpagoda.png" width="900" alt="La pestaña Monitor de la aplicación de Strata junto a un agente de programación"><br>
<sub>El <b>Monitor</b> de la aplicación de Strata (izquierda) mientras un agente de programación escribe el jardín de la pagoda del vídeo (derecha)</sub></p>
Todos los tamaños, descargas y lo que cabe en cada equipo: [docs/MODELS.md](docs/MODELS.md).
- **En el navegador:** abre `http://127.0.0.1:8080`. Tiene **Chat**, un **Monitor** en vivo del modelo y de tu
GPU/CPU/RAM, y **About** con los ajustes y las direcciones.
- **Tus aplicaciones y agentes de programación:** añade un proveedor "compatible con OpenAI" con la URL base
**`http://127.0.0.1:8080/v1`**. Sirve cualquier clave de API y cualquier nombre de modelo.
- Aplicaciones que usan la API de Anthropic: `http://127.0.0.1:8080/v1/messages` (Claude Code:
`ANTHROPIC_BASE_URL=http://127.0.0.1:8080`).
- Codex CLI y otras aplicaciones que usan la API Responses de OpenAI: `/v1/responses`
- **Razonamiento:** elige **off, low, medium o high** en el menú del chat o en el "reasoning effort" de tu aplicación.
Off es lo más rápido. High es lo mejor para preguntas difíciles.
- **Imágenes:** responde que sí a "Images?" en la instalación. Luego haz clic en **Picture** en el chat, o adjunta
imágenes en tu aplicación. Las tarjetas AMD leen imágenes en Linux a través del procesador; en Windows todavía no.
- **Desde tu móvil u otro PC:** `START-HERE.bat --setup --host 0.0.0.0 --api-key <secret>`. Pon siempre una clave.
- **Una petición cada vez:** por defecto Strata responde una petición y las demás esperan. Para responder varias a la
vez, pon `"parallel": 2` ([BATCHING.md](docs/BATCHING.md)). En una tarjeta de 12 GB, cada respuesta va más lenta.
- **Prompts largos:** Strata lee entero el primer mensaje de un chat, más o menos 1 minuto por cada 30,000 tokens. Los
mensajes siguientes empiezan en segundos.
### DeepSeek V4.1-Flash (en el Terminal, desde 0.2.4)
Más: [dónde se guardan tus chats](docs/INSTALL.md#where-things-are-stored), [la API](docs/DETAILS.md#using-it).
[DeepSeek V4.1-Flash](https://huggingface.co/ISTA-DASLab/DeepSeek-V4.1-Flash-1017-GSQ-RCO-GGUF) es otro modelo, mucho más grande. ISTA-DASLab hizo los tres conjuntos de abajo. Strata los ejecuta en un PC con una tarjeta gráfica de 12 GB y de 46 a 64 GB de RAM, y lee el resto de tu SSD. Funciona en tarjetas NVIDIA RTX de las series 30 a 50 y en la Radeon AI PRO R9700. Usa su propio paquete de motor, `strata-dsv41`, que `strata launch` descarga cuando eliges DeepSeek. La app de escritorio todavía no lo ofrece.
## ¿Algo salió mal?
| Conjunto | Descarga | Calidad frente al modelo original | Responde a |
| --- | ---: | --- | --- |
| **IQ1_M** (1,75 bits) | 239 GB | KL 0,217, perplejidad 3,80 (+21 %); código +8,9 %, tareas de agente +10,2 %; más débil con prosa inglesa corriente. Según la ficha del modelo de ISTA-DASLab, conserva el 98,7 % del promedio de tareas del modelo base (AIME25, GPQA-Diamond, LiveCodeBench v6). | aún sin medir |
| **IQ2_XXS** (2.00 bits) | 257 GB | KL 0,170, perplejidad 3,63 (+15,5 %); código +8,6 %, tareas de agente +5,3 % | de 6 a 9 tokens/s en una RTX 3060, unos 5 en una RTX 5070 |
| **IQ2_XS** (2.25 bits) | 274 GB | KL 0,107, perplejidad 3,37 (+7,3 %); código +3,2 %, tareas de agente +2,2 % | unos 11 tokens/s en una Radeon AI PRO R9700 |
| **IQ4_NL** (tabla Engram, no es un tercer modelo) | 111 GB, ya incluidos en las tres descargas de arriba | La tabla de consulta Engram de 4 bits que el modelo lee de la RAM o del SSD. Los tres conjuntos usan este único archivo (`DeepSeek-V4.1-Flash-Engram-IQ4_NL.gguf`). | n/a |
- **Mi PC se congeló la primera vez que arrancó Strata.** Es normal mientras carga el modelo. Espera y no cierres la
ventana. ¿Sigue congelado después de 10 minutos? Reinicia el PC, cierra otros programas y vuelve a intentarlo, o
elige un tamaño más pequeño.
- **Se detuvo durante la descarga o la instalación.** Vuelve a ejecutar `START-HERE.bat` (o `./setup.sh`). Sigue
donde se detuvo.
- **Va muy lento y la luz del disco no deja de parpadear, o dice "the engine stopped unexpectedly".** Tu PC no tiene
suficiente RAM libre. Cierra otros programas (los navegadores usan mucha), o elige un tamaño más pequeño (Q2_0 o
IQ2_XS).
- **Dice que el puerto 8080 ya está en uso.** Strata ya está en marcha. Busca su ventana.
4x Radeon AI PRO R9700: unos 31 tokens/s, de 55 a 69 con DSpark.
Más problemas y sus soluciones: [docs/TROUBLESHOOTING.md](docs/TROUBLESHOOTING.md). ¿Sigues atascado? Abre un
[issue](https://github.com/Niko1221/Strata/issues) y adjunta `strata-<model>.log` de la carpeta de Strata. ¿Has
encontrado un problema de seguridad? Infórmalo en privado: [SECURITY.md](SECURITY.md).
Inícialo con `strata launch --model ist-das-2.00bpw` (o `ist-das-1.75bpw`, `ist-das-2.25bpw`). Detalles, velocidades por tarjeta e instalación manual: [docs/DEEPSEEK41.md](docs/DEEPSEEK41.md).
## ¿Qué velocidad tiene?
Medido con una tarjeta gráfica de 12 GB (RTX 5070, Ryzen 5 7600, 64 GB de RAM). Resultados con otras tarjetas:
[velocidad de cada tamaño](docs/MODELS.md#how-fast-is-each-size), [resultados de la comunidad](docs/COMMUNITY_BENCHMARKS.md).
| Modelo | Las respuestas aparecen a | Lee tu prompt a |
Las barras usan la misma escala en las dos tablas. Una tarjeta con más VRAM es más rápida. Una RTX 3090 (24 GB) debería
escribir unos 100-140 tokens por segundo. La versión del motor detrás de cada cifra está en [DETAILS.md](docs/DETAILS.md#speed-measured).
## Uso
### En la aplicación
La aplicación tiene un **Chat**, un **Monitor** del modelo y de tu GPU, CPU y RAM, una página de **Modelos** y los
**Ajustes**. El chat puede trabajar en una carpeta de tu PC. En modo **Manual** pregunta antes de cambiar un archivo o
ejecutar un comando. Escribe `/loop 10m <what to do>` para repetir un mensaje cada 10 minutos mientras el chat esté abierto.
El chat puede buscar en la web con DuckDuckGo (o Bing), sin cuenta. Puedes añadir una clave de búsqueda en **Ajustes,
Herramientas**. La búsqueda es la única función que envía algo fuera de tu PC (las palabras que buscas).
Para ejecutar dos modelos, activa **Ejecutar varios modelos** y dale a cada modelo su propia tarjeta de la misma marca.
<p align="center"><img src="docs/media/app-chat.png" width="900" alt="The Strata app: a chat where the agent built a small voxel pagoda garden, with the page open in the preview on the right"><br>
<sub>Un chat en la app de Strata: el agente escribió un pequeño jardín de pagoda de vóxeles en un solo archivo HTML, y la página se abre en la vista previa junto a la conversación</sub></p>
### En la Terminal
<p align="center"><img src="docs/media/terminal-monitor.png" width="900" alt="The Strata Terminal monitor overview while the model writes a reply"><br>
<sub><code>strata monitor</code> mientras el modelo responde: velocidad, GPU, memoria y las últimas solicitudes</sub></p>
strata update --check # is there a new engine? (strata update installs it)
strata doctor # check the installed engine files
```
`strata --help` lo lista todo. Arrancar y servir no descargan nada. Solo lo hacen la instalación, `strata launch` (cuando
eliges un modelo) y `strata update`. Para llegar a Strata desde tu móvil u otro PC, usa `strata launch --access lan`. Crea
una clave de API y no arranca en una red sin ella. No se abre ningún navegador salvo que añadas `--open`. Guía:
[docs/TERMINAL_LAUNCH.md](docs/TERMINAL_LAUNCH.md). El cliente completo: [docs/TERMINAL.md](docs/TERMINAL.md).
### Con agentes de IA
Un agente instala Strata con el comando de una línea de la opción B y ejecuta
`strata launch --model NAME --yes --no-tui`. Los pasos, las opciones y los códigos de salida están en [docs/AI_SETUP.md](docs/AI_SETUP.md),
y las reglas del repositorio en [AGENTS.md](AGENTS.md).
### Conecta tus otras aplicaciones
| Tipo de aplicación | URL base |
| --- | --- |
| Compatible con OpenAI (la mayoría de aplicaciones y agentes) | `http://127.0.0.1:8080/v1` (cualquier clave de API, cualquier nombre de modelo) |
| API de Anthropic (Claude Code y otros) | `http://127.0.0.1:8080`, sin `/v1`. Estas aplicaciones añaden `/v1/messages` por su cuenta. Claude Code: `ANTHROPIC_BASE_URL=http://127.0.0.1:8080` |
| API OpenAI Responses (Codex CLI) | `http://127.0.0.1:8080/v1/responses` ([configuración](docs/DETAILS.md#the-responses-api-and-codex-cli)) |
`strata connect` muestra los ajustes exactos para OpenCode, Codex CLI, Claude Code, oh-my-pi, pi, Hermes Agent y OpenClaw.
`strata connect opencode --write` escribe el archivo después de preguntar y guarda una copia de seguridad.
### Imágenes
Las imágenes vienen activadas por defecto en los modelos que pueden leerlas. Adjunta o pega una imagen en el chat, o envíala
desde tu aplicación. El lector de imágenes funciona en el procesador cuando tu tarjeta gráfica tiene menos de 24 GB (unos
segundos por imagen, sin quitar memoria a la tarjeta) y en la tarjeta gráfica a partir de 24 GB. La regla es la misma para
NVIDIA y AMD. Puedes desactivar las imágenes por modelo, o elegir el lector con `--vision off`, `--vision gpu` o
`--vision cpu`.
Las cifras detrás de la regla: en una Radeon AI PRO R9700, el lector de la tarjeta tardó 57 ms en una imagen de 260 tokens y
el del procesador 3,1 s. En tarjetas de 16 GB (Tesla P100 y RTX A4000, Linux), el lector de la tarjeta costó un 3-5 % de
velocidad en texto, porque ocupa unos 1,4 GB que de otro modo usaría el modelo. En un Mac lee la GPU, tenga la memoria que tenga (0,45 s para una imagen de 260 tokens en un M4 de 32 GB, unos 1,2 GB de la memoria compartida);
el procesador tarda unos 50 s por imagen, así que solo lee con `--vision cpu`.
### Conviene saber
- Elige el nivel de razonamiento (**desactivado, bajo, medio o alto**) en el menú del chat o en "reasoning effort" de tu aplicación. Desactivado es lo más rápido.
- Strata responde a una petición a la vez por defecto. Para varias a la vez, usa `--parallel N` en `strata launch` o
`"parallel": 2` en la configuración ([BATCHING.md](docs/BATCHING.md)). En una tarjeta de 12 GB, esto hace más lenta cada respuesta.
- Strata lee entero el primer mensaje de un chat, unos 1 minuto por cada 30.000 tokens. Los mensajes siguientes empiezan en segundos.
- Dónde se guardan tus chats y la API completa: [docs/INSTALL.md](docs/INSTALL.md#where-things-are-stored), [docs/DETAILS.md](docs/DETAILS.md#using-it).
## Compilar desde el código fuente
<details>
<summary>Windows, Linux y Mac desde el repositorio</summary>
[Descarga Strata](https://github.com/Niko1221/Strata/archive/refs/heads/main.zip) y descomprímelo, o haz `git clone`.
**Windows:** haz doble clic en `START-HERE.bat`. **Linux:** ejecuta `./setup.sh` en la carpeta de Strata.
**Mac (Apple Silicon, macOS 15 o posterior):** instala una vez las Command Line Tools de Apple con `xcode-select --install`
(no hace falta Xcode). Abre la Terminal en la carpeta de Strata y ejecuta `./setup.sh`. El script compila el motor para Mac
en tu Mac (unos 7 minutos), hace las mismas preguntas, descarga el modelo y lo arranca. La próxima vez, `./setup.sh` lo
arranca enseguida. Qué tamaño cabe en tu Mac: [docs/MAC.md](docs/MAC.md).
El instalador detecta tu tarjeta y prepara el motor adecuado. Pregunta qué modelo y qué tamaño quieres, cuánto contexto
(cuánto texto mantiene el modelo en mente) y si debe leer imágenes. Pulsa Enter para aceptar la respuesta recomendada.
Luego descarga el modelo (unos 70 GB) y lo arranca. Si la descarga se detiene, vuelve a ejecutar el script y continúa. Tu
navegador abre la página de Strata en `http://127.0.0.1:8080`.
> Mientras el modelo arranca, tu PC puede ir lento o dejar de responder durante 1-3 minutos (más tiempo la primera vez).
> Strata carga 35-55 GB en tu RAM y bloquea una parte para la tarjeta gráfica. Espera y no cierres la ventana.
La próxima vez, ejecuta de nuevo `START-HERE.bat` (o `./setup.sh`). Arranca enseguida y no descarga nada dos veces. Cierra
su ventana para detener el modelo. `UPDATE.bat` (`./update.sh`) actualiza Strata sin arrancarlo. Actualización, Docker,
varias tarjetas, dónde van los archivos y todas las opciones: [docs/INSTALL.md](docs/INSTALL.md).
Para medir tu PC, ejecuta `BENCH.bat` (`./bench.sh`). Tarda 5-10 minutos y escribe un informe que puedes compartir
**Opcional: más velocidad en una tarjeta cuando todo el modelo cabe en la RAM.** Dos ajustes que se activan a mano, de
RubenTorney (#1904, #1905). Ayudan cuando usas una sola tarjeta gráfica y tu RAM guarda todos los expertos, por ejemplo con
Q2_0 o IQ3_XXS en un PC de 64 GB. Abre `strata-<model>.json` en la carpeta de datos de Strata ([dónde](docs/INSTALL.md#where-things-are-stored)) y añade:
-`"--adapt-async", "1"` a la lista `"args"`. Así la caché de expertos se actualiza en un hilo auxiliar en vez de detener la
respuesta. Medido en una RTX 5070 (Windows, Q2_0): +4-5 % en decodificación, +7-10 % en código y documentos largos. Las
respuestas pueden diferir un poco de las del ajuste por defecto y son iguales de una ejecución a otra.
-`"STRATA_PIN_BY_NEED": "1"` en el bloque `"env"` (Windows). Windows solo puede bloquear aproximadamente la mitad de la RAM
para la tarjeta gráfica, y este ajuste bloquea primero los expertos que la tarjeta necesita antes. Medido en la RTX 5070: +4,4 % en lectura del prompt.
```json
"args":[...,"--adapt-async","1"],
"env":{"STRATA_PIN_BY_NEED":"1"}
```
Reinicia Strata después del cambio. Para deshacerlo, quita las dos entradas.
</details>
## ¿Cómo funciona?
Los modelos como este suelen funcionar en servidores con cientos de gigabytes de memoria gráfica. Tu tarjeta gráfica
tiene 12-24 GB. Strata hace que el modelo quepa **repartiendo el trabajo por todo tu PC**. Piensa en una cocina: lo
que usas todo el rato se queda en la encimera, y el resto espera en la despensa.
Los modelos como este suelen ejecutarse en servidores con cientos de gigabytes de memoria gráfica. Tu tarjeta gráfica tiene
12-24 GB. Strata hace que el modelo quepa repartiendo el trabajo por todo tu PC. Imagina una biblioteca.
<p align="center"><img src="docs/media/how-it-works.svg" width="860" alt="Los 24,576 expertos del modelo: los más usados en la tarjeta gráfica, todos en la RAM y una tabla de consulta en el SSD"></p>
- El modelo es un equipo de 24.576 pequeños especialistas ("expertos"), como 24.576 libros de consulta. Cada palabra necesita solo 10 de ellos.
- **Tu tarjeta gráfica es el escritorio.** Es pequeño y rápido. Los unos 4.000 libros que más se usan están abiertos encima.
- **Tu RAM son las estanterías de la sala de lectura.** Los 24.576 libros están ahí. Cuando una palabra necesita un libro que
no está en el escritorio, tu procesador lo coge enseguida de la estantería mientras la tarjeta sigue trabajando.
- **Tu SSD es el archivo.** Guarda una tabla de consulta de 29 GB. El modelo lee unas pocas filas por palabra, así que no
hace falta que esté en la RAM.
- **El modelo es un equipo de 24,576 pequeños especialistas ("expertos").** Cada palabra solo necesita 10 de ellos.
- **Tu tarjeta gráfica** guarda los pocos miles de expertos que más se usan. **Tu RAM** los tiene todos, y **tu
procesador** trabaja con el resto al mismo tiempo. **Tu SSD** guarda una gran tabla de consulta.
<p align="center"><img src="docs/media/how-it-works.svg" width="860" alt="The model's 24,576 experts: the busiest on the graphics card, all of them in RAM, a lookup table on the SSD"></p>
<p align="center"><img src="docs/media/guess-and-check.svg" width="860" alt="Un pequeño ayudante adivina las siguientes palabras; el modelo grande las comprueba todas a la vez y se queda con las correctas"></p>
Un pequeño ayudante también adivina las siguientes palabras. El modelo grande las comprueba todas a la vez y se queda con
las correctas. Obtienes la misma respuesta, entre 1,6 y 1,8 veces antes. Los textos largos se leen en trozos grandes, de
hasta 8.192 tokens a la vez, a más de 1.000 tokens por segundo.
- **Adivinar y comprobar:** un pequeño ayudante adivina las siguientes palabras. El modelo grande las comprueba todas
a la vez. Recibes la misma respuesta, 1.6-1.8x antes.
- **Los textos largos se leen en trozos grandes** (hasta 8,192 tokens cada vez), a más de 1,000 tokens por segundo.
<p align="center"><img src="docs/media/guess-and-check.svg" width="860" alt="A small helper guesses the next words; the big model checks them all at once and keeps the right ones"></p>
La explicación larga: [docs/HOW_IT_WORKS.md](docs/HOW_IT_WORKS.md). Cada parte y sus números:
La explicación más larga: [docs/HOW_IT_WORKS.md](docs/HOW_IT_WORKS.md). Cada parte con sus cifras:
[los detalles](docs/DETAILS.md#how-it-works) y el [artículo](docs/paper/Strata-Paper.pdf).
## ¿Algo ha ido mal?
- **Mi PC se congeló la primera vez que arrancó Strata.** Carga el modelo en la RAM. Espera y no cierres la ventana. ¿Sigue
congelado después de 10 minutos? Reinicia el PC, cierra otros programas e inténtalo de nuevo, o elige un tamaño más pequeño.
- **Se detuvo durante la descarga o la instalación.** Abre de nuevo la aplicación o `strata launch`, o vuelve a ejecutar
`START-HERE.bat` (o `./setup.sh`). Continúa donde se quedó.
- **Va muy lento, la luz del disco parpadea sin parar, o dice "the engine stopped unexpectedly".** Tu PC no tiene bastante
RAM libre. Cierra otros programas (los navegadores gastan mucha) o elige un tamaño más pequeño, como Q2_0 o IQ2_XS.
- **Dice que no hay motor para este PC.** En la aplicación, abre **Ajustes, Motor** y pulsa **Actualizar motor**. En la
Terminal, ejecuta `strata update`. `strata doctor` muestra qué archivos del motor están instalados.
- **Dice que el puerto 8080 ya está en uso.** Strata ya se está ejecutando. Busca su ventana.
Más problemas y soluciones: [docs/TROUBLESHOOTING.md](docs/TROUBLESHOOTING.md). ¿Sigues atascado? Abre una
[incidencia](https://github.com/Niko1221/Strata/issues). En la aplicación, adjunta el archivo de **Ajustes, Motor,
Diagnóstico, Ver registros, Guardar registros**. En la Terminal, pega la salida de `strata doctor`. Si lo ejecutas desde el
código fuente, adjunta `strata-<model>.log` de la carpeta de Strata. Para un problema de seguridad, avísanos en privado:
[SECURITY.md](SECURITY.md).
## Créditos y licencia
El modelo es [Qwen3.8-Flash-Next](https://huggingface.co/Qwen/Qwen3.8-Flash-Next), del equipo de Qwen. Lo
y Unsloth. Strata usa partes de [llama.cpp / ggml](https://github.com/ggml-org/llama.cpp). Todos los créditos:
[docs/HOW_IT_WORKS.md](docs/HOW_IT_WORKS.md#credits). Strata es de código abierto con la [licencia MIT](LICENSE).
Algunas partes y cada modelo tienen sus propias licencias ([cuáles](docs/HOW_IT_WORKS.md#license)).
[docs/HOW_IT_WORKS.md](docs/HOW_IT_WORKS.md#credits). Strata es de código abierto con la [licencia MIT](LICENSE). Algunas
partes y todos los modelos tienen sus propias licencias ([cuáles](docs/HOW_IT_WORKS.md#license)).
## Apoya a Strata
## Contribuir
Strata es libre y de código abierto. Si te resulta útil, puedes apoyar su desarrollo:
¿Has encontrado un error, o quieres enviar un cambio o un benchmark? Lee primero [CONTRIBUTING.md](CONTRIBUTING.md). Explica
qué incluir para que otros puedan comprobar tu trabajo.
<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>
## Apoya Strata
Strata es gratis y tiene licencia MIT. Lo construyo y lo pruebo en el hardware que consigo. Si te funciona bien en tu PC, un café me ayuda
a seguir probando tarjetas nuevas y a arreglar lo que nos cuentas. Sin presión. Usarlo y contar lo que encuentras ayuda igual de mucho.
<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>
¿Te interesa patrocinar Strata? Escríbeme a través de [GitHub](https://github.com/Niko1221).
<p align="center"><b>Faites tourner un modèle d'IA de 125 milliards de paramètres sur votre propre PC de jeu</b><br>
Carte graphique NVIDIA ou AMD (12 Go ou plus) · Windows ou Linux · gratuit et open source</p>
<h3 align="center">Faites tourner un modèle de pointe sur votre propre PC de jeu</h3>
<p align="center"><a href="https://github.com/Niko1221/Strata/releases/download/v0.1.10/Pagoda.mp4"><img src="docs/media/pagoda-preview.webp" width="720" alt="Un jardin de pagode en voxels écrit par le modèle de Strata, qui tourne dans le navigateur"></a><br>
<p align="center">Compatible avec NVIDIA, AMD, Intel · Windows, Linux, Mac · gratuit et open source</p>
<p align="center"><a href="https://github.com/Niko1221/Strata/releases/download/v0.1.10/Pagoda.mp4"><img src="docs/media/pagoda-preview.webp" width="720" alt="A voxel pagoda garden that Strata's model wrote, running in the browser"></a><br>
<sub>Un jardin de pagode en voxels, généré en une seule requête sur une RTX 5070 avec Strata (IQ3_S, contexte de 128K) ·
Strata fait tourner **[Qwen3.8-Flash-Next](https://huggingface.co/Qwen/Qwen3.8-Flash-Next)** sur un PC normal. C'est
un grand modèle d'IA, très capable, qui demande d'habitude un serveur. Il discute, écrit du code, lit des images et
fonctionne avec vos applications et vos agents de code. Rien ne quitte votre PC.
Strata fait tourner **[Qwen3.8-Flash-Next](https://huggingface.co/Qwen/Qwen3.8-Flash-Next)**, un modèle de 125 milliards
de paramètres qui demande normalement un serveur, sur un PC ordinaire. Il discute, écrit du code, lit des images et
fonctionne avec vos applications et vos agents de code. Vos conversations et vos fichiers restent sur votre PC.
## Quelle est sa vitesse ?
**Nouveau ici ?** Regardez [ce qu'il vous faut](#ce-quil-vous-faut), puis [installez l'application](#option-a--lapplication-strata).
Nous l'avons mesurée sur deux PC de jeu ordinaires. Un token correspond à environ ¾ d'un mot.
- **Tourne sur un PC de jeu.** 32 Go de RAM et de mémoire graphique réunis suffisent pour commencer.
- **Fonctionne avec vos outils.** Il parle les API d'OpenAI et d'Anthropic, donc Claude Code, Codex CLI, OpenCode et d'autres agents peuvent s'y connecter.
- **Lit les images.** C'est activé par défaut.
- **Gratuit.** Licence MIT, pas de compte, pas de cloud.
- **Privé.** Aucune télémétrie. Ce que Strata télécharge, quand il utilise internet et comment il vérifie les téléchargements est expliqué dans [docs/SAFETY.md](docs/SAFETY.md).
- **Écrit les réponses :** la vitesse à laquelle la réponse s'affiche dans une courte discussion. 60 tokens par seconde, c'est plus rapide que votre lecture.
- **Lit votre prompt :** la vitesse à laquelle il absorbe ce que vous envoyez (ici un document, du code ou un historique de discussion de 32K tokens).
<table>
<tr><th>NVIDIA : RTX 5070 (12 Go), Ryzen 5 7600, 64 Go de RAM</th><th>AMD : RX 9070 XT (16 Go), Ryzen 9 3900X, 47 Go de RAM</th></tr>
<tr><td>
| Taille | Écrit les réponses | Lit votre prompt |
| --- | ---: | ---: |
| **Q2_0** | 94 tokens/s | 2,650 tokens/s |
| **IQ2_XS** | 79 tokens/s | 2,090 tokens/s |
| **IQ3_XXS** | 62 tokens/s | 1,750 tokens/s |
| **IQ3_S** | 53 tokens/s | 1,620 tokens/s |
| **Coder** | 55 tokens/s | 2,180 tokens/s |
</td><td>
| Taille | Écrit les réponses | Lit votre prompt |
| --- | ---: | ---: |
| **Q2_0** | 60 tokens/s | 1,160 tokens/s |
| **IQ2_XS** | 52 tokens/s | 1,110 tokens/s |
| **Coder** | 44 tokens/s | 1,420 tokens/s |
</td></tr>
</table>
NVIDIA : Q2_0 avec le moteur 0.1.36, les autres lignes avec 0.1.26 (réponses de 4K, prompts de 32K). Les tableaux
complets sont dans [DETAILS.md](docs/DETAILS.md#speed-measured). Une carte avec plus de VRAM va plus vite : une
RTX 3090 (24 Go) devrait écrire environ 100-140 tokens par seconde. Longues discussions et autres cartes :
[vitesse de chaque modèle](docs/MODELS.md#how-fast-is-each-size), [résultats de la communauté](docs/COMMUNITY_BENCHMARKS.md).
<p align="center"><a href="https://buymeacoffee.com/strataengine"><img src="https://cdn.buymeacoffee.com/buttons/v2/default-yellow.png" alt="Buy Me A Coffee" height="50"></a><br>
<sub>Strata est gratuit. S'il tourne bien sur votre PC, un café aide à poursuivre le travail.</sub></p>
Version 0.2.3 · moteur 0.1.48
## Ce qu'il vous faut
| | |
**Il vous faut au moins 32 Go de RAM et de mémoire graphique (VRAM) réunis.** Sur un Mac ou un Ryzen AI Max, c'est la
mémoire unifiée. Plus de mémoire permet un modèle plus gros ou plus rapide.
- Un PC de 32 Go avec une carte de 12 à 16 Go fait tourner le Coder.
- Avec 64 Go de RAM, toutes les tailles fonctionnent.
| Votre ordinateur | Détails |
| --- | --- |
| **Carte graphique**| **NVIDIA** GeForce RTX série 20, 30, 40 ou 50, ou **AMD** Radeon RX 7900 XT / XTX, RX 7800 XT / 7700 XT, RX 9060 XT, RX 9070 / 9070 XT, Radeon AI PRO R9700 ou série RX 6800 / 6900. Il faut **12 Go de VRAM ou plus**. |
| **RAM**| 32 Go ou plus. Votre RAM décide [quel modèle](#quel-modèle-choisir-) tient. Avec 64 Go, toutes les tailles tournent. |
| **Disque** | Environ 80 Go libres. Prenez un SSD si possible : le premier démarrage est bien plus rapide. |
| **Système** | Windows 10 / 11 ou Linux, avec un pilote graphique NVIDIA ou AMD à jour. |
| **NVIDIA** GeForce RTX série 20, 30, 40, 50 (12 Go ou plus) | Installez l'application ou le Terminal. |
| **AMD**Radeon RX 6800 et plus récent, Radeon AI PRO R9700 (12 Go ou plus) | [Cartes AMD](docs/AMD_HIP.md) |
- **Intel Arc**, compilé depuis les sources sous Linux : [Intel Arc](docs/INTEL_ARC.md).
- **AMD Ryzen AI Max (Strix Halo)**, compilé depuis les sources sous Linux : [Strix Halo](docs/STRIX_HALO.md).
- **Processeurs plus anciens sans AVX2** : ça marche, mais lentement. [Older CPUs](docs/INSTALL.md#older-cpus-experimental).
La liste complète : [docs/INSTALL.md](docs/INSTALL.md#what-you-need).
Deux ou trois cartes de la même marque peuvent se partager un modèle ([multi-GPU](docs/MULTI_GPU.md)). Strata ne répartit
jamais un modèle entre des cartes NVIDIA et AMD. Liste complète : [docs/INSTALL.md](docs/INSTALL.md#what-you-need).
## Installation
### Laissez votre IA l'installer
Choisissez une option. Elles lancent toutes le même moteur et utilisent les mêmes modèles.
Vous utilisez un assistant de code IA (Claude Code, Cursor, Codex, GitHub Copilot, ...) ? Collez-lui ceci :
### Option A : l'application Strata
Le plus simple. L'application contient le moteur : installez-la, choisissez un modèle, discutez. Téléchargez le fichier
correspondant à votre système et à votre carte graphique depuis la [page des versions](https://github.com/Niko1221/Strata/releases/latest).
Vous ne savez pas lequel prendre ? Lisez le paragraphe sous le tableau.
| Système | Votre carte graphique | Fichier à télécharger |
| --- | --- | --- |
| **Windows 10 / 11** | NVIDIA RTX, pilote 580 ou plus récent | [`Strata-0.2.3-windows-cuda13.exe`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.3-windows-cuda13.exe) |
| | NVIDIA, carte plus ancienne ou pilote antérieur à 580 | [`Strata-0.2.3-windows-cuda12.exe`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.3-windows-cuda12.exe) |
| | AMD Radeon série RX 9000 ou Radeon AI PRO R9700 | [`Strata-0.2.3-windows-hip.exe`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.3-windows-hip.exe) |
| | AMD Radeon série RX 7000 ou Ryzen AI Max (Strix Halo) | [`Strata-0.2.3-windows-hip-rdna3.exe`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.3-windows-hip-rdna3.exe) |
| **Linux** | NVIDIA RTX, pilote 580 ou plus récent | [`Strata-0.2.3-linux-nvidia-cuda13.AppImage`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.3-linux-nvidia-cuda13.AppImage) |
| | NVIDIA, carte plus ancienne ou pilote antérieur à 580 | [`Strata-0.2.3-linux-nvidia-cuda12.AppImage`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.3-linux-nvidia-cuda12.AppImage) |
| | AMD Radeon série RX 9000 ou Radeon AI PRO R9700 | [`Strata-0.2.3-linux-amd-rx9070.AppImage`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.3-linux-amd-rx9070.AppImage) |
| | AMD Radeon série RX 7000 | [`Strata-0.2.3-linux-amd-rx7900.AppImage`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.3-linux-amd-rx7900.AppImage) |
| | AMD Ryzen AI Max (Strix Halo) | [`Strata-0.2.3-linux-amd-strixhalo.AppImage`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.3-linux-amd-strixhalo.AppImage) |
| **Mac** (Apple Silicon, macOS 15 ou plus récent) | intégrée | [`Strata-0.2.3-mac.dmg`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.3-mac.dmg) |
**Si vous hésitez :** pour NVIDIA, prenez `cuda13` si la version de votre pilote est 580 ou plus récente, sinon `cuda12`.
`cuda12` convient aux cartes plus anciennes et aux pilotes plus anciens. Les fichiers `hip` et `amd` sont pour les cartes
AMD. Les autres cartes AMD (RX 6000) n'ont pas encore d'application prête à l'emploi : voir
[Cartes AMD](docs/AMD_HIP.md).
Ensuite :
- **Windows :** lancez le `.exe`. Il s'installe pour votre utilisateur et ne demande pas de droits d'administrateur. Si
Windows affiche « Windows a protégé votre ordinateur », choisissez **Informations complémentaires**, puis **Exécuter
quand même** (l'installateur n'est pas encore signé).
- **Linux :** rendez le fichier exécutable (`chmod +x`) et lancez-le. Il a besoin de FUSE 2 : `sudo apt install libfuse2`
(Ubuntu 22.04) ou `libfuse2t64` (Ubuntu 24.04 et plus récent).
- **Mac :** ouvrez le `.dmg` et faites glisser Strata dans Applications.
Sur le premier écran, l'application analyse votre PC et recommande un modèle adapté. Appuyez sur Installer. Le modèle est un
gros téléchargement (environ 40 à 70 Go) qui reprend là où il s'est arrêté en cas d'interruption. Quand vous fermez la
fenêtre, Strata et le moteur s'arrêtent.
### Option B : le Terminal (une ligne)
Le Terminal est la commande `strata`, pensée pour les scripts, les serveurs et les agents d'IA.
Linux et Mac :
```sh
curl -fsSL https://github.com/Niko1221/Strata/releases/latest/download/install.sh | sh
Le script analyse votre PC, télécharge le Terminal, vérifie son empreinte SHA-256 et le décompresse dans votre dossier
personnel. Il ne demande pas de droits d'administrateur. Sous Windows, ouvrez ensuite une nouvelle fenêtre de terminal.
Puis lancez Strata :
```sh
strata launch
```
Il pose une question à la fois : modèle, contexte, images, qui peut se connecter. Ensuite il télécharge le moteur pour votre
carte graphique (signé, et lancé à titre d'essai avant usage) ainsi que le modèle, puis commence à servir. Pour les scripts
et les agents d'IA, utilisez des options :
```sh
strata launch --model q2_0 --yes --no-tui
```
Pour lire le script d'installation avant de l'exécuter, téléchargez-le d'abord avec `curl -fsSL <address> -o install.sh`,
lisez-le, puis lancez `sh install.sh`. Tous les écrans et toutes les options : [docs/TERMINAL_LAUNCH.md](docs/TERMINAL_LAUNCH.md).
### Option C : laissez votre IA l'installer
Collez ceci dans Claude Code, Cursor, Codex, GitHub Copilot ou un autre assistant de programmation :
```text
Set up Strata on this PC for me: https://github.com/Niko1221/Strata - follow docs/AI_SETUP.md in that repository.
```
Il vérifie votre carte graphique, votre RAM et votre disque, et choisit le modèle qui convient. Ensuite il l'installe,
le démarre et vous explique comment connecter vos applications. Les outils d'IA peuvent aussi installer, démarrer et
arrêter Strata grâce à son [serveur MCP](docs/MCP_SERVER.md).
L'assistant vérifie votre carte graphique, votre RAM et votre disque, choisit le modèle adapté, l'installe, le démarre et
vous explique comment connecter vos applications. Les outils d'IA peuvent aussi installer, démarrer et arrêter Strata via son
[serveur MCP](docs/MCP_SERVER.md).
### Ou faites-le vous-même
Vous préférez le compiler vous-même ? Voir [Compiler depuis les sources](#compiler-depuis-les-sources).
[Téléchargez Strata](https://github.com/Niko1221/Strata/archive/refs/heads/main.zip) et décompressez-le (ou faites un `git clone`).
**Windows :** double-cliquez sur **`START-HERE.bat`**. **Linux :** lancez **`./setup.sh`** dans le dossier Strata.
## Modèles
Les étapes sont les mêmes pour NVIDIA et AMD. L'installateur détecte votre carte et installe le bon moteur pour elle.
Il vous pose quelques questions :
L'application et `strata launch` recommandent un modèle selon votre mémoire. C'est un seul modèle en plusieurs tailles,
plus ou moins compressé. Les petites tailles sont plus rapides. Les grandes sont un peu plus intelligentes.
- quel modèle et quelle taille,
- combien de contexte (la quantité de texte que le modèle garde en tête),
- s'il doit lire les images.
### Peu de mémoire ? Essayez Gyro
Appuyez sur Entrée à chaque fois pour garder la réponse recommandée. Ensuite il télécharge le modèle (environ 70 Go)
et le démarre. Si le téléchargement s'arrête, relancez-le : il reprend là où il s'était arrêté. Votre navigateur
ouvre l'application Strata à l'adresse `http://127.0.0.1:8080`.
[Gyro](https://huggingface.co/agentionai/Qwen3.8-Flash-Next-Gyro-GGUF) (par agentionai) est la façon la plus légère de faire tourner ce modèle.
> **Pendant le démarrage du modèle, votre PC peut ralentir ou ne plus répondre pendant 1-3 minutes** (plus longtemps la première fois).
> Strata charge 35-55 Go dans votre RAM et en réserve une partie pour la carte graphique. C'est normal. Attendez, et
> ne fermez pas la fenêtre. La fenêtre montre ce que fait Strata.
| Taille | Mémoire nécessaire (RAM + VRAM) | Téléchargement | Ce que c'est |
| --- | ---: | ---: | --- |
| **Gyro-S** | 27,6 Go | 58,5 Go | Pour les PC où rien d'autre ne tient. La qualité est inférieure à celle d'IQ2_XS. |
| **Gyro-M** | 35 Go | 92,0 Go | À peu près la qualité d'IQ3_XXS avec environ 20 % de mémoire GPU en moins. Il écrit les réponses 16 % plus lentement que Q2_0 sur une RTX 3060 de 12 Go et 21 % plus lentement sur une R9700. |
**La fois suivante**, relancez `START-HERE.bat` (ou `./setup.sh`). Il démarre tout de suite et ne télécharge rien
deux fois. Fermez sa fenêtre pour arrêter le modèle. `UPDATE.bat` (`./update.sh`) met Strata à jour sans le démarrer.
Mises à jour, Docker, plusieurs cartes, emplacement des fichiers et toutes les options : [docs/INSTALL.md](docs/INSTALL.md).
Gyro fonctionne sur les cartes NVIDIA et AMD (expérimental sur AMD) sur les cartes Intel Arc sous Linux (testé sur une Arc A750) et, depuis la 0.2.2, sur les Mac Apple Silicon (Gyro-S sur un Mac mini de 32 Go : 9 tokens/s, experts lus sur le SSD). Détails et mesures :
[docs/GYRO.md](docs/GYRO.md). Lancez-le avec `strata launch --model gyro-s`.
## Quel modèle choisir ?
### Les autres tailles
L'installateur en recommande un selon votre RAM. Le même modèle existe en plusieurs tailles, plus ou moins
compressées. Les petites tailles sont plus rapides. Les grandes sont un peu plus intelligentes.
| **32 Go** | **Coder** | il tient dans 32 Go et il est fait pour le code (avec une carte de 24 Go, Q2_0 et IQ2_XS tournent aussi) |
| **48 Go** | **IQ2_XS** (ou Q2_0, le plus rapide) | les plus grandes tailles ne tiennent pas |
| **64 Go** | **IQ2_XS** (recommandé), ou IQ3_XXS / IQ3_S | toutes les tailles tiennent ; IQ3_S est le meilleur et le plus lent |
| **96 Go ou plus** | **IQ3_S**, ou l'UD-IQ4_XS d'Unsloth (~4 bits) | de la place pour les plus grandes tailles, avec tout le reste ouvert |
| **32 Go** | **Gyro-S**, ou le **[Coder](docs/MODELS.md#coder)** pour le code | Le Coder demande 32 Go de RAM et garde la moitié des experts, il est donc moins bon hors du code |
| **40 Go** | **[Swift 1.5](docs/MODELS.md#swift-15)**IQ2_XS (39,2 Go), ou **Gyro-M** (35 Go) | Swift réfléchit moins longtemps avant de répondre. Gyro-M approche la qualité d'IQ3_XXS, mais il écrit les réponses 16 à 21 % plus lentement que Q2_0 |
| **64 Go** | **IQ3_S** (54,8 Go), l'original ou Swift 1.5 | Le plus lent, le meilleur. Il égale le modèle complet dans les tests publiés |
| **96 Go ou plus** | **UD-IQ4_XS** d'Unsloth (environ 72 Go de RAM) jusqu'à **Q8_0** (environ 136 Go), 4 à 8 bits | Plus proche du modèle complet. Avec moins de RAM, Strata lit une partie depuis le SSD, ce qui est beaucoup plus lent |
- **[Coder](docs/MODELS.md#coder) :** une version pour le code, avec la moitié des experts en moins. Elle atteint 91 %
du score SWE-bench Verified du modèle complet (mesuré par ses auteurs) et tient dans 32 Go de RAM. Elle est moins
bonne en dehors du code, y compris pour le chinois et les autres textes CJK (#438). Pour ces cas, prenez Q2_0,
IQ2_XS ou IQ3_S, qui gardent tous les experts.
- **[Swift 1.5](docs/MODELS.md#swift-15) :** un fine-tune qui réfléchit bien moins longtemps avant de répondre. Vous
avez la réponse plus tôt, avec à peu près la même qualité.
- **[Unsloth UD-IQ4_XS](docs/MODELS.md#unsloth-ud-iq4_xs)** : la version ~4 bits d'Unsloth, entre IQ3_S et
UD-Q4_K_XL en qualité. Un téléchargement de 94 Go. Avec moins de ~80 Go de RAM, Strata en lit une partie depuis
le SSD pendant qu'il répond, il y est donc plus lent (un SSD NVMe aide).
- **[Unsloth UD-Q4_K_XL](docs/MODELS.md#unsloth-ud-q4_k_xl-experimental)** (expérimental) : le plus proche du modèle
complet. Mais Strata en lit la plus grande partie depuis le SSD pendant qu'il répond, donc il n'écrit que
7-8.5 tokens/s sur un PC avec 64 Go.
- **[OrcaRouter's Uncensored IQ3_XXS](docs/MODELS.md#orcarouter-uncensored-iq3_xxs) :** vous l'installez à la main.
Il n'est pas dans le menu de l'installateur.
Avoir moins de RAM que ce qu'indique une ligne peut encore fonctionner : Strata passe tout seul en
[mode peu de RAM](docs/MODELS.md#a-big-graphics-card-and-little-ram). Attendez-vous à plus de lenteur, et à bien plus avec
une petite carte graphique.
Tailles, téléchargements et ce qui tient où : [docs/MODELS.md](docs/MODELS.md). Pour ajouter un autre modèle plus
- **[Swift 1.5](docs/MODELS.md#swift-15)** est une version affinée qui réfléchit moins longtemps avant de répondre, pour une qualité et un besoin en RAM à peu près identiques.
- **[NVFP4](docs/NVFP4.md)** est la version 4 bits de NVIDIA pour les cartes NVIDIA. Elle demande environ 80 Go de RAM pour garder tous les experts en RAM.
- **[Qwen3.6-35B-A3B](docs/QWEN36.md)** est un modèle plus petit et différent, qui lit les images et peut répondre à plusieurs requêtes à la fois.
- **[L'Uncensored IQ3_XXS d'OrcaRouter](docs/MODELS.md#orcarouter-uncensored-iq3_xxs)** se configure à la main.
Toutes les tailles, les téléchargements et ce qui tient où : [docs/MODELS.md](docs/MODELS.md).
### DeepSeek V4.1-Flash (dans le Terminal, depuis 0.2.4)
[DeepSeek V4.1-Flash](https://huggingface.co/ISTA-DASLab/DeepSeek-V4.1-Flash-1017-GSQ-RCO-GGUF) est un autre modèle, beaucoup plus gros. ISTA-DASLab a produit les trois versions ci-dessous. Strata les fait tourner sur un PC avec une carte graphique de 12 Go et de 46 à 64 Go de RAM, et lit le reste sur votre SSD. Il fonctionne sur les cartes NVIDIA RTX des séries 30 à 50 et sur la Radeon AI PRO R9700. Il utilise son propre paquet de moteur, `strata-dsv41`, que `strata launch` télécharge quand vous choisissez DeepSeek. L’application de bureau ne le propose pas encore.
| Version | Téléchargement | Qualité par rapport au modèle d’origine | Écrit à |
| --- | ---: | --- | --- |
| **IQ1_M** (1,75 bit) | 239 Go | KL 0,217, perplexité 3,80 (+21 %) ; code +8,9 %, tâches d’agent +10,2 % ; plus faible sur la prose anglaise courante. D’après la fiche du modèle d’ISTA-DASLab, elle garde 98,7 % de la moyenne des tâches du modèle de base (AIME25, GPQA-Diamond, LiveCodeBench v6). | pas encore mesurée |
| **IQ2_XXS** (2.00 bits) | 257 GB | KL 0,170, perplexité 3,63 (+15,5 %) ; code +8,6 %, tâches d’agent +5,3 % | 6 à 9 tokens/s sur une RTX 3060, environ 5 sur une RTX 5070 |
| **IQ2_XS** (2.25 bits) | 274 GB | KL 0,107, perplexité 3,37 (+7,3 %) ; code +3,2 %, tâches d’agent +2,2 % | environ 11 tokens/s sur une Radeon AI PRO R9700 |
| **IQ4_NL** (table Engram, pas un troisième modèle) | 111 Go, déjà compris dans les trois téléchargements ci-dessus | La table de consultation Engram en 4 bits que le modèle lit dans la RAM ou sur le SSD. Les trois versions utilisent ce seul fichier (`DeepSeek-V4.1-Flash-Engram-IQ4_NL.gguf`). | n/a |
4x Radeon AI PRO R9700 : environ 31 tokens/s, 55 à 69 avec DSpark.
Lancez-le avec `strata launch --model ist-das-2.00bpw` (ou `ist-das-1.75bpw`, `ist-das-2.25bpw`). Détails, vitesses par carte et installation manuelle : [docs/DEEPSEEK41.md](docs/DEEPSEEK41.md).
## Quelle est sa vitesse ?
Mesuré avec une carte graphique de 12 Go (RTX 5070, Ryzen 5 7600, 64 Go de RAM). Voir les résultats d'autres cartes :
[vitesse de chaque taille](docs/MODELS.md#how-fast-is-each-size), [résultats de la communauté](docs/COMMUNITY_BENCHMARKS.md).
| Modèle | Les réponses s'affichent à | Lit votre prompt à |
- **Réflexion :** choisissez **off, low, medium ou high** dans le menu du chat ou dans le « reasoning effort » de
votre application. Off est le plus rapide. High est le meilleur pour les questions difficiles.
- **Images :** répondez oui à « Images? » pendant l'installation. Ensuite cliquez sur **Picture** dans le chat, ou
joignez des images dans votre application. Les cartes AMD lisent les images sous Linux via le processeur ; sous
Windows, pas encore.
- **Depuis votre téléphone ou un autre PC :** `START-HERE.bat --setup --host 0.0.0.0 --api-key <secret>`. Définissez
toujours une clé.
- **Une requête à la fois :** par défaut, Strata répond à une requête et les autres attendent. Pour répondre à
plusieurs en même temps, mettez `"parallel": 2` ([BATCHING.md](docs/BATCHING.md)). Sur une carte de 12 Go, chaque
réponse devient alors plus lente.
- **Longs prompts :** Strata lit en entier le premier message d'une discussion, environ 1 minute pour 30,000 tokens.
Les messages suivants démarrent en quelques secondes.
L'application comprend un **Chat**, un **Monitor** du modèle, de votre GPU, de votre CPU et de votre RAM, une page **Models** et les **Settings**. Le chat peut travailler dans un dossier de votre PC. En mode **Manual**, il demande
avant de modifier un fichier ou d'exécuter une commande. Tapez `/loop 10m <what to do>` pour répéter un message toutes les
10 minutes tant que le chat est ouvert.
Plus d'infos : [où sont stockées vos discussions](docs/INSTALL.md#where-things-are-stored), [l'API](docs/DETAILS.md#using-it).
Le chat peut chercher sur le web avec DuckDuckGo (ou Bing), sans compte. Vous pouvez ajouter une clé de recherche sous
**Settings, Tools**. La recherche est la seule fonction qui envoie quelque chose hors de votre PC (les mots recherchés).
## Un problème ?
Pour faire tourner deux modèles, activez **Run multiple models** (exécuter plusieurs modèles) et donnez à chacun sa propre carte de la même marque.
- **Mon PC s'est figé au premier démarrage de Strata.** C'est normal pendant le chargement du modèle. Attendez, et ne
fermez pas la fenêtre. Toujours figé après 10 minutes ? Redémarrez le PC, fermez les autres programmes et
réessayez, ou choisissez une taille plus petite.
- **Il s'est arrêté pendant le téléchargement ou l'installation.** Relancez `START-HERE.bat` (ou `./setup.sh`). Il
reprend là où il s'était arrêté.
- **C'est très lent et le voyant du disque clignote sans arrêt, ou il affiche « the engine stopped unexpectedly ».**
Votre PC n'a pas assez de RAM libre. Fermez les autres programmes (les navigateurs en utilisent beaucoup), ou
choisissez une taille plus petite (Q2_0 ou IQ2_XS).
- **Il dit que le port 8080 est déjà utilisé.** Strata tourne déjà. Cherchez sa fenêtre.
<p align="center"><img src="docs/media/app-chat.png" width="900" alt="The Strata app: a chat where the agent built a small voxel pagoda garden, with the page open in the preview on the right"><br>
<sub>Une discussion dans l'app Strata : l'agent a écrit un petit jardin de pagode en voxels dans un seul fichier HTML, et la page s'ouvre dans l'aperçu à côté de la conversation</sub></p>
Autres problèmes et leurs solutions : [docs/TROUBLESHOOTING.md](docs/TROUBLESHOOTING.md). Toujours bloqué ? Ouvrez une
[issue](https://github.com/Niko1221/Strata/issues) et joignez `strata-<model>.log` depuis le dossier Strata. Vous
avez trouvé un problème de sécurité ? Signalez-le en privé : [SECURITY.md](SECURITY.md).
### Dans le Terminal
<p align="center"><img src="docs/media/terminal-monitor.png" width="900" alt="The Strata Terminal monitor overview while the model writes a reply"><br>
<sub><code>strata monitor</code> pendant que le modèle répond : vitesse, GPU, mémoire et les dernières requêtes</sub></p>
strata update --check # is there a new engine? (strata update installs it)
strata doctor # check the installed engine files
```
`strata --help` liste tout. Démarrer et servir ne téléchargent rien. Seuls l'installation, `strata launch` (quand vous
choisissez un modèle) et `strata update` le font. Pour joindre Strata depuis votre téléphone ou un autre PC, utilisez
`strata launch --access lan`. La commande crée une clé d'API et refuse de démarrer sur un réseau sans elle. Aucun
navigateur ne s'ouvre, sauf si vous ajoutez `--open`. Guide : [docs/TERMINAL_LAUNCH.md](docs/TERMINAL_LAUNCH.md). Le client complet : [docs/TERMINAL.md](docs/TERMINAL.md).
### Avec des agents d'IA
Un agent installe Strata avec la commande en une ligne de l'option B et lance
`strata launch --model NAME --yes --no-tui`. Les étapes, les options et les codes de sortie sont dans [docs/AI_SETUP.md](docs/AI_SETUP.md),
et les règles du dépôt dans [AGENTS.md](AGENTS.md).
### Connecter vos autres applications
| Type d'application | URL de base |
| --- | --- |
| Compatible OpenAI (la plupart des applications et des agents) | `http://127.0.0.1:8080/v1` (n'importe quelle clé d'API, n'importe quel nom de modèle) |
| API Anthropic (Claude Code et d'autres) | `http://127.0.0.1:8080`, sans `/v1`. Ces applications ajoutent `/v1/messages` elles-mêmes. Claude Code : `ANTHROPIC_BASE_URL=http://127.0.0.1:8080` |
| API OpenAI Responses (Codex CLI) | `http://127.0.0.1:8080/v1/responses` ([configuration](docs/DETAILS.md#the-responses-api-and-codex-cli)) |
`strata connect` affiche les réglages exacts pour OpenCode, Codex CLI, Claude Code, oh-my-pi, pi, Hermes Agent et OpenClaw.
`strata connect opencode --write` écrit le fichier après confirmation et garde une sauvegarde.
### Images
Les images sont activées par défaut pour les modèles qui savent les lire. Joignez ou collez une image dans le chat, ou
envoyez-la depuis votre application. Le lecteur d'images tourne sur le processeur quand votre carte graphique a moins de
24 Go (quelques secondes par image, sans prendre de mémoire à la carte) et sur la carte graphique à partir de 24 Go. La
règle est la même pour NVIDIA et AMD. Vous pouvez désactiver les images par modèle, ou choisir le lecteur avec
`--vision off`, `--vision gpu` ou `--vision cpu`.
Les chiffres derrière la règle : sur une Radeon AI PRO R9700, le lecteur de la carte a pris 57 ms pour une image de
260 tokens et celui du processeur 3,1 s. Sur des cartes de 16 Go (Tesla P100 et RTX A4000, Linux), le lecteur de la carte
a coûté 3 à 5 % de vitesse sur le texte, parce qu'il occupe environ 1,4 Go que le modèle utiliserait sinon. Sur un Mac, le lecteur tourne sur le GPU quelle que soit la mémoire (0,45 s pour une image de 260 tokens sur un M4 de 32 Go, environ 1,2 Go de la
mémoire partagée) ; le processeur met environ 50 s par image, il ne lit donc qu'avec `--vision cpu`.
### Bon à savoir
- Choisissez le niveau de réflexion (**désactivé, faible, moyen ou élevé**) dans le menu du chat ou dans le réglage « reasoning effort » de votre application. Désactivé est le plus rapide.
- Strata répond à une requête à la fois par défaut. Pour en traiter plusieurs en même temps, utilisez `--parallel N` dans `strata launch` ou
`"parallel": 2` dans la configuration ([BATCHING.md](docs/BATCHING.md)). Sur une carte de 12 Go, chaque réponse devient alors plus lente.
- Strata lit en entier le premier message d'une discussion, soit environ 1 minute pour 30 000 tokens. Les messages suivants démarrent en quelques secondes.
- Où sont stockées vos discussions et l'API complète : [docs/INSTALL.md](docs/INSTALL.md#where-things-are-stored), [docs/DETAILS.md](docs/DETAILS.md#using-it).
## Compiler depuis les sources
<details>
<summary>Windows, Linux et Mac depuis le dépôt</summary>
[Téléchargez Strata](https://github.com/Niko1221/Strata/archive/refs/heads/main.zip) et décompressez-le, ou faites un `git clone`.
**Windows :** double-cliquez sur `START-HERE.bat`. **Linux :** lancez `./setup.sh` dans le dossier Strata.
**Mac (Apple Silicon, macOS 15 ou plus récent) :** installez une fois les Command Line Tools d'Apple avec `xcode-select --install`
(Xcode lui-même n'est pas nécessaire). Ouvrez le Terminal dans le dossier Strata et lancez `./setup.sh`. Le script compile
le moteur Mac sur votre Mac (environ 7 minutes), pose les mêmes questions, télécharge le modèle et le démarre. La fois
suivante, `./setup.sh` le démarre tout de suite. Quelle taille convient à votre Mac : [docs/MAC.md](docs/MAC.md).
L'installateur détecte votre carte et prépare le bon moteur. Il demande quel modèle et quelle taille, combien de contexte
(la quantité de texte que le modèle garde en tête) et s'il doit lire les images. Appuyez sur Entrée pour la réponse
recommandée. Puis il télécharge le modèle (environ 70 Go) et le démarre. Si le téléchargement s'arrête, relancez le script
et il reprend. Votre navigateur ouvre la page de Strata à l'adresse `http://127.0.0.1:8080`.
> Pendant le démarrage du modèle, votre PC peut être lent ou ne plus répondre pendant 1 à 3 minutes (le plus longtemps la
> première fois). Strata charge 35 à 55 Go dans votre RAM et en verrouille une partie pour la carte graphique. Patientez et
> ne fermez pas la fenêtre.
La fois suivante, relancez `START-HERE.bat` (ou `./setup.sh`). Il démarre tout de suite et ne télécharge rien deux fois.
Fermez sa fenêtre pour arrêter le modèle. `UPDATE.bat` (`./update.sh`) met Strata à jour sans le démarrer. Mise à jour,
Docker, plusieurs cartes, où vont les fichiers et toutes les options : [docs/INSTALL.md](docs/INSTALL.md).
Pour mesurer votre PC, lancez `BENCH.bat` (`./bench.sh`). Cela prend 5 à 10 minutes et produit un rapport que vous pouvez
**Facultatif : plus de vitesse sur une carte quand tout le modèle tient en RAM.** Deux réglages à activer soi-même, par
RubenTorney (#1904, #1905). Ils aident quand vous utilisez une seule carte graphique et que votre RAM contient tous les
experts, par exemple Q2_0 ou IQ3_XXS sur un PC de 64 Go. Ouvrez `strata-<model>.json` dans le dossier de données de Strata ([où](docs/INSTALL.md#where-things-are-stored)) et ajoutez :
-`"--adapt-async", "1"` à la liste `"args"`. Le cache des experts se met alors à jour sur un thread auxiliaire au lieu de
mettre la réponse en pause. Mesuré sur une RTX 5070 (Windows, Q2_0) : +4 à 5 % en décodage, +7 à 10 % sur le code et les
longs documents. Les réponses peuvent différer légèrement de celles du réglage par défaut, et restent identiques d'une exécution à l'autre.
-`"STRATA_PIN_BY_NEED": "1"` dans le bloc `"env"` (Windows). Windows ne peut verrouiller qu'environ la moitié de la RAM
pour la carte graphique, et ce réglage verrouille d'abord les experts dont la carte a le plus besoin. Mesuré sur la RTX 5070 : +4,4 % en lecture du prompt.
```json
"args":[...,"--adapt-async","1"],
"env":{"STRATA_PIN_BY_NEED":"1"}
```
Redémarrez Strata après la modification. Pour annuler, supprimez les deux entrées.
</details>
## Comment ça marche ?
Les modèles comme celui-ci tournent d'habitude sur des serveurs avec des centaines de gigaoctets de mémoire
graphique. Votre carte graphique a 12-24 Go. Strata fait tenir le modèle en **répartissant le travail sur tout votre
PC**. Pensez à une cuisine : ce que vous utilisez tout le temps reste sur le plan de travail, et le reste attend dans
le placard.
Les modèles de ce genre tournent d'habitude sur des serveurs qui disposent de centaines de gigaoctets de mémoire graphique.
Votre carte graphique en a 12 à 24. Strata fait tenir le modèle en répartissant le travail sur tout votre PC. Imaginez une
bibliothèque.
<p align="center"><img src="docs/media/how-it-works.svg" width="860" alt="Les 24,576 experts du modèle : les plus sollicités sur la carte graphique, tous en RAM, une table de correspondance sur le SSD"></p>
- Le modèle est une équipe de 24 576 petits spécialistes (les « experts »), comme 24 576 ouvrages de référence. Chaque mot n'en demande que 10.
- **Votre carte graphique est le bureau.** Il est petit et rapide. Les quelque 4 000 livres les plus utilisés y sont ouverts.
- **Votre RAM, ce sont les étagères de la salle de lecture.** Les 24 576 livres s'y trouvent. Quand un mot demande un livre
qui n'est pas sur le bureau, votre processeur va le chercher aussitôt sur l'étagère, pendant que la carte continue de travailler.
- **Votre SSD, ce sont les archives.** Il contient une table de consultation de 29 Go. Le modèle n'en lit que quelques lignes par
mot, elle n'a donc pas besoin de tenir en RAM.
- **Le modèle est une équipe de 24,576 petits spécialistes (les « experts »).** Chaque mot n'en demande que 10.
- **Votre carte graphique** garde les quelques milliers d'experts les plus utilisés. **Votre RAM** les contient tous,
et **votre processeur** travaille sur les autres en même temps. **Votre SSD** contient une grande table de
correspondance.
<p align="center"><img src="docs/media/how-it-works.svg" width="860" alt="The model's 24,576 experts: the busiest on the graphics card, all of them in RAM, a lookup table on the SSD"></p>
<p align="center"><img src="docs/media/guess-and-check.svg" width="860" alt="Un petit assistant devine les mots suivants ; le grand modèle les vérifie tous d'un coup et garde les bons"></p>
Un petit assistant devine aussi les mots suivants. Le grand modèle les vérifie tous d'un coup et garde les bons. Vous
obtenez la même réponse, 1,6 à 1,8 fois plus tôt. Les textes longs sont lus par gros morceaux, jusqu'à 8 192 tokens à la
fois, à plus de 1 000 tokens par seconde.
- **Deviner, puis vérifier :** un petit assistant devine les quelques mots suivants. Le grand modèle les vérifie
tous d'un coup. Vous obtenez la même réponse, 1.6-1.8x plus tôt.
- **Les longs textes sont lus par gros morceaux** (jusqu'à 8,192 tokens à la fois), à plus de 1,000 tokens par seconde.
<p align="center"><img src="docs/media/guess-and-check.svg" width="860" alt="A small helper guesses the next words; the big model checks them all at once and keeps the right ones"></p>
L'explication plus longue : [docs/HOW_IT_WORKS.md](docs/HOW_IT_WORKS.md). Chaque partie et ses chiffres :
L'explication plus longue : [docs/HOW_IT_WORKS.md](docs/HOW_IT_WORKS.md). Chaque élément avec ses chiffres :
[les détails](docs/DETAILS.md#how-it-works) et l'[article](docs/paper/Strata-Paper.pdf).
## Crédits et licence
## Un problème ?
Le modèle est [Qwen3.8-Flash-Next](https://huggingface.co/Qwen/Qwen3.8-Flash-Next), de l'équipe Qwen. Il a été
compressé par [ISTA-DASLab](https://huggingface.co/ISTA-DASLab/Qwen3.8-Flash-Next-GSQ-RCO-GGUF), UkisAI (Swift 1.5)
- **Mon PC s'est figé au premier démarrage de Strata.** Il charge le modèle en RAM. Patientez et ne fermez pas la fenêtre.
Toujours figé après 10 minutes ? Redémarrez le PC, fermez les autres programmes et réessayez, ou choisissez une taille plus petite.
- **Ça s'est arrêté pendant le téléchargement ou l'installation.** Relancez l'application ou `strata launch`, ou relancez
`START-HERE.bat` (ou `./setup.sh`). Cela reprend là où ça s'était arrêté.
- **C'est très lent, le voyant du disque clignote sans arrêt, ou il affiche « the engine stopped unexpectedly ».** Votre PC
n'a pas assez de RAM libre. Fermez d'autres programmes (les navigateurs en consomment beaucoup) ou choisissez une taille plus petite, comme Q2_0 ou IQ2_XS.
- **Il indique qu'il n'y a pas de moteur pour ce PC.** Dans l'application, ouvrez **Settings, Engine** et appuyez sur **Update engine** (mettre à jour le moteur). Dans le Terminal, lancez `strata update`. `strata doctor` montre quels fichiers du moteur sont installés.
- **Il indique que le port 8080 est déjà utilisé.** Strata tourne déjà. Cherchez sa fenêtre.
Autres problèmes et solutions : [docs/TROUBLESHOOTING.md](docs/TROUBLESHOOTING.md). Toujours bloqué ? Ouvrez une
[issue](https://github.com/Niko1221/Strata/issues). Dans l'application, joignez le fichier obtenu par **Settings, Engine, Diagnostics, View logs, Save logs**. Dans le Terminal, collez la sortie de `strata doctor`. Si
vous avez lancé depuis les sources, joignez `strata-<model>.log` du dossier Strata. Pour un problème de sécurité,
signalez-le en privé : [SECURITY.md](SECURITY.md).
## Remerciements et licence
Le modèle est [Qwen3.8-Flash-Next](https://huggingface.co/Qwen/Qwen3.8-Flash-Next), de l'équipe Qwen. Il a été compressé par
et Unsloth. Strata utilise des parties de [llama.cpp / ggml](https://github.com/ggml-org/llama.cpp). Tous les
crédits : [docs/HOW_IT_WORKS.md](docs/HOW_IT_WORKS.md#credits). Strata est open source sous [licence MIT](LICENSE).
Quelques parties et chaque modèle ont leur propre licence ([lesquelles](docs/HOW_IT_WORKS.md#license)).
remerciements : [docs/HOW_IT_WORKS.md](docs/HOW_IT_WORKS.md#credits). Strata est open source sous [licence MIT](LICENSE).
Quelques parties et tous les modèles ont leurs propres licences ([lesquelles](docs/HOW_IT_WORKS.md#license)).
## Contribuer
Vous avez trouvé un bug, ou vous voulez proposer une modification ou un benchmark ? Lisez d'abord [CONTRIBUTING.md](CONTRIBUTING.md).
Il indique ce qu'il faut inclure pour que d'autres puissent vérifier votre travail.
## Soutenir Strata
Strata est gratuit et open source. S'il vous est utile, vous pouvez soutenir son développement :
Strata est gratuit et sous licence MIT. Je le construis et je le teste sur le matériel que je peux obtenir. S'il tourne bien sur votre PC, un café m'aide
à continuer de tester de nouvelles cartes et à corriger ce que vous signalez. Aucune obligation. L'utiliser et signaler ce que vous constatez aide tout autant.
<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>
<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>
Vous souhaitez sponsoriser Strata ? Contactez-moi via [GitHub](https://github.com/Niko1221).
<p align="center"><a href="https://github.com/Niko1221/Strata/releases/download/v0.1.10/Pagoda.mp4"><img src="docs/media/pagoda-preview.webp" width="720" alt="A voxel pagoda garden that Strata's model wrote, running in the browser"></a><br>
<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>
<p align="center"><img src="docs/media/app-chat.png" width="900" alt="The Strata app: a chat where the agent built a small voxel pagoda garden, with the page open in the preview on the right"><br>
<sub>Strata アプリでのチャット。エージェントが小さなボクセルの塔の庭を 1 つの HTML ファイルとして書き、そのページが会話の横のプレビューで開きます</sub></p>
### ターミナルで
<p align="center"><img src="docs/media/terminal-monitor.png" width="900" alt="The Strata Terminal monitor overview while the model writes a reply"><br>
<p align="center"><img src="docs/media/how-it-works.svg" width="860" alt="The model's 24,576 experts: the busiest on the graphics card, all of them in RAM, a lookup table on the SSD"></p>
<p align="center"><img src="docs/media/guess-and-check.svg" width="860" alt="A small helper guesses the next words; the big model checks them all at once and keeps the right ones"></p>
<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>
<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 align="center"><a href="https://github.com/Niko1221/Strata/releases/download/v0.1.10/Pagoda.mp4"><img src="docs/media/pagoda-preview.webp" width="720" alt="A voxel pagoda garden that Strata's model wrote, running in the browser"></a><br>
<sub>A voxel pagoda garden, 1 shot prompt running on an RTX 5070 with Strata (IQ3_S, 128K context) ·
<a href="https://github.com/Niko1221/Strata/releases/download/v0.1.10/Pagoda.mp4">full video (49 s)</a></sub></p>
Strata runs **[Qwen3.8-Flash-Next](https://huggingface.co/Qwen/Qwen3.8-Flash-Next)** on a normal PC. This is a
large, smart AI model that usually needs a server. It chats, writes code, reads pictures and works with your apps
and coding agents. Nothing leaves your PC.
Strata runs **[Qwen3.8-Flash-Next](https://huggingface.co/Qwen/Qwen3.8-Flash-Next)**, a 125-billion-parameter model that
normally needs a server, on a normal PC. It chats, writes code, reads pictures and works with your apps and coding
agents. Your chats and files stay on your PC.
## How fast is it?
**New here?** Check [what you need](#what-you-need), then [install the app](#option-a-the-strata-app).
We measured it on two ordinary gaming PCs. A token is about ¾ of a word.
- **Runs on a gaming PC.** 32 GB of RAM and graphics memory together is enough to start.
- **Works with your tools.** It speaks the OpenAI and Anthropic APIs, so Claude Code, Codex CLI, OpenCode and other agents connect to it.
- **Reads pictures.** They are on by default.
- **Free.** MIT license, no account, no cloud.
- **Private.** No telemetry. What Strata downloads, when it uses the internet and how it checks downloads is in [docs/SAFETY.md](docs/SAFETY.md).
- **Writes answers:** how fast the reply appears in a short chat. 60 tokens per second is faster than you can read.
- **Reads your prompt:** how fast it takes in what you send (here a 32K-token document, code or chat history).
<p align="center"><a href="https://buymeacoffee.com/strataengine"><img src="https://cdn.buymeacoffee.com/buttons/v2/default-yellow.png" alt="Buy Me A Coffee" height="50"></a><br>
<sub>Strata is free. If it runs well on your PC, a coffee keeps the work on it going.</sub></p>
Version 0.2.3 · engine 0.1.48
## What you need
| | |
**You need at least 32 GB of RAM and graphics memory (VRAM) together.** On a Mac or a Ryzen AI Max, that is the unified
memory. More memory gives a bigger model or a faster one.
- A 32 GB PC with a 12-16 GB card runs the Coder.
- 64 GB of RAM runs every size.
| Your computer | Details |
| --- | --- |
| **Graphics card**| **NVIDIA** GeForce RTX 20, 30, 40 or 50 series, or **AMD** Radeon RX 7900 XT / XTX, RX 7800 XT / 7700 XT, RX 9060 XT, RX 9070 / 9070 XT, Radeon AI PRO R9700 or RX 6800 / 6900 series. It needs **12 GB of VRAM or more**. |
| **RAM**| 32 GB or more. Your RAM decides [which model](#which-model-should-i-pick) fits. 64 GB runs every size. |
| **Disk**| About 80 GB free. Use an SSD if you can: the first start is much faster. |
| **System** | Windows 10 / 11 or Linux, and a current graphics driver from NVIDIA or AMD. |
| **NVIDIA** GeForce RTX 20, 30, 40, 50 series (12 GB or more) | Install the app or the Terminal. |
| **AMD**Radeon RX 6800 and newer, Radeon AI PRO R9700 (12 GB or more) | [AMD cards](docs/AMD_HIP.md) |
| | NVIDIA, older card or driver older than 580 | [`Strata-0.2.3-windows-cuda12.exe`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.3-windows-cuda12.exe) |
| | AMD Radeon RX 9000 series or Radeon AI PRO R9700 | [`Strata-0.2.3-windows-hip.exe`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.3-windows-hip.exe) |
| | AMD Radeon RX 7000 series or Ryzen AI Max (Strix Halo) | [`Strata-0.2.3-windows-hip-rdna3.exe`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.3-windows-hip-rdna3.exe) |
| | NVIDIA, older card or driver older than 580 | [`Strata-0.2.3-linux-nvidia-cuda12.AppImage`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.3-linux-nvidia-cuda12.AppImage) |
| | AMD Radeon RX 9000 series or Radeon AI PRO R9700 | [`Strata-0.2.3-linux-amd-rx9070.AppImage`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.3-linux-amd-rx9070.AppImage) |
| | AMD Radeon RX 7000 series | [`Strata-0.2.3-linux-amd-rx7900.AppImage`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.3-linux-amd-rx7900.AppImage) |
| | AMD Ryzen AI Max (Strix Halo) | [`Strata-0.2.3-linux-amd-strixhalo.AppImage`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.3-linux-amd-strixhalo.AppImage) |
| **Mac** (Apple Silicon, macOS 15 or newer) | built in | [`Strata-0.2.3-mac.dmg`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.3-mac.dmg) |
**If you are unsure:** for NVIDIA, take `cuda13` if your driver is version 580 or newer, otherwise `cuda12`. `cuda12` is
for older cards and older drivers. The `hip` and `amd` files are for AMD cards. RX 6000 cards have no installer of
their own: install `hip-rdna3` (Windows) or `amd-rx7900` (Linux), and on a PC with an RX 6000 card it fetches the RX 6000 engine by itself
(also `strata update`). That engine is built but not yet tested on a real RX 6000 card: see [AMD cards](docs/AMD_HIP.md).
Then:
- **Windows:** run the `.exe`. It installs for your user and needs no administrator rights. If Windows says "Windows
protected your PC", choose **More info**, then **Run anyway** (the installer is not code-signed yet).
- **Linux:** make the file runnable (`chmod +x`) and start it. It needs FUSE 2: `sudo apt install libfuse2` (Ubuntu 22.04)
or `libfuse2t64` (Ubuntu 24.04 and newer).
- **Mac:** open the `.dmg` and drag Strata onto Applications.
On the first screen the app reads your PC and recommends a model that fits. Press install. The model is a big download
(about 40-70 GB) and continues where it stopped if it is interrupted. Closing the window stops Strata and the engine.
### Option B: the Terminal (one line)
The Terminal is the `strata` command, for scripts, servers and AI agents.
Linux and Mac:
```sh
curl -fsSL https://github.com/Niko1221/Strata/releases/latest/download/install.sh | sh
The script checks your PC, downloads the Terminal, verifies its SHA-256 and unpacks it into your home folder. It needs no
administrator rights. On Windows, open a new terminal window afterwards. Then start Strata:
```sh
strata launch
```
It asks one question at a time: model, context, pictures, who may connect. Then it downloads the engine for your graphics
card (signed, and test-started before use) and the model, and starts serving. For scripts and AI agents, use flags:
```sh
strata launch --model q2_0 --yes --no-tui
```
To read the install script before it runs, download it first with `curl -fsSL <address> -o install.sh`, read it, then run
`sh install.sh`. All screens and flags: [docs/TERMINAL_LAUNCH.md](docs/TERMINAL_LAUNCH.md).
### Option C: let your AI set it up
Paste this into Claude Code, Cursor, Codex, GitHub Copilot or another coding assistant:
```text
Set up Strata on this PC for me: https://github.com/Niko1221/Strata - follow docs/AI_SETUP.md in that repository.
```
It checks your graphics card, RAM and disk and picks the model that fits. Then it installs and starts it and tells
you how to connect your apps. AI tools can also install, start and stop Strata through its
[MCP server](docs/MCP_SERVER.md).
It checks your graphics card, RAM and disk, picks the model that fits, installs and starts it, and tells you how to
connect your apps. AI tools can also install, start and stop Strata through its [MCP server](docs/MCP_SERVER.md).
### Or do it yourself
Prefer to build it yourself? See [Build from source](#build-from-source).
[Download Strata](https://github.com/Niko1221/Strata/archive/refs/heads/main.zip) and unzip it (or `git clone` it).
**Windows:** double-click **`START-HERE.bat`**. **Linux:** run **`./setup.sh`** in the Strata folder.
## Models
The steps are the same for NVIDIA and AMD. The installer finds your card and sets up the right engine for it. It
asks you a few questions:
The app and `strata launch` recommend a model for your memory. It is one model in several sizes, compressed more or less.
Smaller sizes are faster. Larger sizes are a bit smarter.
- which model and which size,
- how much context (how much text the model keeps in mind),
- whether it should read pictures.
### Low on memory? Try Gyro
Press Enter each time for the recommended answer. Then it downloads the model (about 70 GB) and starts it. If the
download stops, run it again: it continues where it left off. Your browser opens the Strata app at
`http://127.0.0.1:8080`.
[Gyro](https://huggingface.co/agentionai/Qwen3.8-Flash-Next-Gyro-GGUF) (by agentionai) is the smallest way to run this model.
> **While the model starts, your PC can be slow or stop responding for 1-3 minutes** (longest the first time).
> Strata loads 35-55 GB into your RAM and locks part of it for the graphics card. This is normal. Wait, and don't
> close the window. The window shows what Strata is doing.
| Size | Memory it needs (RAM + VRAM) | Download | What it is |
| --- | ---: | ---: | --- |
| **Gyro-S** | 27.6 GB | 58.5 GB | For PCs that fit nothing else. Quality is lower than IQ2_XS. |
| **Gyro-M** | 35 GB | 92.0 GB | About IQ3_XXS quality with about 20% less GPU memory. It writes answers 16% (12 GB RTX 3060) to 21% (R9700) slower than Q2_0. |
**Next time**, run `START-HERE.bat` (or `./setup.sh`) again. It starts right away and downloads nothing twice. Close
its window to stop the model. `UPDATE.bat` (`./update.sh`) updates Strata without starting it. Updating, Docker,
several cards, where the files go and every option: [docs/INSTALL.md](docs/INSTALL.md).
Gyro runs on NVIDIA and AMD cards (AMD is experimental) on Intel Arc cards on Linux (tested on an Arc A750) and, from 0.2.2, on Apple Silicon Macs (Gyro-S on a 32 GB Mac mini: 9 tok/s, streaming from the SSD; [docs/MAC.md](docs/MAC.md#gyro-s-and-gyro-m-agentionais-trellis-experts)). Details and measurements:
[docs/GYRO.md](docs/GYRO.md). Start it with `strata launch --model gyro-s`.
## Which model should I pick?
### The other sizes
The installer recommends one for your RAM. The same model comes in several sizes, compressed more or less. Smaller
sizes are faster. Larger sizes are a bit smarter.
The tiers count your RAM plus your VRAM, less 5 GB that Windows and the system keep for themselves. Every other size stays a choice.
| Your RAM | Take | Why |
| Your memory (RAM + VRAM) | Take | Notes |
| --- | --- | --- |
| **32 GB** | **Coder** | it fits 32 GB, and it is made for code (with a 24 GB card, Q2_0 and IQ2_XS run too) |
| **48 GB** | **IQ2_XS** (or Q2_0, the fastest) | the larger sizes do not fit |
| **64 GB** | **IQ2_XS** (recommended), or IQ3_XXS / IQ3_S | every size fits; IQ3_S is the best and the slowest |
| **96 GB or more** | **IQ3_S**, or Unsloth's UD-IQ4_XS (~4-bit) | room for the largest sizes with everything else open |
| **32 GB** | **Gyro-S**, or the **[Coder](docs/MODELS.md#coder)** for code | The Coder needs 32 GB of RAM and keeps half of the experts, so it is weaker outside code |
| **40 GB** | **[Swift 1.5](docs/MODELS.md#swift-15)**IQ2_XS (39.2 GB), or **Gyro-M** (35 GB) | Swift thinks for a shorter time before it answers. Gyro-M is close to IQ3_XXS in quality, but it writes answers 16-21% slower than Q2_0 |
| **64 GB** | **IQ3_S** (54.8 GB), the original or Swift 1.5 | The slowest, the best: it matches the full model on the published tests |
| **96 GB or more** | Unsloth's **UD-IQ4_XS** (about 72 GB of RAM) to **Q8_0** (about 136 GB), 4 to 8 bit | Closer to the full model. With less RAM, Strata reads part of them from the SSD, which is much slower |
- **[Coder](docs/MODELS.md#coder):** a coding version with half of the experts removed. It reaches 91% of the full
model's SWE-bench Verified score (measured by its authors) and fits 32 GB of RAM. It is weaker outside code,
including Chinese and other CJK text (#438). For those, take Q2_0, IQ2_XS or IQ3_S, which keep every expert.
- **[Swift 1.5](docs/MODELS.md#swift-15):** a fine-tune that thinks for a much shorter time before it answers. You
get the answer sooner, at about the same quality.
- **[Unsloth UD-IQ4_XS](docs/MODELS.md#unsloth-ud-iq4_xs):** Unsloth's ~4-bit version, between IQ3_S and
UD-Q4_K_XL in quality. A 94 GB download. With less than ~80 GB of RAM, Strata reads part of it from the SSD
while it answers, so it is slower there (an NVMe SSD helps).
- **[Unsloth UD-Q4_K_XL](docs/MODELS.md#unsloth-ud-q4_k_xl-experimental)** (experimental): the closest to the full
model. But Strata reads most of it from the SSD while it answers, so it writes only 7-8.5 tokens/s on a 64 GB PC.
- **[OrcaRouter's Uncensored IQ3_XXS](docs/MODELS.md#orcarouter-uncensored-iq3_xxs):** you set it up by hand. It is
not in the installer's menu.
Less RAM than a row says can still work: Strata switches to its
[low-RAM mode](docs/MODELS.md#a-big-graphics-card-and-little-ram) by itself. Expect it to be slower, and much slower
with a small graphics card.
Sizes, downloads and what fits where: [docs/MODELS.md](docs/MODELS.md). To add another model later, run
`SETUP.bat` (Linux: `./setup.sh --setup`).
More choices:
- **[Swift 1.5](docs/MODELS.md#swift-15)** is a fine-tune that thinks for a shorter time before it answers, at about the same quality and RAM.
- **[NVFP4](docs/NVFP4.md)** is NVIDIA's 4-bit version for NVIDIA cards. It needs about 80 GB of RAM to keep every expert in RAM.
- **[Qwen3.6-35B-A3B](docs/QWEN36.md)** is a smaller, different model that reads pictures and can answer several requests at once.
- **[OrcaRouter's Uncensored IQ3_XXS](docs/MODELS.md#orcarouter-uncensored-iq3_xxs)** is set up by hand.
Every size, download and what fits where: [docs/MODELS.md](docs/MODELS.md).
### DeepSeek V4.1-Flash (in the Terminal, from 0.2.4)
[DeepSeek V4.1-Flash](https://huggingface.co/ISTA-DASLab/DeepSeek-V4.1-Flash-1017-GSQ-RCO-GGUF) is a different, much bigger model. ISTA-DASLab made the three sets below. Strata runs them on a PC with one 12 GB graphics card and 46 to 64 GB of RAM, and reads the rest from your SSD. It runs on NVIDIA RTX 30 to 50 cards and on the Radeon AI PRO R9700. It uses its own engine package, `strata-dsv41`, which `strata launch` downloads when you pick DeepSeek. The desktop app does not offer it yet.
| Set | Download | Quality against the original model | Writes at |
| --- | ---: | --- | --- |
| **IQ1_M** (1.75 bits) | 239 GB | KL 0.217, perplexity 3.80 (+21%); code +8.9%, agent work +10.2%; weaker on plain English prose. ISTA-DASLab's model card: keeps 98.7% of the base model's task average (AIME25, GPQA-Diamond, LiveCodeBench v6). | not measured yet |
| **IQ2_XXS** (2.00 bits) | 257 GB | KL 0.170, perplexity 3.63 (+15.5%); code +8.6%, agent work +5.3% | 6 to 9 tokens/s on an RTX 3060, about 5 on an RTX 5070 |
| **IQ2_XS** (2.25 bits) | 274 GB | KL 0.107, perplexity 3.37 (+7.3%); code +3.2%, agent work +2.2% | about 11 tokens/s on one Radeon AI PRO R9700 |
| **IQ4_NL** (Engram table, not a third model) | 111 GB, already in all three downloads above | The 4-bit Engram lookup table the model reads from RAM or SSD. All three sets use this one file (`DeepSeek-V4.1-Flash-Engram-IQ4_NL.gguf`). | n/a |
4x Radeon AI PRO R9700: about 31 tokens/s, 55 to 69 with DSpark.
Start it with `strata launch --model ist-das-2.00bpw` (or `ist-das-1.75bpw`, `ist-das-2.25bpw`). Details, speeds per card and the manual install: [docs/DEEPSEEK41.md](docs/DEEPSEEK41.md).
## How fast is it?
Based on a 12 GB graphics card (RTX 5070, Ryzen 5 7600, 64 GB RAM). See results from other cards:
[speed of each model](docs/MODELS.md#how-fast-is-each-size), [community results](docs/COMMUNITY_BENCHMARKS.md).
| Model | Answers appear at | Reads your prompt at |
- **Thinking:** choose **off, low, medium or high** in the chat menu or in your app's "reasoning effort". Off is the
fastest. High is best for hard questions.
- **Pictures:** say yes to "Images?" in setup. Then click **Picture** in the chat, or attach pictures in your app.
AMD cards read pictures on Linux through the processor; on Windows they can't yet.
- **From your phone or another PC:** `START-HERE.bat --setup --host 0.0.0.0 --api-key <secret>`. Always set a key.
- **One request at a time:** by default Strata answers one request, and the others wait. To answer several at once,
set `"parallel": 2` ([BATCHING.md](docs/BATCHING.md)). On a 12 GB card this makes each answer slower.
- **Long prompts:** Strata reads the first message of a chat in full, about 1 minute per 30,000 tokens. Follow-up
messages start in seconds.
The app has a **Chat**, a **Monitor** of the model and your GPU, CPU and RAM, a **Models** page and **Settings**. The chat
can work in a folder on your PC. In **Manual** mode it asks before it changes a file or runs a command. Type
`/loop 10m <what to do>` to repeat a message every 10 minutes while the chat is open.
More: [where your chats are stored](docs/INSTALL.md#where-things-are-stored), [the API](docs/DETAILS.md#using-it).
The chat can search the web with DuckDuckGo (or Bing), with no account. You can add a search key under **Settings, Tools**.
Search is the one feature that sends something off your PC, the search words.
## Something went wrong?
To run two models, turn on **Run multiple models** and give each model its own card of the same make.
- **My PC froze the first time Strata started.** This is normal while it loads the model. Wait, and don't close the
window. Still frozen after 10 minutes? Restart the PC, close other programs and try again, or pick a smaller size.
- **It stopped while downloading or installing.** Run `START-HERE.bat` (or `./setup.sh`) again. It continues where
it stopped.
- **It's very slow and the disk light keeps blinking, or it says "the engine stopped unexpectedly".** Your PC does
not have enough free RAM. Close other programs (browsers use a lot), or pick a smaller size (Q2_0 or IQ2_XS).
- **It says port 8080 is already in use.** Strata is already running. Look for its window.
<p align="center"><img src="docs/media/app-chat.png" width="900" alt="The Strata app: a chat where the agent built a small voxel pagoda garden, with the page open in the preview on the right"><br>
<sub>A chat in the Strata app: the agent wrote a small voxel pagoda garden as one HTML file, and the page opens in the preview next to the conversation</sub></p>
More problems and their fixes: [docs/TROUBLESHOOTING.md](docs/TROUBLESHOOTING.md). Still stuck? Open an
[issue](https://github.com/Niko1221/Strata/issues) and attach `strata-<model>.log` from the Strata folder. Found a
security problem? Report it privately: [SECURITY.md](SECURITY.md).
### In the Terminal
<p align="center"><img src="docs/media/terminal-monitor.png" width="900" alt="The Strata Terminal monitor overview while the model writes a reply"><br>
<sub><code>strata monitor</code> while the model writes a reply: speed, GPU, memory and the recent requests</sub></p>
strata update --check # is there a new engine? (strata update installs it)
strata doctor # check the installed engine files
```
`strata --help` lists everything. Starting and serving never download anything. Only the install, `strata launch` (when
you choose a model) and `strata update` do. To reach Strata from your phone or another PC, use
`strata launch --access lan`. It makes an API key (recommended; add `--no-api-key` to go without, with a warning). No browser opens unless
you add `--open`. Guide: [docs/TERMINAL_LAUNCH.md](docs/TERMINAL_LAUNCH.md). Whole client: [docs/TERMINAL.md](docs/TERMINAL.md).
### With AI agents
An agent installs Strata with the one-line command in Option B and runs
`strata launch --model NAME --yes --no-tui`. The steps, flags and exit codes are in [docs/AI_SETUP.md](docs/AI_SETUP.md),
and the repository rules are in [AGENTS.md](AGENTS.md).
### Connect your other apps
| App type | Base URL |
| --- | --- |
| OpenAI-compatible (most apps and agents) | `http://127.0.0.1:8080/v1` (any API key, any model name) |
| Anthropic API (Claude Code and others) | `http://127.0.0.1:8080`, without `/v1`. These apps add `/v1/messages` themselves. Claude Code: `ANTHROPIC_BASE_URL=http://127.0.0.1:8080` |
| OpenAI Responses API (Codex CLI) | `http://127.0.0.1:8080/v1/responses` ([setup](docs/DETAILS.md#the-responses-api-and-codex-cli)) |
`strata connect` prints the exact settings for OpenCode, Codex CLI, Claude Code, oh-my-pi, pi, Hermes Agent and OpenClaw.
`strata connect opencode --write` writes the file after asking and keeps a backup.
### Pictures
Pictures are on by default for models that can read them. Attach or paste a picture in the chat, or send it from your app.
The picture reader runs on the processor when your graphics card has less than 24 GB (a few seconds per picture, nothing
taken from the card's memory) and on the graphics card from 24 GB. The rule is the same for NVIDIA and AMD. Turn pictures
off per model, or choose the reader with `--vision off`, `--vision gpu` or `--vision cpu`.
The numbers behind the rule: on a Radeon AI PRO R9700 the card's reader took 57 ms for a 260-token picture and the
processor's took 3.1 s. On 16 GB cards (Tesla P100 and RTX A4000, Linux) the card's reader cost 3-5% of text speed,
because it holds about 1.4 GB that the model would use otherwise. On a Mac the reader runs on the Mac's GPU whatever its memory (0.45 s for a 260-token picture on an M4 with 32 GB, about 1.2 GB of the shared
memory); the processor reader needs about 50 s per picture there, so it runs only when you ask for it with `--vision cpu`.
### Good to know
- Choose the thinking level (**off, low, medium or high**) in the chat menu or in your app's "reasoning effort". Off is fastest.
- Strata answers one request at a time by default. For several at once, use `--parallel N` in `strata launch` or
`"parallel": 2` in the config ([BATCHING.md](docs/BATCHING.md)). On a 12 GB card this makes each answer slower.
- Strata reads the first message of a chat in full, about 1 minute per 30,000 tokens. Follow-up messages start in seconds.
- Where your chats are stored and the whole API: [docs/INSTALL.md](docs/INSTALL.md#where-things-are-stored), [docs/DETAILS.md](docs/DETAILS.md#using-it).
## Build from source
<details>
<summary>Windows, Linux and Mac from the repository</summary>
[Download Strata](https://github.com/Niko1221/Strata/archive/refs/heads/main.zip) and unzip it, or `git clone` it.
**Windows:** double-click `START-HERE.bat`. **Linux:** run `./setup.sh` in the Strata folder.
**Mac (Apple Silicon, macOS 15 or newer):** install Apple's Command Line Tools once with `xcode-select --install`
(Xcode itself is not needed). Open Terminal in the Strata folder and run `./setup.sh`. It builds the Mac engine on your
Mac (about 7 minutes), asks the same questions, downloads the model and starts it. Next time, `./setup.sh` starts it
right away. Which size fits your Mac: [docs/MAC.md](docs/MAC.md).
The installer finds your card and sets up the right engine. It asks which model and size, how much context (how much text
the model keeps in mind) and whether it should read pictures. Press Enter for the recommended answer. Then it downloads the
model (about 70 GB) and starts it. If the download stops, run it again and it continues. Your browser opens the Strata
page at `http://127.0.0.1:8080`.
> While the model starts, your PC can be slow or stop responding for 1-3 minutes (longest the first time). Strata loads
> 35-55 GB into your RAM and locks part of it for the graphics card. Wait, and don't close the window.
Next time, run `START-HERE.bat` (or `./setup.sh`) again. It starts right away and downloads nothing twice. Close its
window to stop the model. `UPDATE.bat` (`./update.sh`) updates Strata without starting it. Updating, Docker, several cards,
where the files go and every option: [docs/INSTALL.md](docs/INSTALL.md).
To measure your PC, run `BENCH.bat` (`./bench.sh`). It takes 5-10 minutes and writes a report you can share
**Optional: more speed on one card when the whole model fits in RAM.** Two opt-in settings by RubenTorney (#1904, #1905).
They help when you run one graphics card and your RAM holds every expert, for example Q2_0 or IQ3_XXS on a 64 GB PC. Open
`strata-<model>.json` in the Strata data folder ([where](docs/INSTALL.md#where-things-are-stored)) and add:
-`"--adapt-async", "1"` to the `"args"` list. The expert cache then updates on a helper thread instead of pausing the
answer. Measured on an RTX 5070 (Windows, Q2_0): +4-5% decode, +7-10% on code and long documents. Answers can differ
slightly from the default and stay the same from run to run.
-`"STRATA_PIN_BY_NEED": "1"` in the `"env"` block (Windows). Windows can lock only about half the RAM for the graphics
card, and this locks the experts the card needs first. Measured on the RTX 5070: +4.4% prompt reading.
```json
"args":[...,"--adapt-async","1"],
"env":{"STRATA_PIN_BY_NEED":"1"}
```
Restart Strata after the change. To undo, remove the two entries.
</details>
## How does it work?
Models like this one usually run on servers with hundreds of gigabytes of graphics memory. Your graphics card has
12-24 GB. Strata makes the model fit by **sharing the work across your whole PC**. Think of a kitchen: the things
you use all the time stay on the counter, and the rest waits in the pantry.
12-24 GB. Strata makes the model fit by sharing the work across your whole PC. Picture a library.
- The model is a team of 24,576 small specialists ("experts"), like 24,576 reference books. Each word needs only 10 of them.
- **Your graphics card is the desk.** It is small and fast. The roughly 4,000 books used most often lie open on it.
- **Your RAM is the shelves in the reading room.** All 24,576 books are there. When a word needs a book that is not on the
desk, your processor reads it from the shelf at once, while the card keeps working.
- **Your SSD is the archive.** It holds a 29 GB lookup table. The model reads a few rows of it per word, so it does not
need to sit in RAM.
<p align="center"><img src="docs/media/how-it-works.svg" width="860" alt="The model's 24,576 experts: the busiest on the graphics card, all of them in RAM, a lookup table on the SSD"></p>
- **The model is a team of 24,576 small specialists ("experts").** Each word needs only 10 of them.
- **Your graphics card** keeps the few thousand experts that are used most often. **Your RAM** holds all of them,
and **your processor** works on the rest at the same time. **Your SSD** holds a big lookup table.
A small helper also guesses the next few words. The big model checks all of them at once and keeps the right ones. You get
the same answer, 1.6-1.8x sooner. Long texts are read in big pieces, up to 8,192 tokens at a time, at over 1,000 tokens
per second.
<p align="center"><img src="docs/media/guess-and-check.svg" width="860" alt="A small helper guesses the next words; the big model checks them all at once and keeps the right ones"></p>
- **Guess, then check:** a small helper guesses the next few words. The big model checks them all at once. You get
the same answer, 1.6-1.8x sooner.
- **Long texts are read in big pieces** (up to 8,192 tokens at a time), at over 1,000 tokens per second.
The longer explanation: [docs/HOW_IT_WORKS.md](docs/HOW_IT_WORKS.md). Every part and its numbers:
[the details](docs/DETAILS.md#how-it-works) and the [paper](docs/paper/Strata-Paper.pdf).
## Something went wrong?
- **My PC froze the first time Strata started.** It loads the model into RAM. Wait, and don't close the window. Still
frozen after 10 minutes? Restart the PC, close other programs and try again, or pick a smaller size.
- **It stopped while downloading or installing.** Start the app or `strata launch` again, or run `START-HERE.bat` (or
`./setup.sh`) again. It continues where it stopped.
- **It is very slow, the disk light keeps blinking, or it says "the engine stopped unexpectedly".** Your PC does not have
enough free RAM. Close other programs (browsers use a lot), or pick a smaller size such as Q2_0 or IQ2_XS.
- **It says there is no engine for this PC.** In the app, open **Settings, Engine** and press **Update engine**. In the
Terminal, run `strata update`. `strata doctor` shows which engine files are installed.
- **It says port 8080 is already in use.** Strata is already running. Look for its window.
More problems and fixes: [docs/TROUBLESHOOTING.md](docs/TROUBLESHOOTING.md). Still stuck? Open an
[issue](https://github.com/Niko1221/Strata/issues). In the app, attach the file from **Settings, Engine, Diagnostics, View
logs, Save logs**. In the Terminal, paste the output of `strata doctor`. From source, attach `strata-<model>.log` from the
Strata folder. For a security problem, report it privately: [SECURITY.md](SECURITY.md).
## Credits and license
The model is [Qwen3.8-Flash-Next](https://huggingface.co/Qwen/Qwen3.8-Flash-Next) by the Qwen team. It was
compressed by [ISTA-DASLab](https://huggingface.co/ISTA-DASLab/Qwen3.8-Flash-Next-GSQ-RCO-GGUF), UkisAI (Swift 1.5)
The model is [Qwen3.8-Flash-Next](https://huggingface.co/Qwen/Qwen3.8-Flash-Next) by the Qwen team. It was compressed by
and Unsloth. Strata uses parts of [llama.cpp / ggml](https://github.com/ggml-org/llama.cpp). All credits:
[docs/HOW_IT_WORKS.md](docs/HOW_IT_WORKS.md#credits). Strata is open source under the [MIT License](LICENSE). A few
parts and every model have their own licenses ([which ones](docs/HOW_IT_WORKS.md#license)).
[docs/HOW_IT_WORKS.md](docs/HOW_IT_WORKS.md#credits). Strata is open source under the [MIT License](LICENSE). A few parts and
every model have their own licenses ([which ones](docs/HOW_IT_WORKS.md#license)).
## Contributing
Found a bug, or want to send a change or a benchmark? Read [CONTRIBUTING.md](CONTRIBUTING.md) first. It says what to
include so that others can check your work.
## Support Strata
Strata is free and open source. If it is useful to you, you can support its development:
Strata is free and MIT-licensed. I build it and test it on whatever hardware I can get. If it runs well on your PC, a coffee helps
me keep testing new cards and fixing what you report. No pressure. Using it and reporting what you find helps just as much.
<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>
<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>
Interested in sponsoring Strata? Contact me via [GitHub](https://github.com/Niko1221).
<p align="center"><b>Rode um modelo de IA de 125 bilhões de parâmetros no seu próprio PC gamer</b><br>
Placa de vídeo NVIDIA ou AMD (12 GB ou mais) · Windows ou Linux · gratuito e de código aberto</p>
<h3 align="center">Rode um modelo de ponta no seu próprio PC gamer</h3>
<p align="center"><a href="https://github.com/Niko1221/Strata/releases/download/v0.1.10/Pagoda.mp4"><img src="docs/media/pagoda-preview.webp" width="720" alt="Um jardim com pagode em voxels que o modelo do Strata escreveu, rodando no navegador"></a><br>
<sub>Um jardim com pagode em voxels, feito com um único prompt numa RTX 5070 com o Strata (IQ3_S, contexto de 128K) ·
<p align="center">Suporte a NVIDIA, AMD, Intel · Windows, Linux, Mac · gratuito e de código aberto</p>
<p align="center"><a href="https://github.com/Niko1221/Strata/releases/download/v0.1.10/Pagoda.mp4"><img src="docs/media/pagoda-preview.webp" width="720" alt="A voxel pagoda garden that Strata's model wrote, running in the browser"></a><br>
<sub>Um jardim de pagodes em voxels, gerado com um único prompt, rodando numa RTX 5070 com o Strata (IQ3_S, contexto de 128K) ·
O Strata roda o **[Qwen3.8-Flash-Next](https://huggingface.co/Qwen/Qwen3.8-Flash-Next)** num PC comum. É um modelo
de IA grande e inteligente que normalmente precisa de um servidor. Ele conversa, escreve código, entende imagens e
funciona com seus apps e agentes de programação. Nada sai do seu PC.
O Strata roda o **[Qwen3.8-Flash-Next](https://huggingface.co/Qwen/Qwen3.8-Flash-Next)**, um modelo de 125 bilhões de parâmetros que
normalmente precisa de um servidor, num PC comum. Ele conversa, escreve código, lê imagens e funciona com os seus aplicativos e
agentes de programação. Suas conversas e arquivos ficam no seu PC.
## Qual é a velocidade?
**Primeira vez aqui?** Veja [o que você precisa](#o-que-você-precisa) e depois [instale o app](#opção-a-o-app-strata).
Medimos em dois PCs gamer comuns. Um token equivale a cerca de ¾ de uma palavra.
- **Roda num PC gamer.** 32 GB somando memória RAM e memória da placa de vídeo já bastam para começar.
- **Funciona com as suas ferramentas.** Ele fala as APIs da OpenAI e da Anthropic, então Claude Code, Codex CLI, OpenCode e outros agentes se conectam a ele.
- **Lê imagens.** Já vêm ativadas.
- **Gratuito.** Licença MIT, sem conta, sem nuvem.
- **Privado.** Sem telemetria. O que o Strata baixa, quando usa a internet e como confere os downloads está em [docs/SAFETY.md](docs/SAFETY.md).
- **Escreve respostas:** a velocidade com que a resposta aparece numa conversa curta. 60 tokens por segundo é mais rápido do que você consegue ler.
- **Lê seu prompt:** a velocidade com que ele absorve o que você envia (aqui, um documento, código ou histórico de conversa de 32K tokens).
**Você precisa de pelo menos 32 GB somando a memória RAM e a memória da placa de vídeo (VRAM).** Num Mac ou num Ryzen AI Max, isso é a
memória unificada. Mais memória permite um modelo maior ou mais rápido.
</td><td>
- Um PC de 32 GB com uma placa de 12 a 16 GB roda o Coder.
- 64 GB de RAM rodam todos os tamanhos.
| Tamanho | Escreve respostas | Lê seu prompt |
| --- | ---: | ---: |
| **Q2_0** | 60 tokens/s | 1,160 tokens/s |
| **IQ2_XS** | 52 tokens/s | 1,110 tokens/s |
| **Coder** | 44 tokens/s | 1,420 tokens/s |
</td></tr>
</table>
NVIDIA: Q2_0 com o engine 0.1.36, as outras linhas com 0.1.26 (respostas de 4K, prompts de 32K). As tabelas
completas estão em [DETAILS.md](docs/DETAILS.md#speed-measured). Uma placa com mais VRAM é mais rápida: uma
RTX 3090 (24 GB) deve escrever cerca de 100-140 tokens por segundo. Conversas longas e outras placas:
[velocidade de cada modelo](docs/MODELS.md#how-fast-is-each-size), [resultados da comunidade](docs/COMMUNITY_BENCHMARKS.md).
<p align="center"><a href="https://buymeacoffee.com/strataengine"><img src="https://cdn.buymeacoffee.com/buttons/v2/default-yellow.png" alt="Buy Me A Coffee" height="50"></a><br>
<sub>O Strata é gratuito. Se ele roda bem no seu PC, um café ajuda a manter o trabalho nele.</sub></p>
## Do que você precisa
| | |
| Seu computador | Detalhes |
| --- | --- |
| **Placa de vídeo**| **NVIDIA** GeForce RTX séries 20, 30, 40 ou 50, ou **AMD** Radeon RX 7900 XT / XTX, RX 7800 XT / 7700 XT, RX 9060 XT, RX 9070 / 9070 XT, Radeon AI PRO R9700 ou séries RX 6800 / 6900. Ela precisa de **12 GB de VRAM ou mais**. |
| **RAM**| 32 GB ou mais. Sua RAM decide [qual modelo](#qual-modelo-devo-escolher) cabe. Com 64 GB, roda qualquer tamanho. |
| **Disco** | Cerca de 80 GB livres. Use um SSD se puder: a primeira inicialização fica bem mais rápida. |
| **Sistema** | Windows 10 / 11 ou Linux, e um driver de vídeo atualizado da NVIDIA ou da AMD. |
| **NVIDIA** GeForce RTX série 20, 30, 40, 50 (12 GB ou mais) | Instale o app ou o Terminal. |
| **AMD**Radeon RX 6800 e mais novas, Radeon AI PRO R9700 (12 GB ou mais) | [Placas AMD](docs/AMD_HIP.md) |
| **Mac com Apple Silicon** (M1 ou mais novo, macOS 15 ou mais novo) | [Mac](docs/MAC.md) |
| **AMD Ryzen AI Max (Strix Halo)** e outros sistemas de memória unificada | [Strix Halo](docs/STRIX_HALO.md) |
| Placas e processadores mais antigos | [GPUs antigas](docs/OLDER_GPUS.md), [CPUs antigas](docs/INSTALL.md#older-cpus-experimental) |
O instalador cuida de todo o resto. Duas ou três placas podem dividir o modelo ([multi-GPU](docs/MULTI_GPU.md)).
Você também precisa de:
Experimental, escrito e testado por membros da comunidade nas próprias máquinas:
- cerca de 80 GB de espaço livre no disco (um SSD deixa a primeira inicialização bem mais rápida)
- Windows 10 / 11, Linux ou macOS
- um driver de vídeo atualizado
- **Placas de vídeo mais antigas** (Tesla P40 / V100, GTX 10, Radeon VII / MI50, RX 6700 XT, RX 5500 XT): [Older GPUs](docs/OLDER_GPUS.md).
- **Intel Arc**, compilado a partir do código-fonte no Linux: [Intel Arc](docs/INTEL_ARC.md).
- **AMD Ryzen AI Max (Strix Halo)**, compilado a partir do código-fonte no Linux: [Strix Halo](docs/STRIX_HALO.md).
- **Processadores mais antigos sem AVX2**: funcionam, mas devagar. [Older CPUs](docs/INSTALL.md#older-cpus-experimental).
A lista completa: [docs/INSTALL.md](docs/INSTALL.md#what-you-need).
Duas ou três placas da mesma marca podem dividir um modelo ([multi-GPU](docs/MULTI_GPU.md)). O Strata nunca divide um modelo entre
placas NVIDIA e AMD. Lista completa: [docs/INSTALL.md](docs/INSTALL.md#what-you-need).
## Instalação
### Deixe sua IA instalar
Escolha uma opção. Todas iniciam o mesmo mecanismo e usam os mesmos modelos.
Você usa um assistente de programação com IA (Claude Code, Cursor, Codex, GitHub Copilot, ...)? Cole isto nele:
### Opção A: o app Strata
O jeito mais fácil. O app já traz o mecanismo: instale, escolha um modelo e converse. Baixe o arquivo para o seu sistema e a sua
placa de vídeo na [página de releases](https://github.com/Niko1221/Strata/releases/latest). Não sabe qual? Veja a explicação
logo abaixo da tabela.
| Sistema | Sua placa de vídeo | Arquivo para baixar |
| --- | --- | --- |
| **Windows 10 / 11** | NVIDIA RTX, driver 580 ou mais novo | [`Strata-0.2.3-windows-cuda13.exe`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.3-windows-cuda13.exe) |
| | NVIDIA, placa mais antiga ou driver anterior ao 580 | [`Strata-0.2.3-windows-cuda12.exe`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.3-windows-cuda12.exe) |
| | AMD Radeon série RX 9000 ou Radeon AI PRO R9700 | [`Strata-0.2.3-windows-hip.exe`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.3-windows-hip.exe) |
| | AMD Radeon série RX 7000 ou Ryzen AI Max (Strix Halo) | [`Strata-0.2.3-windows-hip-rdna3.exe`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.3-windows-hip-rdna3.exe) |
| **Linux** | NVIDIA RTX, driver 580 ou mais novo | [`Strata-0.2.3-linux-nvidia-cuda13.AppImage`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.3-linux-nvidia-cuda13.AppImage) |
| | NVIDIA, placa mais antiga ou driver anterior ao 580 | [`Strata-0.2.3-linux-nvidia-cuda12.AppImage`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.3-linux-nvidia-cuda12.AppImage) |
| | AMD Radeon série RX 9000 ou Radeon AI PRO R9700 | [`Strata-0.2.3-linux-amd-rx9070.AppImage`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.3-linux-amd-rx9070.AppImage) |
| | AMD Radeon série RX 7000 | [`Strata-0.2.3-linux-amd-rx7900.AppImage`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.3-linux-amd-rx7900.AppImage) |
| | AMD Ryzen AI Max (Strix Halo) | [`Strata-0.2.3-linux-amd-strixhalo.AppImage`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.3-linux-amd-strixhalo.AppImage) |
| **Mac** (Apple Silicon, macOS 15 ou mais novo) | integrada | [`Strata-0.2.3-mac.dmg`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.3-mac.dmg) |
**Se você estiver em dúvida:** para NVIDIA, use `cuda13` se o seu driver for a versão 580 ou mais nova, e `cuda12` nos outros casos. O `cuda12`
serve para placas e drivers mais antigos. Os arquivos `hip` e `amd` são para placas AMD. Outras placas AMD (RX 6000) ainda não têm app pronto: veja [Placas AMD](docs/AMD_HIP.md).
Depois:
- **Windows:** execute o `.exe`. Ele instala só para o seu usuário e não pede direitos de administrador. Se o Windows disser "O Windows
protegeu o computador", escolha **Mais informações** e depois **Executar assim mesmo** (o instalador ainda não tem assinatura de código).
- **Linux:** torne o arquivo executável (`chmod +x`) e inicie. É preciso ter o FUSE 2: `sudo apt install libfuse2` (Ubuntu 22.04)
ou `libfuse2t64` (Ubuntu 24.04 e mais novos).
- **Mac:** abra o `.dmg` e arraste o Strata para Aplicativos.
Na primeira tela, o app lê o seu PC e recomenda um modelo que caiba nele. Clique em instalar. O modelo é um download grande
(cerca de 40 a 70 GB) e continua de onde parou se for interrompido. Fechar a janela encerra o Strata e o mecanismo.
### Opção B: o Terminal (uma linha)
O Terminal é o comando `strata`, para scripts, servidores e agentes de IA.
Linux e Mac:
```sh
curl -fsSL https://github.com/Niko1221/Strata/releases/latest/download/install.sh | sh
O script confere o seu PC, baixa o Terminal, verifica o SHA-256 e descompacta na sua pasta pessoal. Não precisa de direitos de
administrador. No Windows, abra uma nova janela do terminal depois. Então inicie o Strata:
```sh
strata launch
```
Ele faz uma pergunta por vez: modelo, contexto, imagens, quem pode se conectar. Depois baixa o mecanismo para a sua placa de vídeo
(assinada e testada antes do uso) e o modelo, e começa a servir. Em scripts e agentes de IA, use flags:
```sh
strata launch --model q2_0 --yes --no-tui
```
Para ler o script de instalação antes de executá-lo, baixe-o primeiro com `curl -fsSL <address> -o install.sh`, leia e então rode
`sh install.sh`. Todas as telas e flags: [docs/TERMINAL_LAUNCH.md](docs/TERMINAL_LAUNCH.md).
### Opção C: deixe a sua IA configurar
Cole isto no Claude Code, Cursor, Codex, GitHub Copilot ou outro assistente de programação:
```text
Set up Strata on this PC for me: https://github.com/Niko1221/Strata - follow docs/AI_SETUP.md in that repository.
```
Ele verifica sua placa de vídeo, RAM e disco e escolhe o modelo que cabe. Depois instala, inicia e explica como
conectar seus apps. Ferramentas de IA também podem instalar, iniciar e parar o Strata pelo
[servidor MCP](docs/MCP_SERVER.md) dele.
Ele confere a sua placa de vídeo, a RAM e o disco, escolhe o modelo que cabe, instala e inicia, e diz como conectar os seus
aplicativos. Ferramentas de IA também podem instalar, iniciar e parar o Strata pelo [servidor MCP](docs/MCP_SERVER.md) dele.
### Ou faça você mesmo
Prefere compilar por conta própria? Veja [Compilar a partir do código-fonte](#compilar-a-partir-do-código-fonte).
[Baixe o Strata](https://github.com/Niko1221/Strata/archive/refs/heads/main.zip) e descompacte (ou use `git clone`).
**Windows:** clique duas vezes em **`START-HERE.bat`**. **Linux:** rode **`./setup.sh`** na pasta do Strata.
## Modelos
Os passos são os mesmos para NVIDIA e AMD. O instalador encontra sua placa e configura o engine certo para ela.
Ele faz algumas perguntas:
- qual modelo e qual tamanho,
- quanto contexto (quanto texto o modelo guarda na memória),
- se ele deve entender imagens.
Aperte Enter em cada uma para usar a resposta recomendada. Depois ele baixa o modelo (cerca de 70 GB) e o inicia.
Se o download parar, rode de novo: ele continua de onde parou. Seu navegador abre o app do Strata em
`http://127.0.0.1:8080`.
> **Enquanto o modelo inicia, seu PC pode ficar lento ou parar de responder por 1-3 minutos** (mais na primeira vez).
> O Strata carrega 35-55 GB na sua RAM e reserva parte dela para a placa de vídeo. Isso é normal. Espere e não
> feche a janela. A janela mostra o que o Strata está fazendo.
**Da próxima vez**, rode `START-HERE.bat` (ou `./setup.sh`) de novo. Ele inicia na hora e não baixa nada duas
vezes. Feche a janela para parar o modelo. `UPDATE.bat` (`./update.sh`) atualiza o Strata sem iniciá-lo.
Atualização, Docker, várias placas, onde ficam os arquivos e todas as opções: [docs/INSTALL.md](docs/INSTALL.md).
## Qual modelo devo escolher?
O instalador recomenda um de acordo com sua RAM. O mesmo modelo vem em vários tamanhos, mais ou menos comprimidos.
O app e o `stratalaunch` recomendam um modelo para a sua memória. É um único modelo em vários tamanhos, mais ou menos comprimidos.
Os tamanhos menores são mais rápidos. Os maiores são um pouco mais inteligentes.
| Sua RAM | Escolha | Por quê |
### Pouca memória? Experimente o Gyro
O [Gyro](https://huggingface.co/agentionai/Qwen3.8-Flash-Next-Gyro-GGUF) (da agentionai) é a menor forma de rodar este modelo.
| Tamanho | Memória necessária (RAM + VRAM) | Download | O que é |
| --- | ---: | ---: | --- |
| **Gyro-S** | 27,6 GB | 58,5 GB | Para PCs onde nenhum outro tamanho cabe. A qualidade é menor que a do IQ2_XS. |
| **Gyro-M** | 35 GB | 92,0 GB | Qualidade próxima à do IQ3_XXS, com cerca de 20% menos memória da GPU. Ele escreve as respostas 16% mais devagar que o Q2_0 numa RTX 3060 de 12 GB e 21% mais devagar numa R9700. |
O Gyro roda em placas NVIDIA e AMD (AMD é experimental) em placas Intel Arc no Linux (testado numa Arc A750) e, desde a 0.2.2, em Macs com Apple Silicon (Gyro-S num Mac mini de 32 GB: 9 tokens/s, com os especialistas lidos do SSD). Detalhes e medições:
[docs/GYRO.md](docs/GYRO.md). Inicie com `strata launch --model gyro-s`.
| **32 GB** | **Coder** | cabe em 32 GB e é feito para código (com uma placa de 24 GB, Q2_0 e IQ2_XS também rodam) |
| **48 GB** | **IQ2_XS** (ou Q2_0, o mais rápido) | os tamanhos maiores não cabem |
| **64 GB** | **IQ2_XS** (recomendado), ou IQ3_XXS / IQ3_S | todos os tamanhos cabem; IQ3_S é o melhor e o mais lento |
| **96 GB ou mais** | **IQ3_S**, ou o UD-IQ4_XS da Unsloth (~4-bit) | espaço para os maiores tamanhos com todo o resto aberto |
| **32 GB** | **Gyro-S**, ou o **[Coder](docs/MODELS.md#coder)** para código | O Coder precisa de 32 GB de RAM e mantém metade dos experts, então é mais fraco fora de código |
| **40 GB** | **[Swift 1.5](docs/MODELS.md#swift-15)** IQ2_XS (39,2 GB), ou **Gyro-M** (35 GB) | O Swift pensa por menos tempo antes de responder. O Gyro-M chega perto do IQ3_XXS em qualidade, mas escreve as respostas 16 a 21% mais devagar que o Q2_0 |
| **48 GB** | **IQ3_XXS** (47,0 GB) | Mais lento, melhor |
| **64 GB** | **IQ3_S** (54,8 GB), o original ou o Swift 1.5 | O mais lento, o melhor: empata com o modelo completo nos testes publicados |
| **96 GB ou mais** | **UD-IQ4_XS** da Unsloth (cerca de 72 GB de RAM) até **Q8_0** (cerca de 136 GB), de 4 a 8 bits | Mais perto do modelo completo. Com menos RAM, o Strata lê parte deles do SSD, o que é bem mais lento |
- **[Coder](docs/MODELS.md#coder):** uma versão para programação com metade dos experts removida. Ela alcança 91%
da pontuação do modelo completo no SWE-bench Verified (medido pelos autores) e cabe em 32 GB de RAM. É mais fraca
fora do código, inclusive em chinês e outros textos CJK (#438). Para isso, escolha Q2_0, IQ2_XS ou IQ3_S, que
mantêm todos os experts.
- **[Swift 1.5](docs/MODELS.md#swift-15):** um fine-tune que pensa por bem menos tempo antes de responder. Você
recebe a resposta mais cedo, com quase a mesma qualidade.
- **[Unsloth UD-IQ4_XS](docs/MODELS.md#unsloth-ud-iq4_xs):** a versão de ~4 bits da Unsloth, entre IQ3_S e
UD-Q4_K_XL em qualidade. Um download de 94 GB. Com menos de ~80 GB de RAM, o Strata lê uma parte dele do SSD
enquanto responde, então ali ele fica mais lento (um SSD NVMe ajuda).
- **[Unsloth UD-Q4_K_XL](docs/MODELS.md#unsloth-ud-q4_k_xl-experimental)** (experimental): o mais próximo do modelo
completo. Mas o Strata lê a maior parte dele do SSD enquanto responde, então ele escreve só 7-8.5 tokens/s num PC
com 64 GB.
- **[OrcaRouter's Uncensored IQ3_XXS](docs/MODELS.md#orcarouter-uncensored-iq3_xxs):** você configura à mão. Ele
não está no menu do instalador.
Com menos RAM do que a linha indica, ainda pode funcionar: o Strata muda sozinho para o
[modo de pouca RAM](docs/MODELS.md#a-big-graphics-card-and-little-ram). Espere mais lentidão, e muito mais com uma placa de vídeo pequena.
Tamanhos, downloads e o que cabe onde: [docs/MODELS.md](docs/MODELS.md). Para adicionar outro modelo depois, rode
`SETUP.bat` (Linux: `./setup.sh --setup`).
Mais opções:
- **[Swift 1.5](docs/MODELS.md#swift-15)** é um ajuste fino que pensa por menos tempo antes de responder, com qualidade e RAM parecidas.
- **[NVFP4](docs/NVFP4.md)** é a versão de 4 bits da NVIDIA para placas NVIDIA. Precisa de cerca de 80 GB de RAM para manter todos os experts na RAM.
- **[Qwen3.6-35B-A3B](docs/QWEN36.md)** é outro modelo, menor, que lê imagens e pode atender vários pedidos ao mesmo tempo.
- **[Uncensored IQ3_XXS da OrcaRouter](docs/MODELS.md#orcarouter-uncensored-iq3_xxs)** é configurado manualmente.
Todos os tamanhos, downloads e o que cabe em cada configuração: [docs/MODELS.md](docs/MODELS.md).
### DeepSeek V4.1-Flash (no Terminal, a partir da 0.2.4)
[DeepSeek V4.1-Flash](https://huggingface.co/ISTA-DASLab/DeepSeek-V4.1-Flash-1017-GSQ-RCO-GGUF) é outro modelo, bem maior. A ISTA-DASLab fez os três conjuntos abaixo. O Strata os executa em um PC com uma placa de vídeo de 12 GB e de 46 a 64 GB de RAM, e lê o resto do seu SSD. Ele roda em placas NVIDIA RTX das séries 30 a 50 e na Radeon AI PRO R9700. Usa um pacote de motor próprio, `strata-dsv41`, que o `strata launch` baixa quando você escolhe o DeepSeek. O app de desktop ainda não o oferece.
| Conjunto | Download | Qualidade em relação ao modelo original | Responde a |
| --- | ---: | --- | --- |
| **IQ1_M** (1,75 bits) | 239 GB | KL 0,217, perplexidade 3,80 (+21%); código +8,9%, tarefas de agente +10,2%; mais fraco em prosa comum em inglês. Segundo o cartão do modelo da ISTA-DASLab, mantém 98,7% da média de tarefas do modelo base (AIME25, GPQA-Diamond, LiveCodeBench v6). | ainda não medida |
| **IQ2_XXS** (2.00 bits) | 257 GB | KL 0,170, perplexidade 3,63 (+15,5%); código +8,6%, tarefas de agente +5,3% | 6 a 9 tokens/s em uma RTX 3060, cerca de 5 em uma RTX 5070 |
| **IQ2_XS** (2.25 bits) | 274 GB | KL 0,107, perplexidade 3,37 (+7,3%); código +3,2%, tarefas de agente +2,2% | cerca de 11 tokens/s em uma Radeon AI PRO R9700 |
| **IQ4_NL** (tabela Engram, não é um terceiro modelo) | 111 GB, já incluídos nos três downloads acima | A tabela de consulta Engram de 4 bits que o modelo lê da RAM ou do SSD. Os três conjuntos usam este único arquivo (`DeepSeek-V4.1-Flash-Engram-IQ4_NL.gguf`). | n/a |
4x Radeon AI PRO R9700: cerca de 31 tokens/s, 55 a 69 com DSpark.
Inicie com `strata launch --model ist-das-2.00bpw` (ou `ist-das-1.75bpw`, `ist-das-2.25bpw`). Detalhes, velocidades por placa e a instalação manual: [docs/DEEPSEEK41.md](docs/DEEPSEEK41.md).
## Qual é a velocidade?
Com base numa placa de vídeo de 12 GB (RTX 5070, Ryzen 5 7600, 64 GB de RAM). Veja resultados de outras placas:
[velocidade de cada modelo](docs/MODELS.md#how-fast-is-each-size), [resultados da comunidade](docs/COMMUNITY_BENCHMARKS.md).
| Modelo | As respostas aparecem a | Lê o seu prompt a |
- **Raciocínio:** escolha **off, low, medium ou high** no menu do chat ou no "reasoning effort" do seu app. Off é o
mais rápido. High é o melhor para perguntas difíceis.
- **Imagens:** responda sim para "Images?" na instalação. Depois clique em **Picture** no chat, ou anexe imagens no
seu app. Placas AMD entendem imagens no Linux usando o processador; no Windows, ainda não.
- **Do seu celular ou de outro PC:** `START-HERE.bat --setup --host 0.0.0.0 --api-key <secret>`. Sempre defina uma chave.
- **Uma requisição por vez:** por padrão, o Strata responde uma requisição e as outras esperam. Para responder
várias ao mesmo tempo, defina `"parallel": 2` ([BATCHING.md](docs/BATCHING.md)). Numa placa de 12 GB, isso deixa
cada resposta mais lenta.
- **Prompts longos:** o Strata lê a primeira mensagem de uma conversa inteira, cerca de 1 minuto a cada 30,000
tokens. As mensagens seguintes começam em segundos.
O app tem um**Chat**, um **Monitor**do modelo e da sua GPU, CPU e RAM, uma página de **Modelos** e as **Configurações**. O chat
pode trabalhar numa pasta do seu PC. No modo **Manual**, ele pergunta antes de alterar um arquivo ou executar um comando. Digite
`/loop 10m <o que fazer>` para repetir uma mensagem a cada 10 minutos enquanto o chat estiver aberto.
Mais: [onde suas conversas ficam salvas](docs/INSTALL.md#where-things-are-stored), [a API](docs/DETAILS.md#using-it).
O chat pode pesquisar na web com o DuckDuckGo (ou o Bing), sem conta. Você pode adicionar uma chave de busca em **Configurações, Ferramentas**.
A busca é o único recurso que envia algo para fora do seu PC: os termos da pesquisa.
## Algo deu errado?
Para rodar dois modelos, ative **Executar vários modelos** e dê a cada modelo a sua própria placa da mesma marca.
- **Meu PC travou na primeira vez que o Strata iniciou.** Isso é normal enquanto ele carrega o modelo. Espere e não
feche a janela. Ainda travado depois de 10 minutos? Reinicie o PC, feche outros programas e tente de novo, ou
escolha um tamanho menor.
- **Parou durante o download ou a instalação.** Rode `START-HERE.bat` (ou `./setup.sh`) de novo. Ele continua de
onde parou.
- **Está muito lento e a luz do disco não para de piscar, ou aparece "the engine stopped unexpectedly".** Seu PC
não tem RAM livre suficiente. Feche outros programas (navegadores usam muita), ou escolha um tamanho menor (Q2_0
ou IQ2_XS).
- **Diz que a porta 8080 já está em uso.** O Strata já está rodando. Procure a janela dele.
<p align="center"><img src="docs/media/app-chat.png" width="900" alt="The Strata app: a chat where the agent built a smallvoxel pagoda garden, with the page open in the preview on the right"><br>
<sub>Um chat no app Strata: o agente escreveu um pequeno jardim de pagode em voxels em um único arquivo HTML, e a página abre na pré-visualização ao lado da conversa</sub></p>
Mais problemas e como resolver: [docs/TROUBLESHOOTING.md](docs/TROUBLESHOOTING.md). Ainda sem solução? Abra uma
[issue](https://github.com/Niko1221/Strata/issues) e anexe o `strata-<model>.log` da pasta do Strata. Encontrou um
problema de segurança? Informe em particular: [SECURITY.md](SECURITY.md).
### No Terminal
<p align="center"><img src="docs/media/terminal-monitor.png" width="900" alt="The Strata Terminal monitor overview while the model writes a reply"><br>
<sub><code>strata monitor</code> enquanto o modelo responde: velocidade, GPU, memória e as solicitações recentes</sub></p>
```sh
strata launch # telas guiadas: modelo, contexto, imagens, acesso e depois servir
strata launch --model q2_0 --yes --no-tui # sem perguntas: serve, mostra o endereço e volta
strata launch --plan --model q2_0 # mostra o que aconteceria; nada muda
strata launch --status # o que está sendo servido agora
Um agente instala o Strata com o comando de uma linha da Opção B e executa
`strata launch --model NAME --yes --no-tui`. Os passos, as flags e os códigos de saída estão em [docs/AI_SETUP.md](docs/AI_SETUP.md),
e as regras do repositório estão em [AGENTS.md](AGENTS.md).
### Conecte os seus outros aplicativos
| Tipo de aplicativo | URL base |
| --- | --- |
| Compatível com OpenAI (a maioria dos apps e agentes) | `http://127.0.0.1:8080/v1` (qualquer chave de API, qualquer nome de modelo) |
| API da Anthropic (Claude Code e outros) | `http://127.0.0.1:8080`, sem `/v1`. Esses aplicativos adicionam `/v1/messages` sozinhos. Claude Code: `ANTHROPIC_BASE_URL=http://127.0.0.1:8080` |
| OpenAI Responses API (Codex CLI) | `http://127.0.0.1:8080/v1/responses` ([configuração](docs/DETAILS.md#the-responses-api-and-codex-cli)) |
`strata connect` mostra as configurações exatas para OpenCode, Codex CLI, Claude Code, oh-my-pi, pi, Hermes Agent e OpenClaw.
`strata connect opencode --write` grava o arquivo depois de perguntar e mantém um backup.
### Imagens
As imagens vêm ativadas nos modelos que sabem lê-las. Anexe ou cole uma imagem no chat, ou envie pelo seu aplicativo.
O leitor de imagens roda no processador quando a sua placa de vídeo tem menos de 24 GB (alguns segundos por imagem, sem tirar nada
da memória da placa) e roda na placa de vídeo a partir de 24 GB. A regra é a mesma para NVIDIA e AMD. Desative as imagens
por modelo, ou escolha o leitor com `--vision off`, `--vision gpu` ou `--vision cpu`.
Os números por trás da regra: numa Radeon AI PRO R9700, o leitor da placa levou 57 ms para uma imagem de 260 tokens e o do
processador levou 3,1 s. Em placas de 16 GB (Tesla P100 e RTX A4000, Linux), o leitor da placa custou de 3 a 5% da velocidade do texto,
porque ocupa cerca de 1,4 GB que o modelo usaria de outra forma. No Mac, a GPU faz a leitura, seja qual for a memória (0,45 s para uma imagem de 260 tokens em um M4 de 32 GB, cerca de 1,2 GB da memória compartilhada);
o processador leva cerca de 50 s por imagem, então só lê com `--vision cpu`.
### Bom saber
- Escolha o nível de raciocínio (**off, low, medium ou high**) no menu do chat ou no "reasoning effort" do seu aplicativo. Off é o mais rápido.
- Por padrão, o Strata responde a um pedido por vez. Para vários ao mesmo tempo, use `--parallel N` no `strata launch` ou
`"parallel": 2` na configuração ([BATCHING.md](docs/BATCHING.md)). Numa placa de 12 GB, isso deixa cada resposta mais lenta.
- O Strata lê a primeira mensagem de uma conversa por inteiro, cerca de 1 minuto a cada 30.000 tokens. As mensagens seguintes começam em segundos.
- Onde ficam as suas conversas e a API completa: [docs/INSTALL.md](docs/INSTALL.md#where-things-are-stored), [docs/DETAILS.md](docs/DETAILS.md#using-it).
## Compilar a partir do código-fonte
<details>
<summary>Windows, Linux e Mac a partir do repositório</summary>
[Baixe o Strata](https://github.com/Niko1221/Strata/archive/refs/heads/main.zip) e descompacte, ou use `git clone`.
**Windows:** dê dois cliques em `START-HERE.bat`. **Linux:** execute `./setup.sh` na pasta do Strata.
**Mac (Apple Silicon, macOS 15 ou mais novo):** instale uma vez as Command Line Tools da Apple com `xcode-select --install`
(o Xcode em si não é necessário). Abra o Terminal na pasta do Strata e execute `./setup.sh`. Ele compila o mecanismo do Mac no seu
Mac (cerca de 7 minutos), faz as mesmas perguntas, baixa o modelo e inicia. Da próxima vez, `./setup.sh` inicia
na hora. Qual tamanho cabe no seu Mac: [docs/MAC.md](docs/MAC.md).
O instalador encontra a sua placa e configura o mecanismo certo. Ele pergunta qual modelo e tamanho, quanto contexto (quanto texto
o modelo mantém em mente) e se deve ler imagens. Pressione Enter para aceitar a resposta recomendada. Depois ele baixa o
modelo (cerca de 70 GB) e inicia. Se o download parar, execute de novo e ele continua. O navegador abre a página do Strata em
`http://127.0.0.1:8080`.
> Enquanto o modelo inicia, o seu PC pode ficar lento ou parar de responder por 1 a 3 minutos (mais tempo na primeira vez). O Strata carrega
> de 35 a 55 GB na sua RAM e trava parte dela para a placa de vídeo. Espere e não feche a janela.
Da próxima vez, execute `START-HERE.bat` (ou `./setup.sh`) de novo. Ele inicia na hora e não baixa nada duas vezes. Feche a janela
dele para parar o modelo. `UPDATE.bat` (`./update.sh`) atualiza o Strata sem iniciá-lo. Atualização, Docker, várias placas,
onde ficam os arquivos e todas as opções: [docs/INSTALL.md](docs/INSTALL.md).
Para medir o seu PC, execute `BENCH.bat` (`./bench.sh`). Leva de 5 a 10 minutos e gera um relatório que você pode compartilhar
**Opcional: mais velocidade numa placa quando o modelo inteiro cabe na RAM.** Duas configurações opcionais do RubenTorney (#1904, #1905).
Elas ajudam quando você usa uma placa de vídeo e a sua RAM comporta todos os experts, por exemplo Q2_0 ou IQ3_XXS num PC de 64 GB. Abra
`strata-<model>.json` na pasta de dados do Strata ([onde fica](docs/INSTALL.md#where-things-are-stored)) e adicione:
-`"--adapt-async", "1"` à lista `"args"`. O cache de experts passa a ser atualizado numa thread auxiliar, em vez de pausar a
resposta. Medido numa RTX 5070 (Windows, Q2_0): +4 a 5% na geração, +7 a 10% em código e documentos longos. As respostas podem diferir
um pouco do padrão e continuam iguais de uma execução para outra.
-`"STRATA_PIN_BY_NEED": "1"` no bloco `"env"` (Windows). O Windows só consegue travar cerca de metade da RAM para a placa
de vídeo, e isso trava primeiro os experts de que a placa precisa. Medido na RTX 5070: +4,4% na leitura do prompt.
```json
"args":[...,"--adapt-async","1"],
"env":{"STRATA_PIN_BY_NEED":"1"}
```
Reinicie o Strata depois da alteração. Para desfazer, remova as duas entradas.
</details>
## Como funciona?
Modelos como este normalmente rodam em servidores com centenas de gigabytes de memória de vídeo. Sua placa de vídeo
tem 12-24 GB. O Strata faz o modelo caber **dividindo o trabalho pelo PC inteiro**. Pense numa cozinha: o que você
usa o tempo todo fica na bancada, e o resto espera na despensa.
Modelos como este costumam rodar em servidores com centenas de gigabytes de memória de vídeo. A sua placa de vídeo tem
12 a 24 GB. O Strata faz o modelo caber dividindo o trabalho entre o PC inteiro. Imagine uma biblioteca.
<p align="center"><img src="docs/media/how-it-works.svg" width="860" alt="Os 24,576 experts do modelo: os mais usados na placa de vídeo, todos na RAM, uma tabela de consulta no SSD"></p>
- O modelo é uma equipe de 24.576 pequenos especialistas ("experts"), como 24.576 livros de consulta. Cada palavra precisa de apenas 10 deles.
- **A sua placa de vídeo é a mesa.** Ela é pequena e rápida. Os cerca de 4.000 livros mais usados ficam abertos nela.
- **A sua RAM são as estantes da sala de leitura.** Os 24.576 livros estão lá. Quando uma palavra precisa de um livro que não está na
mesa, o seu processador lê da estante na hora, enquanto a placa continua trabalhando.
- **O seu SSD é o arquivo.** Ele guarda uma tabela de consulta de 29 GB. O modelo lê algumas linhas dela por palavra, então ela não
precisa ficar na RAM.
- **O modelo é uma equipe de 24,576 pequenos especialistas ("experts").** Cada palavra precisa de só 10 deles.
- **Sua placa de vídeo** guarda os poucos milhares de experts usados com mais frequência. **Sua RAM** guarda todos
eles, e **seu processador** trabalha no resto ao mesmo tempo. **Seu SSD** guarda uma grande tabela de consulta.
<p align="center"><img src="docs/media/how-it-works.svg" width="860" alt="The model's 24,576 experts: the busiest on the graphics card, all of them in RAM, a lookup table on the SSD"></p>
<p align="center"><img src="docs/media/guess-and-check.svg" width="860" alt="Um pequeno ajudante adivinha as próximas palavras; o modelo grande confere todas de uma vez e fica com as certas"></p>
Um pequeno ajudante também adivinha as próximas palavras. O modelo grande confere todas de uma vez e fica com as certas. Você recebe
a mesma resposta, de 1,6 a 1,8 vez mais rápido. Textos longos são lidos em blocos grandes, de até 8.192 tokens por vez, a mais de 1.000 tokens
por segundo.
- **Adivinhar e conferir:** um pequeno ajudante adivinha as próximas palavras. O modelo grande confere todas de
uma vez. Você recebe a mesma resposta, 1.6-1.8x mais cedo.
- **Textos longos são lidos em grandes pedaços** (até 8,192 tokens por vez), a mais de 1,000 tokens por segundo.
<p align="center"><img src="docs/media/guess-and-check.svg" width="860" alt="A small helper guesses the next words; the big model checks them all at once and keeps the right ones"></p>
A explicação mais longa: [docs/HOW_IT_WORKS.md](docs/HOW_IT_WORKS.md). Cada parte e seus números:
[os detalhes](docs/DETAILS.md#how-it-works) e o [artigo](docs/paper/Strata-Paper.pdf).
## Algo deu errado?
- **Meu PC travou na primeira vez que o Strata iniciou.** Ele carrega o modelo na RAM. Espere e não feche a janela. Ainda
travado depois de 10 minutos? Reinicie o PC, feche outros programas e tente de novo, ou escolha um tamanho menor.
- **Parou durante o download ou a instalação.** Abra o app ou o `strata launch` de novo, ou execute `START-HERE.bat` (ou
`./setup.sh`) outra vez. Ele continua de onde parou.
- **Está muito lento, a luz do disco não para de piscar, ou aparece "o mecanismo parou inesperadamente".** O seu PC não tem
RAM livre suficiente. Feche outros programas (os navegadores usam muita), ou escolha um tamanho menor, como Q2_0 ou IQ2_XS.
- **Diz que não há mecanismo para este PC.** No app, abra **Configurações, Mecanismo** e clique em **Atualizar mecanismo**. No
Terminal, execute `strata update`. O `strata doctor` mostra quais arquivos do mecanismo estão instalados.
- **Diz que a porta 8080 já está em uso.** O Strata já está rodando. Procure a janela dele.
Mais problemas e soluções: [docs/TROUBLESHOOTING.md](docs/TROUBLESHOOTING.md). Ainda sem resolver? Abra uma
[issue](https://github.com/Niko1221/Strata/issues). No app, anexe o arquivo de **Configurações, Mecanismo, Diagnóstico, Ver
registros, Salvar logs**. No Terminal, cole a saída de `strata doctor`. Se estiver rodando pelo código-fonte, anexe o `strata-<model>.log` da pasta do
Strata. Para um problema de segurança, informe em particular: [SECURITY.md](SECURITY.md).
## Créditos e licença
O modelo é o [Qwen3.8-Flash-Next](https://huggingface.co/Qwen/Qwen3.8-Flash-Next), da equipe Qwen. Ele foi
comprimido por [ISTA-DASLab](https://huggingface.co/ISTA-DASLab/Qwen3.8-Flash-Next-GSQ-RCO-GGUF), UkisAI (Swift 1.5)
O modelo é o [Qwen3.8-Flash-Next](https://huggingface.co/Qwen/Qwen3.8-Flash-Next), da equipe Qwen. Ele foi comprimido por
e Unsloth. O Strata usa partes do [llama.cpp / ggml](https://github.com/ggml-org/llama.cpp). Todos os créditos:
[docs/HOW_IT_WORKS.md](docs/HOW_IT_WORKS.md#credits). O Strata é de código aberto sob a [MIT License](LICENSE).
Algumas partes e todos os modelos têm licenças próprias ([quais](docs/HOW_IT_WORKS.md#license)).
[docs/HOW_IT_WORKS.md](docs/HOW_IT_WORKS.md#credits). O Strata é de código aberto sob a [Licença MIT](LICENSE). Algumas partes e
todos os modelos têm licenças próprias ([quais](docs/HOW_IT_WORKS.md#license)).
## Contribuindo
Encontrou um bug, ou quer enviar uma alteração ou um benchmark? Leia primeiro o [CONTRIBUTING.md](CONTRIBUTING.md). Ele diz o que
incluir para que outras pessoas possam conferir o seu trabalho.
## Apoie o Strata
O Strata é gratuito e de código aberto. Se ele é útil para você, você pode apoiar o desenvolvimento:
O Strata é gratuito e tem licença MIT. Eu o desenvolvo e testo em todo hardware que consigo arranjar. Se ele roda bem no seu PC, um café me ajuda
a continuar testando placas novas e corrigindo o que você relatar. Sem pressão. Usar e relatar o que encontrar ajuda tanto quanto.
<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>
<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>
Quer patrocinar o Strata? Fale comigo pelo [GitHub](https://github.com/Niko1221).
<p align="center"><a href="https://github.com/Niko1221/Strata/releases/download/v0.1.10/Pagoda.mp4"><img src="docs/media/pagoda-preview.webp" width="720" alt="A voxel pagoda garden that Strata's model wrote, running in the browser"></a><br>
<p align="center"><img src="docs/media/app-chat.png" width="900" alt="The Strata app: a chat where the agent built a small voxel pagoda garden, with the page open in the preview on the right"><br>
<sub>Strata 应用中的一次对话:智能体用一个 HTML 文件写出了一个小型体素宝塔花园,页面在对话旁的预览中打开</sub></p>
### 在终端里
<p align="center"><img src="docs/media/terminal-monitor.png" width="900" alt="The Strata Terminal monitor overview while the model writes a reply"><br>
<p align="center"><img src="docs/media/how-it-works.svg" width="860" alt="The model's 24,576 experts: the busiest on the graphics card, all of them in RAM, a lookup table on the SSD"></p>
<p align="center"><img src="docs/media/guess-and-check.svg" width="860" alt="A small helper guesses the next words; the big model checks them all at once and keeps the right ones"></p>
<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>
@@ -39,6 +39,57 @@ this PC only. The details and every setting are in [docs/DETAILS.md](docs/DETAIL
- **MCP tools** are opt-in: only the servers you put in the run config, only for requests from Strata's own page
that ask for them. They run with your user's rights, and the model decides when to call them.
- **The request monitor** (`/api-monitor`, which keeps the last prompts and answers in memory) is off unless
`"api_monitor": true` is set.
`"api_monitor": true` is set. Apart from it, the server keeps in its process memory the text and tokens of its
last few prompts, whatever `"api_monitor"` says, to encode the next turn faster; no endpoint shows them, and they
are gone when the server stops.
## The desktop app and the Terminal
Strata comes in three parts that share one engine: the **desktop app**, the **Terminal** (`strata`) and the **engine** that runs
the model. The section above is about the engine's HTTP server. This is what the other two add.
- **The app's host listens on this PC only.** The app serves its page from a local host process on `127.0.0.1` (port 8791 unless
changed). A request to its `/api/` endpoints that carries a browser `Origin` from any other site gets 403. The app shell holds a
random secret (32 characters or more, compared in constant time) that is needed to quit the host or tell it which window is open,
and the host gives its own helper processes a second random secret. By default the models the app starts are served on this PC only.
- **Sharing is a separate, key-protected listener.** "Share with other devices" (and the Cloudflare tunnel) will not start until an
API key is set. It opens a different listener that answers only `/v1/models`, `/v1/chat/completions` and `/v1/messages`, never the
app's page or its host endpoints.
- **App and Terminal talk through an owner channel.** One owner process per installation starts and ends the engine, and a second
client (the Terminal) attaches to it. On macOS and Linux that is a private Unix socket in a folder only you can open. On Windows it
is a TCP listener on `127.0.0.1` that answers only requests carrying a 256-bit token kept in an owner-only folder. That is a weaker
guarantee than a socket other users cannot open: on Windows the token is what protects it, as with any loopback service that has a
token (`strata_control/owner_transport.py`). The bridge between the app shell and the host (used for the app update) is also loopback and protected by a token.
- **Starting a model and serving never download anything.** Downloads and updates are one explicit step: `strata update`, or "Update
engine" and "Update Strata" in the app's Settings. Models and saved tuning are never replaced by an update.
- **Updates come from a signed feed.** The release publishes `strata-feed.json` and `strata-feed.json.sig` (Ed25519). An installed
Strata checks the signature against the public key it carries (`strata_control/update_keys.json`), refuses a feed older than the last
one it accepted (a replayed old feed) or expired, and checks every engine package against its SHA-256, its signed manifest and the
card in your PC before it test-starts it and switches to it. Any failure keeps the engine you had. The app installer itself is signed
with a separate key (Tauri's minisign key) that the app and the Terminal both check. The private keys are kept offline by the owner.
See [packaging/UPDATES.md](packaging/UPDATES.md).
### How far to trust `curl | sh` and `irm | iex`
The Terminal installs with one line:
```sh
curl -fsSL https://github.com/Niko1221/Strata/releases/latest/download/install.sh | sh
Piping a script into a shell runs whatever the server sends, so it deserves the same care as running any downloaded program.
What the script does is short and written at its top: it fetches the Terminal archive named in the release's `install-manifest.txt`,
checks its SHA-256, unpacks it into your home folder (no administrator rights, nothing outside your home), checks the release's signed feed
with the Terminal it just unpacked and removes it again if that fails, and puts `strata` on your PATH.
Those checks have a limit. The checksum comes from the same release as the archive, and the public key that checks the feed arrives inside
that archive, so for the first install all trust rests on HTTPS and on the `Niko1221/Strata` GitHub release itself. The checks catch a
damaged or half-finished download and a feed that was not signed by the release key; they do not protect you if the release has been replaced
by someone who controls it. Later updates are checked against the key already on your PC, which is a stronger position.
If that is not enough: save the script (`curl -fsSLO https://github.com/Niko1221/Strata/releases/latest/download/install.sh`), read it, and
run it from the file; or install the desktop app or build from source instead and check the code you build.
Strata has not had an outside security audit yet.
What Strata downloads, when it uses the internet, what stays on your PC and how downloads are checked is written up in [docs/SAFETY.md](docs/SAFETY.md).
rem Strata for Windows: the first run installs everything and starts the model; later runs just start it.
rem Needs only an NVIDIA or AMD graphics driver. Python is installed for your user account if it is missing (no admin needed).
setlocal
rem Python reads and writes text as UTF-8, not in the Windows code page (cp1251, cp866, ...): a folder or user name with Cyrillic or other letters stays intact (#3202).
set"PYTHONUTF8=1"
title Strata
cd /d "%~dp0"
rem A bundled Python runtime (the python\ folder of the terminal archive) is used when it is there: nothing to find,
rem install or download. SETUP.bat and UPDATE.bat run this file, so they use it too.
ifexist"python\python.exe"gotobundled
ifexist".venv\Scripts\python.exe"gotorun
call:findpy
@@ -36,6 +41,14 @@ echo Could not create the Python environment in .venv
* --family qwen --model IQ3_XXS --data <data folder that has the model files> [--smi "nvidia-smi --query-gpu=memory.used --format=csv,noheader,nounits"]
constchat=awaitcall('/v1/chat/completions','POST',{model:instance?.apiModelId??modelId.toLowerCase(),max_tokens:400,stream:false,temperature:0,messages:[{role:'user',content:'Reply with exactly one word: pong'}]});
record('the host sees two or more graphics cards',gpus.filter(g=>g.usable!==false).length>=2,JSON.stringify(gpus.map(g=>({i:g.index,n:g.name,gb:g.vram_gb}))));
record('the model list offers the cards choice for this model',!!opt&&opt.options.length>=2,JSON.stringify(opt?.options?.map(o=>({key:o.key,cards:o.cards,verdict:o.verdict,rec:o.recommended}))));
record('a model that fits one card recommends the single largest card (the split is offered, not forced)',opt?.recommended==='single'&&opt.options.find(o=>o.key==='split'),`recommended=${opt?.recommended}`);
record(`load with ${label} starts the engine`,loaded.job.state==='done'&&instance?.status==='ready',`${loaded.job.state}${loaded.job.error??''}; instance=${instance?.status}; gpus=${JSON.stringify(instance?.gpus)}`);
record(`the instance runs on exactly the chosen card(s)`,JSON.stringify([...(instance?.gpus??[])].sort())===JSON.stringify([...cards].sort()),`want ${JSON.stringify(cards)} got ${JSON.stringify(instance?.gpus)}`);
record(`only the chosen card(s) hold the model (graphics memory in use: ${used.join(', ')} MiB)`,JSON.stringify(holding)===JSON.stringify([...cards].sort()),`holding ${JSON.stringify(holding)}`);
for(constneedof[nodeExe,join(control,'strata.py'),join(repoHost,'dist','index.html')])if(!existsSync(need)){console.error(`missing ${need} (run stage-control.py, fetch-node and "npm run build" in apps/host first)`);process.exit(2);}
check((state?.catalog?.families?.length??0)>0&&sizes>=3,'the catalog has real families and sizes',`${state?.catalog?.families?.length} families, ${sizes} sizes`);
check(!!state?.catalog?.recommendation,'the catalog has a recommendation',JSON.stringify(state?.catalog?.recommendation??null).slice(0,120));
check(Array.isArray(state?.hardware?.gpus),'the hardware lists GPUs',(state?.hardware?.gpus??[]).map(g=>`${g.name}${g.vram_gb} GB`).join('; ')||'none on this PC');
check(state?.installDir&&realpathSync.native(state.installDir).toLowerCase().startsWith(realpathSync.native(work).toLowerCase()),'models go to a writable per-user folder, not the bundle',state?.installDir);
check(plan.status===200&&!!plan.body?.plan,'the install plan for the recommended model is a plan (nothing downloaded)',plan.body?.summary?.slice(0,100)??JSON.stringify(plan.body).slice(0,150));
check(!existsSync(join(work,'Strata-data','models')),'a plan wrote no model files');
constcontrolJson=(...argv)=>{constr=control(...argv);letjson=null;try{json=JSON.parse(r.stdout.slice(r.stdout.indexOf('{')));}catch{/* not json */}return{code:r.status,json,text:r.stdout+r.stderr};};
record('fixtures',packages.length>0&&existsSync(join(dirs.models,`strata-${modelId}.json`)),`engine package(s) ${packages.join(', ')} in ${dirs.components}; run config strata-${modelId}.json in ${dirs.models}`);
}else{
installFakeEngine();
makeTokenizerAndConfig();
record('fixtures',true,`stub engine package in ${dirs.components}; run config strata-stub.json`);
record('models come from Control',native.available&&model?.ready===true&&native.packaged===true,`available=${native.available}; model ready=${model?.ready}; ${native.summary??native.error}`);
record('load through the host starts the engine under Control',loaded.job.state==='done'&&instance?.status==='ready',`job=${loaded.job.state}${loaded.job.error??''}; instance=${instance?.status}`);
record('the engine process running is the binary from the component folder',!!ran,listed[0]?listed[0].trim().slice(0,160):'no engine process under the component folder');
}else{
record('the engine that ran is the one in the component folder',ran&&ran.script.toLowerCase().startsWith(dirs.components.toLowerCase()),ran?.script??'engine never ran');
record('a second client sees the same owner and instance (ownership)',runtime.json?.state==='ready'&&!!runtime.json.owner_id&&!!runtime.json.instance_id&&runtime.json.host_pid!==host.pid,
`state=${runtime.json?.state}; owner=${String(runtime.json?.owner_id).slice(0,8)}; instance=${String(runtime.json?.instance_id).slice(0,8)}; owner host pid ${runtime.json?.host_pid} is not the app host (${host.pid})`);
?{model:instance?.apiModelId??modelId,messages:[{role:'user',content:'Reply with exactly one word: pong'}],max_tokens:200,temperature:0,reasoning_effort:'none',stream:false}
constanswer=`${message.content??''}${message.reasoning_content??''}`;// the stub emits token 97 ("a"); the server files it as reasoning
record(real?'the real model answers through the host (the answer text is checked)':'the engine answers through the host (the stub token, not an error)',
record('no engine for this PC is a notice and nothing is started',missing.job.state==='error'&&/No engine for this PC/.test(missing.job.error)&&/never downloads/.test(missing.job.error)&&!existsSync(mark),
record('clean shutdown stops the engine the app started and exits',stopping.status===202&&code===0&&final.json?.state==='stopped'&&(!ran2||!alive(ran2.pid)),`status=${stopping.status}; host exit=${code}; runtime=${final.json?.state}; engine alive=${ran2?alive(ran2.pid):'n/a'}`);
// ---- hard kill: an engine whose app host dies ends with it (the owner follows the host: runtime start --die-with-pid)
record('nothing is left to stop afterwards',/stopped|not managed/.test(JSON.stringify(cleanup.json))&&controlJson('runtime','status','--profile','app-1').json?.state==='stopped',JSON.stringify(cleanup.json).slice(0,160));
// ---- the shell itself is killed (what the desktop shell's crash or kill looks like to the host): the host leaves, and
// its engine with it, with nothing announced. Windows also has the job object; this is the path that works everywhere.
conststarted2=awaitstartHostViaShell();
if(started2.failed){record('host starts under a stand-in shell',false,'');try{started2.shell.kill('SIGKILL');}catch{/* gone */}}
try{consthid=join(dirs.components,'engine.hidden-by-flow');if(real&&existsSync(hid)&&!existsSync(join(dirs.components,'engine')))renameSync(hid,join(dirs.components,'engine'));}catch{/* reported by the run */}
<div class="onboard-welcome-copy"><h1>State of the art. At home.</h1><p>Strata runs AI on your own computer.<br>Setting it up takes a few minutes.</p>
<div class="onboard-benefits"><span>${check}Private</span><span>${check}Fast</span><span>${check}Easy to use</span></div></div>
<footer class="onboard-footer onboard-footer--welcome"><button class="st-btn st-btn--primary">Install Strata</button><small>Installs for you only. No administrator needed.</small></footer>`,
<div class="mock-body"><h2 class="mock-title">Installing Strata…</h2><p class="mock-sub">Copying the app and its engine room. You can keep working meanwhile.</p>
<footer class="onboard-footer"><span></span><span class="mock-hint">Next: choose your first model inside Strata.</span><button class="st-btn st-btn--primary" disabled>Installing…</button></footer>`,
<div class="onboard-welcome-copy"><h1>Strata is ready.</h1><p>Open it to pick your first model. Strata downloads it for you and gets it ready.</p></div>
<footer class="onboard-footer onboard-footer--welcome"><button class="st-btn st-btn--primary">Open Strata</button><small>A shortcut was added to your Start menu and desktop.</small></footer>`,
* (the app folder is <root>/app: node/, host/, control/ ...; <root>/components gets the engine, from <root>/app/engine-bundle when the app carries one)
for(leti=0;i<180&&status?.engineSetup;i++){awaitdelay(1000);status=(awaitcall('/api/control/status')).body;}// the engine that came with the app is copied on the first start
console.log(' good preview:',JSON.stringify(goodPreview.body).slice(0,500));
record('a folder of a supported model is recognised',goodPreview.status===200&&goodPreview.body?.preview?.file_set_status==='plausible'&&!!selection,`family=${selection?.family}; model=${selection?.model}; files=${goodPreview.body?.preview?.file_count}`);
if(!selection||args.only==='preview')return;
// ---- prepare (what the page's button does): install from the folder, nothing copied or downloaded
record('the catalog lists the recognised version',!!catalogSize,catalogSize?`id=${catalogSize.id}`:`no ${selection.family}/${selection.model} in the catalog`);
record('the model is ready and points at the folder, not a copy',entry?.ready===true,`model=${modelId}; ready=${entry?.ready}; dataFolder=${entry?.dataFolder}`);
record('load starts the engine from the component folder',loaded.job.state==='done'&&instance?.status==='ready',`job=${loaded.job.state}${loaded.job.error??''}; instance=${instance?.status}`);
if(instance?.status!=='ready')return;
record('one engine process is running',engineProcs().length===1,engineProcs().map(l=>l.trim().slice(0,140)).join(' | '));
// ---- a chat answer, checked
constchat=awaitfetch(base+'/api/chat/stream',{method:'POST',headers:{'content-type':'application/json'},body:JSON.stringify({sessionId:'e2e-chat',message:'Reply with exactly one word: pong',history:[],nativeModelId:instance.id,thinking:'Off',agent:false,workspaceId:null})});
try{constf=JSON.parse(line);if(f.type==='delta')text+=f.text??'';if(f.type==='done')done=f;if(f.type==='error')text+=` [error: ${f.message}]`;}catch{/* partial line */}
}
}
record('the model answers through the app (the answer text is checked)',chat.status===200&&/pong/i.test(text),`status=${chat.status}; answer=${JSON.stringify(text.slice(0,120))}; tokensOut=${done?.tokensOut}; tps=${done?.tps}`);
# every checksum in the README must be the checksum of a file that is here, and a line that names a file and carries a checksum must carry that file's checksum
hashes="";bad=0;seen=0
for d in "${DIRS[@]}";dofor f in "$d"/*;do[ -f "$f"]&& grep -qF "$(basename "$f")""$VERIFY"&&hashes="$hashes$(sha_of "$f")";done;done# only files the README names are hashed (a folder may hold big unrelated files)
f="$(find_file "$name")"||{echo"make-tester-readme: no file named $name in: ${DIRS[*]} (a README must not carry a hash it did not compute)" >&2;exit 1;}
* Also: --silent <url> (a server whose model list says nothing about pictures: ON by default), --agent (the same picture through the agent; needs a Harness)
record('the engine component carries the picture reader and the host knows it (visionReader)',reader&&native.visionReader===true,`component=${reader} host=${native.visionReader}`);
console.log(` answer from the API model: ${JSON.stringify((sent.text??sent.message??'').slice(0,300))}`);
record('the picture goes to the API model and is answered',sent.status===200&&!sent.error&&(sent.text??'').trim().length>0&&names((sent.text??'').toLowerCase()),`status=${sent.status} err=${sent.error??''}`);
if(args['text-only']){
awaitsaveApi('Text only server',args['text-only'],[{id:'plain',name:'Plain model'}]);
constt=awaitreadsAfter('Text only server','plain',false);
record('a server without a picture reader is known not to read pictures',t.m?.reads===false,JSON.stringify(t.m));
record('a picture for it is refused in plain words that say how to fix it',refused.status===409&&/Read pictures with this model/.test(refused.message??'')&&/Settings/.test(refused.message??''),`${refused.status}`);
constticked=awaitsaveApi('Text only server',args['text-only'],[{id:'plain',name:'Plain model',vision:true}]);
constafter=(awaitcall('/api/model-providers')).body.connections.find(x=>x.name==='Text only server');
record('ticking "Read pictures with this model" is saved and makes the model read pictures',after?.models[0]?.vision===true&&after?.models[0]?.reads===true,JSON.stringify(after?.models[0]));
record('with the tick the picture is sent on (the server itself decides what to do with it)',through.status===200&&(through.error??'')!==''?/turn off/.test(through.error):through.status===200,`${through.status}${through.message??through.error??''}`);
record('a model switched OFF refuses pictures and says where to turn them on again',notRead.status===409&&/not to read pictures/.test(notRead.message??'')&&/Settings/.test(notRead.message??''),`${notRead.status}${notRead.message??''}`);
console.log(` other style: ${JSON.stringify(style.message)}`);
record('an API of another style says plainly why pictures stay in the preview',style.status===409&&/OpenAI/.test(style.message??''),`${style.status}`);
record('the model is prepared with its picture reader',!!model?.visionPrepared,JSON.stringify({ready:model?.ready,visionPrepared:model?.visionPrepared}));
record('the model loads with the picture reader',loaded.job.state==='done'&&instance?.status==='ready'&&instance?.vision===true,`${loaded.job.state}${loaded.job.error??''} vision=${instance?.vision}`);
record('the model loaded with pictures ON without being asked per load (default)',instance?.vision===true,'');
console.log(` answer from the local model (${Math.round((Date.now()-t0)/1000)} s): ${JSON.stringify((local.text??local.message??'').slice(0,300))}`);
constlow=(local.text??'').toLowerCase();
record('the local model answers about the picture (names the shapes)',local.status===200&&/red|circle/.test(low)&&/blue|square/.test(low)&&/green|triangle/.test(low),`status=${local.status} err=${local.error??''}`);
record('the local model reads STRATA 42',low.includes('42'));
// the same picture through the agent (the Harness route must declare image input: the "does not declare image input" error of the tester)
if(args.agent){// needs a Harness beside the host (the installed layout has one; a bare test box does not)
record('a picture for the OFF model is refused with a next step',refusedLocal.status===409&&/Read pictures/.test(refusedLocal.message??'')&&/Models/.test(refusedLocal.message??''),`${refusedLocal.status}`);
# The launcher records the environment the AppImage was started from, before its own hooks add the AppImage's libraries and GTK/GIO
# settings (LD_LIBRARY_PATH, GTK_*, XDG_DATA_DIRS ...). The host gives it back to every program that is not Strata's own (zenity,
# kdialog, xdg-open, the assistant's shell): apps/host/server/external-env.mjs. Without it a system program loads the AppImage's libssl
# and dies ("OPENSSL_3.2.0 not found"). The line goes right after `this_dir=...`, ahead of every hook.
if ! grep -q STRATA_ORIGINAL_ENV "$WORK/AppDir/AppRun";then
sed -i '/^this_dir=/a # Strata: the environment this AppImage was started from (given back to programs that are not Strata'"'"'s own)\nexport STRATA_ORIGINAL_ENV="$(env -0 | base64 -w0)"'"$WORK/AppDir/AppRun"
grep -q STRATA_ORIGINAL_ENV "$WORK/AppDir/AppRun"||{echo"could not add the environment record to AppRun" >&2;exit 1;}
check(order.length===2&&order[0].startsWith('SPAN')&&order[1].startsWith('BUTTON')&&Number(order[0].split(':')[1])<Number(order[1].split(':')[1]),'the time left sits left of Stop',order.join(' '));
check(samples.length>=3&&samples.every((v,i)=>i===0||v>=samples[i-1]),'one combined count that only goes up',samples.map(v=>v.toFixed(1)).join(', '));
check(overflow<=2,'the Models tab fits the first view without scrolling',String(overflow)+' px over');
check(awaitpage.locator('.family-switch').count()===1,'the model family switch is in the navbar');
check(awaitpage.locator('h1').filter({hasText:/^Model$/}).count()===0,'and the navbar does not say "Model"');
check(/IQ3_S|High quality/.test(summary??''),'Models selects the model that is on this PC, not the recommendation',(summary??'').replace(/\s+/g,' ').trim());
check(!/IQ3_XXS|Balanced/.test(summary??''),'the recommendation (IQ3_XXS) is not what is selected');
check(inst?.resourceProfile==='work','the running model reports the profile it was started with',String(inst?.resourceProfile));
awaitdelay(1500);
check(awaitwork().getAttribute('aria-pressed')==='true'&&awaitfull().getAttribute('aria-pressed')==='false','after the load the page still shows Leave 20%, not Full performance');
check(!/Download and prepare|Download . load|approximately|[0-9] GB/i.test(text),'for a downloaded model the pop-up has no download wording or size',text.replace(/\s+/g,' ').slice(0,600));
check(/Apply/.test(text),'it says what happens: apply settings and load');
}elseconsole.log('info the settings were the saved ones, so loading needed no pop-up at all');
// ------------------------------------------------------------------------------------------------ version list, This computer, Tune, a task
for(constschemeof['light','dark'])awaitscenario(scheme==='light'?'ui2':'ui2d',{installed:true,env:{FAKE_TOKEN_DELAY:'0.02',FAKE_REPLY:'I am thinking about it. </think> '+'Here is the answer, word by word. '.repeat(12),STRATA_TUNE_NO_QUIET_CHECK:'1'}},async({base,api,browser,shot})=>{
check(/This computer/.test(pcText)&&/(Fits fully in memory|read from the SSD)/.test(pcText),`${scheme}: This computer says in one line whether it fits`,pcText.slice(0,200));
check(awaitpc.locator('.pc-meter').count()>=1,`${scheme}: it shows the memory bars`);
check(awaitpc.locator('.chip, .st-pill, [class*="badge"]').count()===0,`${scheme}: no chips or badges in it`);
for(constjobofjson?.jobs??[])if(job.kind==='autotune'&&fakePhase==='done'){job.state='done';job.progress=null;job.result={outcome:'improved',gain_pct:18,base:{decode_tok_s:41.2},chosen:{decode_tok_s:48.6},preset:{id:'fast',name:'Fast · 64K'},answers:{class:'exact',fields:[]}};}elseif(job.kind==='autotune'&&job.state==='running')job.progress={stage:'rounds',text:null,minutesLeft:9,runsDone:3,runsPlanned:6,elapsedS:300,step:'Trying: more experts kept in memory (round 1 of 2)',speeds:[{label:'base',tok_s:41.2},{label:'Fewer helper threads',tok_s:40.1},{label:'More experts kept in memory',tok_s:46.8}]};
check(sawRun,`${scheme}: a step is shown as running`);
check(sawSpeed,`${scheme}: the live speed shows a number while it runs`);
if(sawSpeed){fakePhase='done';awaitpage.locator('.tune-outcome').waitFor({timeout:15000}).catch(()=>{});check(/18% faster/.test((awaitpage.locator('.tune-outcome__body h3').textContent().catch(()=>''))??''),`${scheme}: the result card shows the gain as its headline`);check(/Fast/.test((awaitpage.locator('.tune-outcome__facts').textContent().catch(()=>''))??''),`${scheme}: and what it was saved as`);}
// a task: the opened task is skimmable, with pause / delete / Edit / Run now
constmade=awaitapi('/api/tasks',{method:'POST',headers:{'content-type':'application/json'},body:JSON.stringify({title:'Open Downloads folder',prompt:'Open my Downloads folder and tell me what is new.',frequency:'weekdays',at:'08:00',weekday:1,timeZone:'Europe/Ljubljana',mode:'manual',enabled:true})});
constid=made.body?.id??made.body?.task?.id;
check(!!id,`${scheme}: a task is created`,JSON.stringify(made.body).slice(0,120));
check(awaitpage.getByRole('button',{name:/^Pause /}).count()===1&&awaitpage.getByRole('button',{name:/^Delete /}).count()>=1&&awaitpage.getByRole('button',{name:/Run now/}).count()>=1,`${scheme}: pause, delete, Edit and Run now are in the header`);
check(awaitpage.locator('.main__back').count()===0&&awaitpage.locator('.main__title-line .task-state').count()===0,`${scheme}: nothing sits on the left of an opened task's navbar`);
check(/Weekdays/.test(factsText)&&/Schedule/i.test(factsText)&&/Last result/i.test(factsText),`${scheme}: schedule, what it may do and the last result are at the top`,factsText.slice(0,160));
awaitscenario('tune',{installed:true,env:{FAKE_TOKEN_DELAY:'0.02',FAKE_REPLY:['I am thinking about it.','</think>','','Here is a list:','','- one','- two','','## Heading','','Some `code` and **bold** text.','','| a | b |','|---|---|','| 1 | 2 |','','```','code line','```',''].join(String.fromCharCode(10))}},async({base,api,browser,dirs,shot})=>{
constpage=awaitnewPage(browser,base);
awaitclickText(page,/get started/i);
awaitpage.getByText(/Choose a version/i).waitFor({timeout:30000});
check(tps>15&&tps<120,'the speed shown is the decode speed (the engine paced at ~50 tokens a second)',speedText.replace(/\s+/g,' ').slice(0,160));
check(/32\.8K|32K|32,768|32768/.test(contextText)&&!/128K/.test(contextText),'the context shown is the one the model runs with (32K), not 128K',contextText.replace(/\s+/g,' ').slice(0,160));
check(sizes[0][1]===12&&sizes[1][1]===14&&sizes[2][1]===16&&sizes[3][1]===22&&sizes[0][2]===12&&sizes[3][2]===22,'the reading size changes the text size of the conversation',JSON.stringify(sizes));
check(/Delete this version\?/.test(dialog)&&/Remove 83\.6 GB of downloaded files\. You can download this version again\./.test(dialog),'the confirm dialog uses the existing words and the real size',dialog.replace(/\s+/g,' ').slice(0,600));
check(/running model will be unloaded/i.test(dialog),'and says the loaded model is unloaded first');
console.log('info chat opened in',open,'ms; long tasks',JSON.stringify(long),'requests',JSON.stringify(requests.filter(r=>r[1]>100)),'all',requests.length);
check(open<1500&&Math.max(0,...long)<300,'Models to Chat opens at once, with no long block',open+' ms, longest task '+Math.max(0,...long)+' ms');
check(awaitpage.getByRole('button',{name:/^Tune model$/}).count()===1,'the Models tab has the Tune model button (the real tuner, not the old short benchmark)');
awaitpage.getByRole('button',{name:/Other local models/i}).click().catch(async()=>{awaitclickText(page,/Manage models/i);awaitclickText(page,/Use a local GGUF/i);});
awaitpage.getByText(/Folder with the GGUF files/i).first().waitFor({timeout:15000}).catch(()=>{});
awaitshot(page,'2-local-gguf');
check(awaitpage.getByText(/Folder with the GGUF files/i).count()>0,'the local GGUF page opens in the same design as Models');
awaitpage.getByPlaceholder(/Full path to the folder/i).fill(join(dirs.install,'does-not-exist'));
check(/°C/.test(mon),'the Monitor shows GPU data with no model loaded',(mon.match(/.{0,30}°C.{0,20}/)?.[0]??'no temperature text').replace(/\s+/g,' '));
for(leti=0;i<40;i++){messages.push({id:'u'+i,role:'user',content:'question '+i,at:now},{id:'a'+i,role:'assistant',content:'answer number '+i+' with some text to fill the line and more of it.',at:now,tokens:20,inputTokens:5});}
check(/server_exited/.test(details)&&/exited before/.test(details),'Details holds the code and the reason the server logged',details.replace(/\s+/g,' ').slice(0,120));
constbadOpen=awaitnoScroll(page);
check(badOpen.length===0,'and the pop-up still does not scroll with Details open',badOpen.join('; '));
check(saved.prefs.contentFontSize===19&&saved.prefs.language==='de','text size and language survive a restart on the same port',JSON.stringify(saved.prefs));
record('the model is installed with the picture reader prepared (visionPrepared)',!!model?.visionPrepared,JSON.stringify({ready:model?.ready,visionPrepared:model?.visionPrepared,images:model?.images}));
record('load starts the engine with the picture reader',loaded.job.state==='done'&&instance?.status==='ready',`${loaded.job.state}${loaded.job.error??''}; instance=${instance?.status}; vision=${instance?.vision}`);
record('the host says the loaded model reads pictures',!!instance?.vision,`vision=${instance?.vision}`);
// ---- a real picture through the host's own /v1 endpoint
description="Strata Desktop — local AI, tuned to one model."
edition="2021"
rust-version="1.77"
license="MIT"
# Linux: the drop of a file on the window is read straight from the GTK drag signals (see hook_linux_drops in main.rs); the versions are the ones Tauri itself uses.
{"core":{"default_permission":{"identifier":"default","description":"Default core plugins set.","permissions":["core:path:default","core:event:default","core:window:default","core:webview:default","core:app:default","core:image:default","core:resources:default","core:menu:default","core:tray:default"]},"permissions":{},"permission_sets":{},"global_scope_schema":null},"core:app":{"default_permission":{"identifier":"default","description":"Default permissions for the plugin.","permissions":["allow-version","allow-name","allow-tauri-version","allow-identifier","allow-bundle-type","allow-register-listener","allow-remove-listener","allow-supports-multiple-windows"]},"permissions":{"allow-app-hide":{"identifier":"allow-app-hide","description":"Enables the app_hide command without any pre-configured scope.","commands":{"allow":["app_hide"],"deny":[]}},"allow-app-show":{"identifier":"allow-app-show","description":"Enables the app_show command without any pre-configured scope.","commands":{"allow":["app_show"],"deny":[]}},"allow-bundle-type":{"identifier":"allow-bundle-type","description":"Enables the bundle_type command without any pre-configured scope.","commands":{"allow":["bundle_type"],"deny":[]}},"allow-default-window-icon":{"identifier":"allow-default-window-icon","description":"Enables the default_window_icon command without any pre-configured scope.","commands":{"allow":["default_window_icon"],"deny":[]}},"allow-exit":{"identifier":"allow-exit","description":"Enables the exit command without any pre-configured scope.","commands":{"allow":["exit"],"deny":[]}},"allow-fetch-data-store-identifiers":{"identifier":"allow-fetch-data-store-identifiers","description":"Enables the fetch_data_store_identifiers command without any pre-configured scope.","commands":{"allow":["fetch_data_store_identifiers"],"deny":[]}},"allow-identifier":{"identifier":"allow-identifier","description":"Enables the identifier command without any pre-configured scope.","commands":{"allow":["identifier"],"deny":[]}},"allow-name":{"identifier":"allow-name","description":"Enables the name command without any pre-configured scope.","commands":{"allow":["name"],"deny":[]}},"allow-register-listener":{"identifier":"allow-register-listener","description":"Enables the register_listener command without any pre-configured scope.","commands":{"allow":["register_listener"],"deny":[]}},"allow-remove-data-store":{"identifier":"allow-remove-data-store","description":"Enables the remove_data_store command without any pre-configured scope.","commands":{"allow":["remove_data_store"],"deny":[]}},"allow-remove-listener":{"identifier":"allow-remove-listener","description":"Enables the remove_listener command without any pre-configured scope.","commands":{"allow":["remove_listener"],"deny":[]}},"allow-set-app-theme":{"identifier":"allow-set-app-theme","description":"Enables the set_app_theme command without any pre-configured scope.","commands":{"allow":["set_app_theme"],"deny":[]}},"allow-set-dock-visibility":{"identifier":"allow-set-dock-visibility","description":"Enables the set_dock_visibility command without any pre-configured scope.","commands":{"allow":["set_dock_visibility"],"deny":[]}},"allow-supports-multiple-windows":{"identifier":"allow-supports-multiple-windows","description":"Enables the supports_multiple_windows command without any pre-configured scope.","commands":{"allow":["supports_multiple_windows"],"deny":[]}},"allow-tauri-version":{"identifier":"allow-tauri-version","description":"Enables the tauri_version command without any pre-configured scope.","commands":{"allow":["tauri_version"],"deny":[]}},"allow-version":{"identifier":"allow-version","description":"Enables the version command without any pre-configured scope.","commands":{"allow":["version"],"deny":[]}},"deny-app-hide":{"identifier":"deny-app-hide","description":"Denies the app_hide command without any pre-configured scope.","commands":{"allow":[],"deny":["app_hide"]}},"deny-app-show":{"identifier":"deny-app-show","description":"Denies the app_show command without any pre-configured scope.","commands":{"allow":[],"deny":["app_show"]}},"deny-bundle-type":{"identifier":"deny-bundle-type","description":"Denies the bundle_type command without any pre-configured scope.","commands":{"allow":[],"deny":["bundle_type"]}},"deny-default-window-icon":{"identifier":"deny-default-window-icon","description":"Denies the default_window_icon command without any pre-configured scope.","commands":{"allow":[],"deny":["default_window_icon"]}},"deny-exit":{"identifier":"deny-exit","description":"Denies the exit command without any pre-configured scope.","commands":{"allow":[],"deny":["exit"]}},"deny-fetch-data-store-identifiers":{"identifier":"deny-fetch-data-store-identifiers","description":"Denies the fetch_data_store_identifiers command without any pre-configured scope.","commands":{"allow":[],"deny":["fetch_data_store_identifiers"]}},"deny-identifier":{"identifier":"deny-identifier","description":"DenLine truncated
Loaded 100 of 2258 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.