Same 15 files as the 0.2.6 bump (issue template, AGENTS.md, CMakeLists.txt 0.1.52, desktop and host package files, Cargo.toml and the strata-desktop entry in Cargo.lock, SettingsUpdates comment, qa1-storage test, metal 0.1.52-metal, build_hip_linux.sh PKGVER 0.1.52, strata_cli PRODUCT_VERSION, the two version tests), plus the Strata-0.2.7-<file> names in the md files.
The engine's own tower (319 MiB) through strata launch and setup (--vision off still turns it off); setup fetches
the pinned 972 MB mmproj; the server sends pictures to the tower. Checked on 4x R9700, 1x R9700 and a real
launch on 2x A6000 (download, pack, picture answer over HTTP); 0/10 stalls, KL unchanged. Also: a newer shard 1
from --model-path (ISTA's 2026-10-26 chat template) is accepted after its own size and SHA-256 check.
Pipelined groups beat the serial default at 2 and 4 streams on 2x A6000 (+17/+22%) and 2x A4000 (+7..12/+17..20%);
one stream equal, -4% on A4000 IQ3_XXS. STRATA_BATCH_GROUPS_NV_SERIAL=1 or STRATA_DEFAULTS=026 gives serial.
ISTA re-published (and patch_chat_template.py rewrites) the first shard of each set with a larger chat template, so a shard 1 on disk can be 11 KB
larger than the pinned revision's. setup refused it (size mismatch; the GGUF preflight does not read DeepSeek tensors, so it was not taken as a
whole own file). The pinned revision stays the download; the newer shard 1 is accepted by its own size and SHA-256 with --gguf-dir.
native_arithmetic_parity failed on an Intel i9-13900K (AVX2 + AVX-VNNI, ggml built native)
in five of its eight configurations: "gu=16 down=20 ff=96 nt=2 token=0". Not the VNNI row
path: STRATA_NO_AVXVNNI=1 changes nothing, STRATA_NO_IQ4NL=1 passes. The IQ4_NL down
reference (iq4nl_reference_rows) differs from that ggml's one-token dot on the scalar tail
block of a row with an odd number of blocks (ff=96 is three): that ggml does not contract
`sumf += d * sumi` into an FMA, the reference lets the compiler do it. On the other
machines tested the two agree, so it is a property of the ggml build, not of the CPU.
With an unfused tail in the reference all eight configurations pass on that machine.
The invariant path now checks each reference once against the linked ggml-cpu on synthetic
blocks before using it (iq4nl_reference_ok for IQ4_NL with 2, 3, 5 and 20 blocks;
iq512_reference_ok for types 18, 21 and 22 with SwiGLU, on AVX-512 CPUs). On a mismatch
it prints one line and the invariant path uses ggml's own one-token dot per token: slower
on such a build, never different. The Default path is not touched; only the opt-in
STRATA_SPEC_PROB=1 decode reaches this code.
Tests: native_arithmetic_parity, eight configurations, on the i9-13900K (4,866,048
comparisons, with the fallback line), z440 and the owner PC (see the series notes).
0.2.1 made a layer split over exactly two NVIDIA cards with --batch and no --batch-groups run one serial group (measured
then on 2x RTX A4000, IQ3_XXS: +12..16%). With the 0.2.7 code the pipelined groups are faster at 2 and 4 streams on
every box and quant measured, so the default goes back to the pipeline.
Measured (tools/serve_load.py, --batch 4, 256 tokens per request, interleaved pairs, total tok/s, pipelined / serial):
- 2x RTX A6000, IQ3_S, all experts in VRAM: 1 stream 101.7 / 101.7; 2 streams 125.7 / 107.4 (+17%); 4 streams 171.4 / 141.0
(+22%). The v0.2.6 binary on the same box: serial default 104.9 and 137.2, --batch-groups auto 124.6 and 169.6.
- 2x RTX A4000, Q2_0 pack (17037 of 24576 pairs cached, 4 clean pairs): 2 streams 80.2-82.5 / 75.0-75.5 (+7..9%, 4 of 4);
4 streams 110.8-111.5 / 94.8-95.3 (+17%, 4 of 4); 1 stream 63.6-68.7 / 68.2-70.3.
- 2x RTX A4000, IQ3_XXS (13056 of 24576 pairs cached, 3 pairs): 2 streams 73.2-73.7 / 65.8-66.5 (+12%, 3 of 3); 4 streams
100.5-100.7 / 82.9-83.6 (+20%, 3 of 3); 1 stream 58.4-60.5 / 60.7-62.6 (serial about 4% faster, 3 of 3).
The single stream runs on the solo path in both; only the A4000s show the 4%.
What changed: batch_groups.hpp has a pure rule nv_serial_wanted(env value, STRATA_DEFAULTS=026). STRATA_BATCH_GROUPS_NV_SERIAL
set to anything but 0 asks for the serial group, an explicit 0 wins over STRATA_DEFAULTS=026, and with the variable unset or
empty STRATA_DEFAULTS=026 gives v0.2.6's serial group. An explicit --batch-groups (a number or auto) still wins. Three or more
cards, AMD builds and --batch-mtp/--adapt-async handling are unchanged. The serial message names the opt-in.
Tests: batch_groups_test (the rule over the env values and 026, two cards pipeline, the existing serial cases). Real engine on
2x A6000 (IQ3_S), no --batch-groups: the engine says "2 groups of 2 slots" / "2 groups of 1 slot"; with 4 and 2 slots, greedy,
greedy+seeded sampled, slots finishing at different lengths and a cancel in the middle of a draft, every slot's tokens equal its
solo decode and the pipelined build's. A long-prompt slot differs from the pre-merge base build in the same way with the flag
and without it (it is the parent's prompt path, not this change).
Not covered: no run on AMD or on three cards; the 4% single-stream cost on the A4000s is measured on one box.
THIS COMMIT ALONE MAKES THE WALK A DEFAULT. Drop it and the layer-major prefill stays
an opt-in (STRATA_PREFILL_LAYER_MAJOR=1) with the chunk-major path as the default.
Rule (include/strata/core/sm120_series.hpp, pure, tested in src/core/arch_defaults_test.cpp):
- prefill_layer_major_default(hip, stages, v026): on for two or more stages on CUDA,
off on one GPU, in a HIP build, and under STRATA_DEFAULTS=026 (qwen_defaults_026());
- prefill_layer_major_resolve(env, ...): STRATA_PREFILL_LAYER_MAJOR=0 / 1 (any text
atoi reads) wins in both directions, over the rule and over STRATA_DEFAULTS=026.
Every decline of the earlier commits stays and runs in lm_prepare as before (HIP,
image prompts, DFlash2 taps, KVA, stop_at_chunk, a pending force_chunk, a peer GPU,
APR / NVFP4 / WMMA / fused layers, STRATA_EXPERT_NIBBLE, no VRAM, no pinned RAM):
such a prompt reads chunk-major with nothing but a log line. When the walk is on by
default (nothing set) the VRAM margin is 512 MiB instead of 96 and the super-chunk
target 32768 tokens (the size measured) instead of 8192; "a short prompt" is not logged.
STRATA_PREFILL_LM_TOKENS and STRATA_PREFILL_LM_MARGIN_MIB still win.
Measured (z440, 2x RTX A4000 16 GB, Qwen3.8-Flash-Next IQ3_XXS, experts streamed, 5
interleaved pairs, time to first token, medians): 8K 2.8x, 20K 4.8-5.7x, 32K 5.1-5.5x
faster (141 s to 26-28 s); decode 38.6 vs 38.4 tok/s; needles 12 of 12 found in both
arms. The speed-up is larger than the report's 2x because --prefill auto picks 512-token
chunks there. The first-token KL numbers are in the gate notes (3976-kl.sh).
Not covered: one GPU (stays opt-in), HIP, SYCL, Metal, three or more stages, a card
with more VRAM than the pack needs (where the experts are VRAM-resident and the
chunk-major path has little to re-read).
Problem. #3948: with STRATA_QFUSE=1 the verified text changes and MTP acceptance drops, on CUDA. The QFUSE kernels are bit-exact
(gr_parity, gdn_parity, 10,500 random windows each on sm_86 and sm_120, after 91a4b8a67), yet the engine's greedy text still differed
from QFUSE off on every prompt tried. Model run, l620 (RTX PRO 5000, sm_120), Qwen3.8-Flash-Next IQ3_S, MTP --spec 6, identity-gate
flags, 4 prompts of 4K and 12K tokens, 96 greedy tokens: QFUSE on differed from off on 4 of 4 (the off-vs-off control was identical).
Cause. A window of one token advances the GDN state and the indexer tail itself ("self-commit") so that commit() needs no commit
graph. Verifier::record_window decides this with
`T == 1 && !always_publish_ && !batch_rec_ && !g_qfuse() && one_token_self_commit()`: with STRATA_QFUSE the fused GDN output
kernel takes the q8_1 destination and the window does not self-commit. Verifier::commit() skipped the commit graph with
`last_t_ == 1 && !always_publish_ && one_token_self_commit()`, without the QFUSE term. So with QFUSE on, every accepted one-token
window left the recurrent state (and the indexer tail) one token behind. Windows of one token are the ordinary case when the drafter
offers nothing, which matches the report: fewer drafts offered, lower acceptance, different text, and an unchanged text where
no window of one token occurs. On HIP (where QFUSE is the default on gfx1151) one_token_self_commit() is off unless
STRATA_ONE_TOKEN_COMMIT=1, so the two sides agreed there.
Change. commit() takes the same condition: `... && one_token_self_commit() && !g_qfuse()`. Without STRATA_QFUSE nothing changes.
Result (same l620 run). QFUSE on with this change against QFUSE off, same build: logits md5 and the 96 greedy tokens byte-identical
on all 4 prompts (IDENTITY PASS); the build without the change differs on all 4. The default path (QFUSE unset) of the merged head
against the f-027-int2 head (2a2264c21): first-window logits and greedy tokens byte-identical on the same 4 prompts.
Not covered. Serve answers and draft counters (3948-qfuse.sh step 2, SERVE=1), sm_86, and the reporter's 40-prompt set.
The report asks for the first data mismatch between the GGUF row bytes and what the direct reader delivered, and for an early
check of the gathered values; neither exists. Its trace stops at "the embedding has 96 non-finite values" without saying whether
the reader gave a wrong row or the table holds one.
With STRATA_PLE_CHECK=1 (a tester switch, off by default; one static flag read per gather call when off) every direct-mode decode (read_row, collect,
gather_batch, also from the prefetched slots) checks the row's 160 values. For a value that is not finite or beyond 1e5 (an IQ4_NL
row cannot decode beyond 8.3e6; real PLE values are orders smaller) it re-reads that row from the file with a buffered read and prints
one line to stderr (at most 8 per table): the row, its file offset and page offset, the value, and either "the delivered bytes ARE the
file's bytes" or "THE DELIVERED BYTES DIFFER FROM THE FILE from byte N" with the first 8 bytes of each. That separates a reader
fault (the bug) from a damaged table. gates/3602-ple-direct.sh sets it in its direct arms.
Checked: ple_reader_test --selftest on z440 with the switch on prints the "ARE the file's bytes" line for the synthetic table's random
rows (which decode to huge values); with it off the output and the bytes are unchanged (ple_reader_selftest passes).
Not checked: the "DIFFER" branch has no natural trigger (that is the point); it was read, not run.
Problem. #3948: with STRATA_QFUSE=1 the verified text changes and MTP acceptance drops (RTX 3080, sm_86). The report suspected the
group counter or the nat_xq_ offsets of fused_gr.cu; the audit and the new two-group test (previous commit of this stream)
rule those out. What the new test found instead, on an RTX 5070 (sm_120, CUDA 13.0, MSVC), over 10,500 random windows of two groups: in
about 1 run of 400 one q8_1 code of the fused image differs by exactly one from native_quantize_q8_1's, in every mode (group-relative,
window-indexed, direct, graph replay). The scale/sum pair and the other codes agree.
Cause. native_mmvq.cu, which holds native_quantize_q8_1 (the quantiser every consumer of a q8_1 image was written for), is
compiled with --use_fast_math (CMakeLists.txt, the native_* list), so its `amax / 127` and `x / d` are the fast full-range division
(div.full.f32, up to 2 ulp off). fused_gr.cu (the hyper-connection read's tail, gr_up_q32_kernel) and verify_kernels.cu (the GDN
output norm, gdn_q8_1_store) are not in that list and divide with IEEE rounding. roundf(x / d) then lands on the other side of a .5
at the rare element whose quotient is next to a boundary. One changed activation code per ~10^6 values is about one per few
hundred tokens at 80 hyper-connection reads of 2560 values per token: enough to move a near-tie in the verify, and the
draft (which quantises with the separate kernel) disagrees with the verifier a little more often, which lowers acceptance. That
matches the report (acceptance 0.755 against 0.79, different text, and an unchanged text where no draft is made).
Change. include/strata/kernels/q8_1_finite.hpp: q81_div(a, b) is `a / b`, except in a translation unit that defines
STRATA_Q81_FASTMATH_DIV (CUDA device code only, never HIP), where it is the same PTX division the fast-math file gets (div.full.f32).
q8_1_quant and the scale use it. fused_gr.cu and verify_kernels.cu define the macro; nothing else does, so every other kernel,
the separate quantiser and every HIP build compile to what they did. STRATA_QFUSE is off by default on CUDA (it is on by default on
gfx1151, which is HIP and unchanged), so the default path is bit-identical.
Result (RTX 5070, sm_120): gr_parity's two-group window test: 10,500 runs with 1,500 random inputs for each T, all modes, zero
differences (before: dozens); the whole of gr_parity, gdn_parity and gdn_parity with STRATA_GDN_SPLIT=1 pass.
Tests. gr_parity: the two-group test prints the first differing bytes (block, field), and STRATA_GRP_REPS=N / STRATA_GRP_KEEP_GOING=1
repeat it longer and count every failure. gdn_parity: the q8_1 destination check uses its own stream (native_quantize_q8_1 refuses the
null stream, which crashed the test on Windows), unbuffered stdout and STRATA_GDN_PARITY_TRACE=1.
Not covered. sm_86 (the report's card) has not run the new test yet (z440 was reserved for other jobs); the cause is not
architecture specific. The model run (gates/3948-qfuse.sh) is still owed: QFUSE on against off must now be byte-identical, and the
draft counters equal.
Problem. #3844: --spec 5 measured +6.1% decode on an RTX 5060 Ti, but a saved preset that carries it was left out entirely
("outside the safe values [3, 4]"), and an imported one was refused. The tuner's BOUNDS are the values it tries; they are
not a finding that a value is harmful. The never list (--spec 6 and wider: 4% to 16% slower, --expert-cache 0, ...) is.
Change (Terminal / Control; touches strata_control/tune_knobs.py, strata_control/presets.py, strata_cli/tune_commands.py and
serve/server.py).
- tune_knobs.check_setting_parts(args, env, ctx) returns (never, bounds). check_setting is the two lists together, in the
order it had, so the tuner still skips --spec 5 and its tests are unchanged.
- presets.overlay applies a preset whose only problems are outside BOUNDS and returns info["warnings"], one short line each
("--spec 5: not tried by the tuner, kept because it is your setting"). A never-list setting still means "not applied".
presets.create and import_preset refuse only never-list settings.
- serve/server.py prints each warning as "[strata] warning: ..." when the preset is applied; strata_cli tune_commands
_effective adds "warnings" to its payload.
Tests. tests/tuning/test_presets.py: a preset with --spec 5 is saved, applied (the flag becomes 5, the input config is not
modified), warns once, and an in-range preset has no warnings; the never list still refuses --spec 6; import of --spec 5 works.
tests/tuning/test_autotune.py: --spec 5 stays in the tuner's refused list. python -m pytest tests/tuning: 274 passed;
tests/terminal -k "tun or preset or overlay": passed.
Not covered. The app's Details text does not show the warning yet (it reads the payload key above).
Problem. #2408 / PR #2409: restoring a large parked conversation on a layer split ended the server with a CUDA out of memory
on a later card. The PR checks the free VRAM of every card against --vram-reserve-mib before a restore. Its own review
points out that a loaded card is always below that reserve, so the check would skip nearly every restore on a split (and
on one GPU). It is not ported.
What a restore allocates. conversation_snapshot_restore writes into the session's existing buffers (pools(st), the GDN
state, the indexer tails); refresh_residency only launches kv_stream_reset or copies into the existing ring. The elastic K/V
growth (kvg_ensure) allocates, but only on one GPU. The one thing a restore can create on a card is the calling thread's
private copy stream for it (conversation_copy.hpp private_stream), which it takes from the pool of finished threads when
there is one (#3350). A failed restore is fatal by contract (the session may be half written). So the need is one stream
and one event, and only for a card whose stream does not exist yet (a snapshot loaded from disk, or a restore on a thread
that never copied on that card).
Change. conv_cache.hpp: kRestoreFirstUseMib (64) and restore_fits(free, stream_ready, n, need): a restore is refused only
for a card that has no copy stream yet and less than 64 MiB free. conversation_copy_stream_ready(device) says whether a
card has one. generate.cpp: before the restore, each card of the run is checked; a snapshot that would not fit goes back to
the cache and the prompt is read again, with a log line. A card that has its stream is never refused, however full.
No sample (a failed cudaMemGetInfo) means the restore goes ahead as it did.
Tests. conv_cache_test: the predicate (cards with a stream are admitted at any free size; the edge at the need; no
sample / no cards / no need). conversation_copy_streams_test: copy_stream_ready before and after the first copy, in the pool
after a thread ends, and the measured cost of a card's first copy stream and event (printed; the test requires under
16 MiB, so 64 MiB leaves a wide margin). conversation_snapshot_test: after the first restore, six more restores of two
images leave the free VRAM where it was.
Not covered. The pure predicate and the generate.cpp code build on z440 (CUDA 13.0); the GPU tests are listed in the stream
notes. No model run: gates/2409-restore-guard.sh (a held-VRAM start on a layer split) shows the skip line and an
unchanged answer on the second turn.
Problem. On a layer split the later cards are left with very little VRAM after loading (#3702: 68 MiB on the last card; #3638:
268 and 666 MiB, then every batched prompt read of more than ~400 tokens ends non-finite after hours; #3797: the weight arena
fails with 320 MiB free and the message does not say who holds the rest). The window-graph eviction (12b0bce95) and the copy
stream pool (#3350) are in already; this is the rest of the code audit of what is allocated after load.
Audit. The table of every lazy allocation after load (what, when, size, how a failure is handled) is in
phaseA-tests/notes-027-gh-eng-d.md. Bugs found and fixed here, each small:
- verify.cpp capture / capture_batch and mtp.cpp finish_capture: the cudaGraphUpload and its sync were only printed. The
upload is a warm-up, but a failure (no VRAM) stayed as the thread's pending error and the next kernel wrapper's
cudaGetLastError ended the engine. The error is now cleared and the first launch uploads the graph.
- verify.cpp capture_commit and mtp.cpp finish_capture: the commit graph called cudaGraphInstantiate directly (no
eviction of older layouts, unlike the window graphs) and reported the status of the end-of-capture call, which is success,
so the message read "no error". capture_commit now goes through instantiate_evicting and reports the instantiate's status.
- instantiate_evicting instantiates into a local and sets the member only on success (what a failed instantiate leaves in
its output is not specified, and a non-null member means "captured").
- prefill.cpp carve: after a failed take of the shared region the sub-allocators had no base and each cudaMalloc'd its
buffer (hundreds of MiB in all) on the card that had just run out. carve now returns at once; the caller reports the
failure as before.
- generate.cpp: a failed set_stage_helper / set_peer is reported and the run goes on, but its CUDA error stayed pending;
cleared.
Found and not changed: relend() re-plans only the first participant (#3638 comment). Read: it runs only from the VRAM
command, which is off on a layer split, so it cannot touch a later stage's loan. Other candidates (ignored cudaEventCreate for
the pipelined fences, unchecked expert copies, cudaFuncSetAttribute flags set before success) are in the notes.
Changes for the report of #3797 and the reserve (#3702).
- include/strata/program/gpu_report.hpp: on a failed weight-arena allocation (first card and each stage) the engine now
also prints, for every card of the run, the free and total memory and the largest other programs that hold memory on it
(NVML, loaded at run time; where the driver does not list them it says so). Text only; the failure is the same.
tests/program/gpu_report_test.cpp pins the text.
- include/strata/program/stage_reserve.hpp: the arithmetic of stage_room (what the split keeps free on a card) moved out of
generate.cpp unchanged; tests/program/stage_reserve_test.cpp compares it with the old expression over a grid. The file
also holds a PROPOSAL for a larger window term on a later card (96 MiB + 6 MiB per further window size + 48 MiB, 174 MiB
for --spec 4) which nothing calls: the default placement is not changed. gates/3702-later-reserve.sh measures it.
- The low-VRAM warning of a later card now adds how many MiB the plan meant to keep free there and how far off that was.
Tests: gpu_report_test, stage_reserve_test (host only, built on z440).
Not covered: no model was run. What the ~700 MiB the plan does not count on the last card is (window graph sets, kernel code
loads) needs the run in gates/3702-later-reserve.sh.
Problem. #3948 reports that STRATA_QFUSE=1 changes the verified text and lowers MTP acceptance on an RTX 3080 (sm_86); the
control (QFUSE off against off) was noisy too. The reporter read the code and suspects the per-group counter in
fused_gr.cu gr_q8_tail (not reset when a graph replays with another T) or the nat_xq_ offsets of a window of two groups.
Audit (no model run). The counter is reset by the block that finishes the group (atomicExch before it reads the 32 values),
so a launch leaves it at zero whatever T is; two blocks own a 32-column group in every kernel that calls gr_q8_tail, the
32-column kernel (gr_up_q32_kernel) quantises on its own without a counter; the fence pattern is the usual
threadFenceReduction one. The two destinations are consistent with their consumers in verify.cpp: the attention half's
xq_ is indexed from the group's first token (the projections read it from row 0), the expert half's nat_xq_ by the window's
token (the experts read it from tb). The GDN side (verify_kernels.cu gdn_q8_1_store, rows from t_out_begin) matches its
consumer too. I found no defect by reading. One thing the tests below will show if it matters: nvcc ignores the
`#pragma clang fp contract(off)` in gdn_q8_1_store, so on CUDA the ds.sum field could differ by one ulp from the separate
quantiser (the q8 bytes would not).
Tests (synthetic data, no model).
- gr_parity: the engine's G = 2 call pattern - the window split at (T + 1) / 2, two fused_gr_read_multi calls in a row on
one stream with the same counters, group-relative destinations (xq_) and window-indexed ones (nat_xq_), direct and as a
graph replay; fresh random inputs for every one of 200 repetitions for each T = 2..8; after every run both groups' q8_1
bytes equal native_quantize_q8_1 of `mixed`, nothing outside the written ranges changed, and all counters are back at zero.
- gdn_parity: gdn_step_norm_multi with a q8_1 destination for every T and t_out_begin (verify and commit calls): the bytes
equal native_quantize_q8_1 of the output rows, the outputs and the state are the ones of the call without it, and
nothing is written past the rows. A second ctest, gdn_parity_split, runs all of gdn_parity with STRATA_GDN_SPLIT=1 (the
4-blocks-a-head step, which has its own output-norm kernel).
Not covered. The reporter's model run (QFUSE on/off, MTP, 40 prompts): gates/3948-qfuse.sh. The tests were built on z440
(CUDA 13.0, sm_86); their GPU run is listed in the stream notes.
Problem. #3602 reports a deterministic all-logits NaN with --ple-io direct on IQ3_S (Windows, two GGUF shards) where
--ple-io mmap passes; a diagnostic build found non-finite values already in the gathered PLE embedding of one token. The
bytes the model got were not the file's (the largest finite value, 7.9e6, is what a random fp16 scale gives an IQ4_NL
row). This commit audits the reader and builds the test the report asks for; it does not claim to have found that
fault.
Audit (no model run). src/ngram/ple_reader.cpp, src/platform/direct_file.cpp and the consumers (PleTable gather_batch /
prefetch_rows / collect, prefill's double-buffered ple_gather, verify's windows): rows are copied and cached under the
reader's mutex after a complete read, short reads are checked per row, straddling rows get a two-page read, the offset
arithmetic is 64-bit, the set-associative cache and the slot buffers are only touched under the lock, and prefill's two
host buffers are ordered by the ple_copied events. I found no way to hand out a row before its read completed or from a
stale buffer.
One real bug, in the error path. After a failed read the I/O worker parks for good (worker_loop), but issue() went on
queueing jobs, so the next collect() waited for them for ever. issue() now returns a ticket with no jobs once the reader
has failed, and collect() returns the error at once (and drops the ticket). Same change in the SYCL copy
(sycl/src/ngram/ple_reader.cpp).
Tests.
- src/ngram/ple_reader_stress_test.cpp (ctest ple_reader_stress): 7 table shapes (header 0/192/4000/4051, rows of 1, 90,
110, 160 and 4096 bytes, with and without bytes after the table) x 15 reader configurations (io thread or caller,
in-flight 1/2/3/64, row cache of 0, 16 and 100000 rows, injected completion delays, keep-alive), four client threads
with one to three tickets outstanding collected in any order; every delivered row is compared byte for byte with a
buffered read of the file; collected buffers are compared again later (a late write would show); rows on every page
phase, the last rows, out-of-range rows; the reader after a failed read (the file is cut short under it); with --big a
5 GiB sparse table with rows around 2^31 and 2^32 and at its end.
- table_is_consistent_under_concurrent_use in src/ngram/ple_reader_test.cpp: one PleTable used by a prompt thread
(big gather_batch), a decode window (prefetch_rows, gather_batch) and single gathers at once, against the mapped reader.
Results (z440, Linux, g++ 13, reader + direct_file + test built alone): plain build with --big OK; ASan+UBSan OK. The
error-path test against the unchanged reader hangs (the test exits 3 after 20 s); with the change it passes. ThreadSanitizer
flagged a data race that was a mistake in the test (the delay changed from a second thread in the caller-thread mode,
which has no lock); fixed in the test.
Not covered. The NaN itself: it needs a run on the reporter's configuration (gate script gates/3602-ple-direct.sh). The
Windows run of the new test is listed in the stream notes.
Gyro-L (agentionai's 50 GiB file) stores its experts as a pair trellis: 128
values per block, a fp16 scale and 64 steps of K bits (K = 5, 6, 7), two
weights per step taken from the first pair of the TQ2_T codebook entry the
step's state selects. The engine refused the file because it did not know the
three type ids. This adds them next to the existing trellis types 144-146
without changing those.
What changed:
- gguf_reader.hpp / tools/gguf_reader.py: names and block sizes (42 / 50 / 58 bytes).
- tq_cpu.*, tq_states.hpp, tq_avx2.cpp: tq_supported / tq_kbits / tq_block_bytes
for the new ids (the bytes per block are 2 + 8K for the pair layout, 2 + 4K
for the others), a scalar pair kernel (the reference) and an AVX2 pair kernel
that is bit for bit equal to it; AVX-512 uses the AVX2 pair kernel.
- iq_kernels.cu: TqInfo, the lane decode (tqp_states8, a byte window), the
dot product, the decode-once Split, the fp16 dequantizer and the format lists
for 147-149. They are reached only by a pack that holds these types.
- moe_mmq.cu, strata_ggml_tq_overlay.cmake, third_party/ggml-tq/tq.cuh,
ggml-common-tq.h: the MMQ prompt path for the three types (overlay
enumerators, traits, tile loader branch, instances).
- native_expert_parity: names tqp5-7 and skips its synthetic check, with a
message, on a ggml that has no trellis traits.
- docs/GYRO.md: the "not supported" sentence now says how Gyro-L is read.
Changes to the contributor's patch:
- tq_cpu.cpp called the AVX2 pair kernel without the x86 guard the rest of the
file has, which breaks the Metal build (it compiles tq_cpu.cpp without
tq_avx2.cpp): the pair rows go through one helper that falls back to the scalar kernel.
- moe_mmq.cu named GGML_TYPE_TQP5-7 under STRATA_MMQ_TQ, but the public
agentionai llama.cpp has the older trellis types and not these: a new
STRATA_MMQ_TQP (set for the overlay and for a tree that has the tqp instances)
guards them, so that tree still builds. The HIP overlay lists get the three
instances too, and ggml_tq_overlay_test checks them (and sets the CMake
policy its IN_LIST needs).
- native_expert_parity: the skip test compared a type-name pointer to null, but
ggml_type_name of an id past the table reads past it (the test crashed on
an upstream ggml); it compares against GGML_TYPE_COUNT.
- docs/GYRO.md keeps the plain description and leaves out the contributor's
measured speeds (we have not repeated them).
Tests added: src/kernels/cpu/tqp_cpu_test.cpp (no ggml, no model: the scalar
kernel against a bit-by-bit reference from the format definition, every ISA
equal to scalar for 1-8 tokens and for gate/up rows, the type predicates for ids
0-299 against the old formulas), the pair types in tq_pascal_bench (fp16
dequantizer bit for bit against the definition, decode kernels within 3e-2,
decode-once == per-column), tools/test_gguf_tqp.py, and native_expert_parity
tqp5/6/7 registered when the ggml tree has the tqp instances.
Checked (no model): tqp_cpu_test passes on x86 (AVX2 on z440; AVX2 and
AVX-512 on the owner PC with MSVC); tq_pascal_bench on an RTX 5070 (sm_120,
MSVC build): 0 failures, the TQP dequantizer matches the definition in all
bits, and the checksum lines of TQ2_T/TQK6/TQK7 are byte-identical to the
build without this change. Builds: CUDA sm_86 (gcc), sm_60 (CUDA 12.9),
sm_120 (MSVC), SYCL, and the CPU files for aarch64.
Not covered: the contributor's own parity runs need a ggml tree that carries the
types (not public here), and the 50 GiB model run (load check, coherent
answer, 32K prompt, MTP, Gyro-S/M identity) is still to be done. Metal, SYCL and
HIP have no kernels for the types; HIP was read, not built.
Problem (KeyanHu-git): the gated proposal needs the choice between the MTP window and a
lookup window to be fixed by the history, not by the draws of the rounds it will verify.
Freezing the policy for the whole request (the first commit) does that, but also freezes
the cost table, so a repeat-heavy request kept the window sizes it inherited. In the
author's diagnostic workload the frozen policy chose mean lookup windows of 5.87 and 3.38
on two repeat requests; with this change 4.31 and 4.05 (from the PR text; not
reproduced here).
Change: the request's copy of the DraftPolicy (source_policy) gets three calls, used only
when gated_proposal is on:
- observe_committed(): the round's accept and probe counts, applied at once. A lookup
window counts as a probe of its size (tried_), so a size whose cost is only guessed is
tried kProbes times, not every round.
- defer_cost(end, t, ms): the round's time is held. A time covers the window and the next
MTP draft, so it depends on every Philox counter up to
max(p + T - 1, p + a + 1 + S_mtp).
- apply_costs(committed): applies, in order, the held times whose end is behind the
committed position, with the same update observe() makes (including the STRATA_SPEC_COST
clamp), in the context bucket the round ran in; delayed_n_ keeps a held time from
counting as a second probe.
choose() counts the probes as cost_n_ - delayed_n_ + tried_.
Port note: the DraftPolicy on the current tree keeps its costs per context bucket
(STRATA_SPEC_COST), so tried_, delayed_n_ and the held times carry the bucket. A gated
request does not run with STRATA_SPEC_COST (it falls back to the older rule), so its
copy stays in bucket 0.
Default path: unchanged. choose() reads two new terms; on any path that never calls the new
functions tried_ and delayed_n_ are +0.0 and cost_n_ - 0.0 + 0.0 == cost_n_ exactly.
Checked by driving the old DraftPolicy (namespace-renamed copy of f-027-gh-eng) and the
new one with the same random call sequences (observe, observe_chain, observe_probs,
set_context, seed_costs, set_robust, choose, chain, keep_draft; 400 sequences of 3000
steps): 25,983,538 bitwise comparisons of picks, costs and rates, 0 mismatches; one
observe_committed call makes the same harness report 60,528 mismatches. generate.cpp:
nothing calls the new functions when gated_proposal is false.
Tests: draft_policy_test (the PR's added assertions, on the bucketed class) and
spec_admission_test pass on z440 and on the owner PC.
Not covered: the sampling argument for the delay boundary (that a held time cannot steer
a window through a counter that is drawn again) is the author's; a unit test cannot show
it, only the distribution run (gate script 3416-spec-prob-dist.sh, arm NEWLK) can.
Problem (KeyanHu-git): the rejection rule of the previous commit judges each row against
the target's probabilities p. For a native (IQ) pack the CPU experts round a token's rows
differently depending on how many tokens share its expert in the window (the grouped
kernels against ggml's one-token dot, #152). So p of one row moved with the window width,
and the same sampled text could depend on the draft length. On random blocks a grouped
Default call differs from the token alone in 700 to 12000 of about 18000 output floats
for the IQ gate/up and IQ4_NL down formats.
Change: NativeArithmetic { Default, WindowInvariant }, fixed for one pool batch.
native_gu_rows / native_down_rows become thin wrappers of one template with the arithmetic
as a parameter. The Default instance keeps every old branch (the new conditions are
compile-time true there). WindowInvariant gives each token the bits of the one-token call
on this build and CPU: kernels that already match it stay grouped (NVFP4, Trellis and
Hadamard rows, KQ256 when ggml runs its AVX2/FMA dot); the IQ rows that differ use
iq512_reference_gu_rows (types 18, 21, 22: the same decode, ggml AVX2's accumulation
order, one weight decode per group) or iq4nl_reference_rows (IQ4_NL down), and anything
else falls to the one-token dot per token. ExpertPool::run_split_multi_native takes the
arithmetic and expert_pool_dispatch_multi passes it; generate.cpp uses the invariant
pool only when the MTP drafts probabilistically (mtp.prob(), i.e. only with
STRATA_SPEC_PROB=1). Prefill, greedy decode and every other decode run Default.
The pool picks the entry point with one compare and a direct call per chunk.
Default path (STRATA_SPEC_PROB unset): unchanged, shown by running the old and the new
native_gu_rows / native_down_rows (Default) on the same random blocks: 61 format, width
and amplitude lines (IQ2_XXS/XS/S, IQ3_XXS/S, IQ4_XS, NVFP4, Q4_K, Trellis with and
without folded Hadamard, IQ4_NL / Q2_0 / Q5_1 / Q8_0 down; widths 1 to 8; row sub-ranges),
output hashes identical in 10 settings of the environment switches on z440 (AVX2) and
in 13 on the owner PC (Zen4, AVX-512, MSVC), including STRATA_NO_IQ512, STRATA_IQ_GATHER
and STRATA_IQ_MT_MIN=1. g++ codegen of the AVX-512 row kernels is not byte-identical for
5 of 87 functions (inlining and register allocation of the same source); the AVX-512
kernels were run only under MSVC.
Tests: native_arithmetic_parity, eight ctest configurations (default, AVX, KQ256,
IQ_MT_MIN=1, no512, ggml only, NVFP4 AVX2, NVFP4 ggml), registered in CMakeLists.txt and
sycl/CMakeLists.txt. It compares the invariant entry points at every group width 1..8
with the one-token calls, bitwise, for the IQ, NVFP4, Q4_K/Q5_1/Q8_0, Trellis and
Hadamard formats. All eight pass on z440 and on the owner PC; the existing CPU expert
tests (iq_avx2_parity, iq_avx1_parity, iq512_gather_parity, pool_tasks_test,
nvfp4_cpu_parity, router_dot_parity, q2_bitplane_parity) pass.
Not covered: an ARM CPU (not built here), an AVX-512 machine with fast gathers (Sapphire
Rapids), the layer-split call site, and any model run.
Problem (KeyanHu-git, refs #2028, #2591): with STRATA_SPEC_PROB=1 the default
--spec-min-p gate tests the token drawn from the draft head's distribution q, but the
verifier still judged the draft against the unfiltered q and treated a failed gate as a
plain sample from p. That shifts the output distribution: with p = (0.5, 0.5),
q = (0.875, 0.125) and threshold 0.5 it emits (0.5625, 0.4375).
Change: the gate is part of the proposal. Keep q's mass on the tokens that pass the gate
without renormalising (mu); a draw that fails the gate becomes NULL. An offered token D
is accepted with min(1, p(D)/mu(D)); a rejection and NULL draw from the normalised
positive part of p - mu; a position with no attempted proposal is a plain sample from p.
Serve and the CLI build the same packet (core/gated_proposal.hpp) and give it to the
verifier explicitly (Verifier::set_spec_proposal, sample_tokens_proposal). The explicit
call has no plain-p fallback: an invalid packet row is reported, not sampled around.
STRATA_SPEC_PROB_GATE=top keeps its normalised-q rule.
Default path (STRATA_SPEC_PROB unset): unchanged. mtp.prob() is false, so gated_proposal
is false, the new draft-result argument is null, GatedDraftLimit does nothing,
Verifier::run takes the old branch (set_spec_q clears the measure flag), and the
sampler kernel's old instance is the old code with the new branches compiled out.
Read hunk by hunk; no hunk runs a new branch or a new allocation per token. The
default-identity model run is written as a gate script and not run yet.
Port notes against the PR as submitted:
- Unsupported combinations (parallel or multi-device decode, --lookup-chain,
--adapt-swaps, PCIe share, --spec-min-p not finite, STRATA_SPEC_COST, DFlash2,
STRATA_SPEC_T_CYCLE, --spec-corrupt, Metal) used to be an error that ended the
request or the server. They now warn once and keep the older rule, so setting the
variable never stops a run. --adapt-swaps 96 and the PCIe share are on by default, so
the exact rule needs --lookup-chain 0 --adapt-swaps 0 --pcie-frac 0.
- The lookup source of a probabilistic window is chosen by a copy of the policy fixed at
the request start, without asking whether the MTP's first guess agrees. Learning from
committed rounds inside a request is the policy commit of this series.
- The CLI loop on the current tree has the out-of-memory window retry, DFlash2 and
STRATA_SPEC_T_CYCLE; the gated path does not use the retry (a narrower window would
change the room the packet was built for). Serve keeps merged first windows
(STRATA_ADMIT_MERGE_FIRST) at their length.
- docs/DETAILS.md: two short bullets instead of the PR's long paragraph (that paragraph
no longer exists on this tree). bench/results and the PNGs are not ported.
Tests: gated_proposal_test and spec_admission_test (default, pick, top, exact-top-only)
pass on z440 (CPU, g++ 13) and on the owner PC (MSVC). On the owner PC's RTX 5070
(sm_120, MSVC build, synthetic data, no model) spec_proposal_parity, spec_verify_parity and
sampler_parity (default, one-block and old-kernel configurations) pass. Also built:
CUDA sm_86 on z440 and boston, SYCL on P1006 (engine and host tests). The Metal path is
not built here.
Not covered: a model-level distribution test (chi-square of the first tokens against
plain sampling) and the identity run with the variable unset. Both are written as gate
scripts (3416-spec-prob-dist.sh, 3416-default-identity.sh) and not run.
Prefill on one card, decode on another, session handed over through RAM; exact token for token (state hash match at 4K
and 16K on 2x A4000 and 2x A6000). Nothing changes unless split_serve / --split-serve is set. On two alike cards it is
slower than the layer split on every workload measured (docs say so); it can only pay on dissimilar cards. Idea and
manual setup by MarcoTool (#4000).
Measured on 2x RTX A6000 (IQ3_S): slower than the layer split on every workload;
exactness checks pass (Q2_0 on 2x A4000, IQ3_S on 2x A6000).
Idea and manual setup by @MarcoTool in #4000.
No copy, code-constant, JSON-spelling, retrieval or unanswerable defects (retrieval 12/12, JSON 3/3, unanswerable 9/9, extraction equal to off); two long-winded answers, which the runs with the prior off also show two or three of per run. Story +15%, code +37% against off.
f-027-gh-eng gained STRATA_EXPERT_NIBBLE (4-bit expert slots, the sm_60 experimental
build): only run_impl gathers such a layer for MMQ. The layer-major walk would read
the slot as GGUF blocks, so lm_prepare now declines when any layer is converted and
the prompt takes the chunk-major path. Compiled out of every build without
STRATA_EXPERT_NIBBLE_BUILT.
The engine did not build on aarch64: the CPU expert path assumed the x86 AVX
kernels, the CPUID probes and the x86 spin/fence intrinsics. After this change
the strata target compiles on aarch64 (CUDA host code, native GGUF packs).
On x86 nothing is selected differently.
What changed:
- CMake: STRATA_CPU_X86 is decided from CMAKE_SYSTEM_PROCESSOR (an unset
processor stays the x86 build). On aarch64 src/kernels/cpu/portable.cpp
stands in for the AVX kernel sources; native experts run on ggml-cpu's NEON
dot products. -ffp-contract=off on aarch64. STRATA_ISA_FLOOR is ignored with
a warning there.
- include/strata/platform/cpu_relax.hpp: strata_cpu_pause / strata_store_fence
map to _mm_pause / _mm_sfence on x86 and to yield / dmb oshst on aarch64.
verify.cpp, pool.cpp, session.cpp, mtp.cpp and graph.cpp use them.
- expert_layout.cpp: the CPUID/XCR0 probes compile on x86 only and report no
AVX on other CPUs.
- generate.cpp: --cpu-oracle-q8-0 is rejected on a non-x86 CPU.
- setup.py: nvidia-smi's "[N/A]" memory on GB10 (unified memory) takes the
pool size from /proc/meminfo on ARM64 only; the CPU probe no longer needs
the x86 "flags" line; setup warns that the ARM64 engine is compiled on the
PC and which packs work.
Changes to the contributor's patch, found while moving it onto the 0.2.7 line:
- portable.cpp had no iq512x_rows / iq512x_supported / iq512_gather_on (added
to the x86 kernels after the PR was written): the aarch64 link would fail.
- The Metal build (Apple silicon) shares these files. It keeps its barrier
(dmb ishst), its plain router loop (its bf16_rows_dot_multi is a stub), its
Metal start-up block in generate.cpp and its "unknown" CPU name.
- The CPU side of the GPU doorbell: strata_load_fence() after the spins that
wait for a flag the GPU raised (dmb ishld on ARM, nothing on x86 and on
Apple silicon), so the data written before the flag is not read early.
- A card whose memory nvidia-smi does not report stays unlisted on x86 (the
new fallback is for ARM64 only).
- tests/terminal/test_setup_arm64.py.
Checked: with the x86 CUDA build (sm_86, gcc 13 -O3, z440) the objects of
verify, session, mtp, graph, expert_source, pool, expert_layout and generate
are byte-identical before and after (the x86 wrappers are macros for the same
intrinsics). Every C++ file of the strata target compiles for aarch64 with
clang, and the symbols of the CPU kernel library link up (syntax and symbols
only: no ARM machine, no ARM run). The Metal-shared files compile for
aarch64-macos with STRATA_GPU_METAL.
Not covered: no ARM hardware here. The tests that include x86 headers
(iq_avx2_parity and the other AVX parity tools) and the DeepSeek target do
not build on aarch64; the strata target does not need them. Memory ordering
on ARM is reasoned, not run. The Q2_0 and Gyro packs need the x86 kernels.
DETAILS.md gets two sections: the decode switches (GRAPH_PRIO, MMVQ_IL_PF) with the
two sm_80-only defaults named, and the prompt switches (PF_FUSED_NT / BATCH / F16 /
WS, GDN_TC, PLE_COMPACT / PLE_COPY_STREAM, EMBED_VRAM, DEQUANT_SIDE) with what is
bitwise and what is rounding-level. MMVQ_IL_TABLE.md gets the sm_80 row of the table
list and a paragraph on the hoisted-load tables (kIlPf_80 / kIlPf58_80, Q6_K only) and
on measuring a card with mmvq_il_parity --bench --emit-table --pf 0,3,4.
Rewritten from the PR's text: every figure is labelled as the reporter's (one CMP
170HX; we have no sm_80 card and measured none of them), and the prompt-switch
section is new (the PR documented only the decode switches and the conversation
cache).
Original docs by linxuhao (PR #3779, commit 6d6f0f39).
Problem: the interleaved small-batch matvec (native_mmvq_il) has one weight block
in flight per warp: its four `if (kbx < bpr)` sections each sit behind their own
branch, so on a GA100 (ncu: 432 GB/s on the qkv Q6_K matrix) the loads of a group
are not issued together.
Change, opt-in (every variant is bitwise native_mmvq): native_mmvq_il_pf_kernel
issues every weight load of a group (4 virtual warps x R rows) before any of the
group's dots, with the addresses clamped to the row's last block for a tail, and
with the ring variants the next group's loads before this group's dots. The
per-lane accumulation order is the plain kernel's. P2's forms replace
conversions by exact ones where the value is a small integer: (float)p becomes
p + 1.5 * 2^23 minus 1.5 * 2^23 for |p| < 2^22, and the saturating byte subtract
of Q6_K's codes (0..63, so nothing saturates) becomes two instructions. Q4_K,
Q5_K, IQ4_XS and Q6_K have the variants; Q3_K (the Q2_0 pack's type) keeps the
plain kernel.
- STRATA_MMVQ_IL_PF=1: the measured table (kIlPf_80 / kIlPf58_80: Q6_K only, read
only when the card is sm_80; on every other card 1 does nothing);
=2..6: one variant for every type and shape (A/B only, any CUDA card).
- Unset or 0: the plain kernels through the same launch code (the variant argument
is 0, and the added code is one cached environment read and a switch).
- CUDA only; HIP has no interleaved path.
Tests: mmvq_il_parity --pf a,b,.. bit-checks each listed variant against
native_mmvq for every type and shape, T = 1..8 and rows 0/1/2/4, and with --bench
times them interleaved (PFBENCH lines) and prints the joint (variant, rows) table
in the source form of kIlPf_<cc>. New ctest entry mmvq_il_parity_pf runs it with
--pf 0,2,3,4,5,6 on whatever card is present.
Reporter's numbers (linxuhao, CMP 170HX): decode +1.4 % (two reference / on / reference /
on sets) over the plain kernels; with STRATA_GRAPH_PRIO decode +1.3 % in their
per-feature pair (4K prompts +3.9 %, 16K +1.2 %, 52K +0.2 % in the same pair, which
the reporter reads as noise). Q4_K, Q5_K and IQ4_XS variants tie the plain kernels
there. Theirs, not measured here.
Original change by linxuhao (PR #3779, commit 116992ab).
Harness only; no engine code. The bench part of mmvq_il_parity times the
interleaved matvec cold (24 copies of the weights rotated past the L2), hot (one
copy), hot with other weights' reads in between (--gap MB, what a window's GEMVs do
between a prefetch and its consumer), and with the timed weights in a persisting L2
set-aside (--persist); --ab runs the five arms per case in one process, because
the clocks ramp over the first ~100 ms of a run. A 150 ms warm-up of launches runs
before the first timed case, and MMVQ_IL_TMIN bounds T from below.
The parity part is unchanged: without --bench the same checks run in the same
order (every rows choice against native_mmvq, bitwise). The bench code is CUDA only
(the test is built with STRATA_ENABLE_CUDA); the persisting-L2 calls are left out
under STRATA_HIP_GFX906.
Original change by linxuhao (PR #3779, commit 1f4b8139).
Problem: the decode window graph runs every kernel node at the launch stream's
priority, so the one-block indexer kernels of the side branch wait behind the
queued blocks of the big q GEMV, and the shared expert competes with the routed
experts.
Change, opt-in (bitwise by construction: no kernel changes, the same launches on
the same inputs; only the dispatch order moves): with STRATA_GRAPH_PRIO=1 the
side mixer's branch stream (GDN gates, QSA indexer chain) is created at the
device's highest priority, cs_ and side2_ in the middle, the shared expert's
stream at the lowest, and the window graph is instantiated with
cudaGraphInstantiateFlagUseNodePriority. If a driver did not copy the stream
priority into the nodes, the capture-time notes (the branch forks and the
priority streams' tails) set the attribute per node. One log line per captured
window prints the node priority histogram. A card whose priority range is 0..0
changes nothing.
CUDA only: the members and the three methods sit under !STRATA_USE_HIP &&
!STRATA_HIP_GFX906 && !STRATA_GPU_METAL; graph_prio_on() is false there.
Merged by hand with #3702 (the window-graph eviction): instantiate_evicting now
takes both keep_t and the flags, and the retry after freeing other window
sizes keeps the flags (the merged code would otherwise have dropped the node
priorities on that retry).
With the variable unset: every stream is created as before and the capture-time
notes return at once.
Tests: none of its own (a priority is a scheduling hint with no value to compare).
The window graphs are exercised by the engine; see the checklist for the
reporter's greedy-answer identity check.
Reporter's numbers (linxuhao, CMP 170HX, IQ3_S, --spec 3, 3K context, interleaved in
one session, each arm with STRATA_QIL=1): reference 125.3 / 126.6, GRAPH_PRIO
127.8 / 129.0, reference 125.0 / 126.8 tok/s, +1.8 %. Greedy 11 of 11 identical. In
the later per-feature pair together with STRATA_MMVQ_IL_PF: decode +1.3 %. Theirs, not
measured here.
Original change by linxuhao (PR #3779, commit 68c487c3).
One section in DETAILS.md for the switches the four conversation-cache commits
add: what each copies where, its VRAM and host memory cost, that they stay off
on HIP, gfx906 and Metal, and the measured times.
Rewritten from the PR's section: the times are labelled as the reporter's
(linxuhao, one CMP 170HX on PCIe Gen2 x4; we have no sm_80 card and measured
none of them), the independent RTX 3090 data of bitaccesscomau is added, and the
pool row describes STRATA_CKPT_POOL_READY=N instead of the PR's change to the
meaning of STRATA_CKPT_POOL=N (STRATA_CKPT_POOL=1 is unchanged).
Original section by linxuhao (PR #3779, commit a04f4399).
Problem: restoring a parked conversation copies its K/V from the host over the
link: 27-107 ms over a PCIe Gen2 x4 link.
Change, opt-in and only with STRATA_PARK_ASYNC=1 (CUDA only): a park also keeps
the whole K/V of its image in a VRAM slot of its own (64 MiB granules, up to N MiB
in all, only while 1.5 GB of VRAM stays free beside it); the background copy to
the host reads from the slot, and a restore of that image copies device to device
from the slot (about 1 ms) without waiting for the background copy. Each K/V
layer carries its own join-and-release token, so any order of destruction is safe
(the layers outlive their image when they are retained for the next park). Under
VRAM pressure the elastic K/V's growth releases every slot and the stage, then
retries once (both growth sites in generate.cpp).
With the variable unset the slot code is not reached: the budget is 0, a restore
finds dev_tag 0 and takes the host path, and the growth sites see
conversation_park_vram_release_all() return 0 and behave as before.
Tests: none of its own (it needs a card with the VRAM for the slot and a parked
conversation); see the checklist for the reporters' A/B/A conversation check with
STRATA_PARK_VRAM_MIB set.
Reporter's numbers (linxuhao, CMP 170HX): restore of a 3K conversation 28-47 ->
7-12 ms; outputs identical. The first restore of a run is slower (41.6 / 56.9 ms)
than later ones (6.5-12.6 ms) and they have not found why. Theirs, not measured
here.
Original change by linxuhao (PR #3779, commit bafca410).
Problem (reporter, CMP 170HX, PCIe Gen2 x4): a conversation switch's park still
paid 38-52 ms for the live state, because the STRATA_CKPT_POOL pool was empty at
every park (a switch takes two checkpoints), so the 118 MB GDN vector was
allocated fresh and page-faulted; and a checkpoint's 37 small synchronous
device-to-host copies queue behind the 4 MiB DMA pieces of STRATA_CKPT_ASYNC.
Change:
- STRATA_CKPT_POOL_READY=N (opt-in, 1..16, with STRATA_CKPT_POOL on): the pool
keeps N ready checkpoints, pre-sized to the session's state sizes with their
pages in, topped up on a background thread after any take (not with a layer
split).
- STRATA_CKPT_GATHER=1 (CUDA, opt-in): the small running-state vectors (ple, and
each layer's indexer tail, dead row and block position) are gathered device to
device into one buffer and fetched with one copy into a pinned buffer.
- The park stage is allocated rounded up to 64 MiB (a growing conversation does not
cudaFree/cudaMalloc it every time) and a VRAM-resident K/V skips the pointer
attribute query.
- The background helpers (pool top-up, the checkpoint DMA, the park DMA) leave the
host's pinned core (aux_cpus.hpp); no output depends on where a thread runs.
- A parked image whose live GDN state is still being copied joins before its
buffers are freed (SavedConversation::live_pending).
- STRATA_PARK_TRACE prints the joins, the live-state vectors' readiness and the
K/V sub-timers.
Differs from the PR as submitted: the PR made STRATA_CKPT_POOL=1 allocate two
ready checkpoints at startup (+236 MB of host RSS) and read STRATA_CKPT_POOL=N as
a count. That changes what an existing switch does, so here STRATA_CKPT_POOL=1
keeps its meaning (the pool holds dropped checkpoints only, nothing is allocated
ahead of time) and the ready pool has its own variable, STRATA_CKPT_POOL_READY.
A user of the PR's STRATA_CKPT_POOL=2 sets STRATA_CKPT_POOL=1
STRATA_CKPT_POOL_READY=2. The PR's one-checkpoint refill after a park with
STRATA_PARK_ASYNC is replaced by the top-up, as in the PR, and so is only active
with the new variable. Merged by hand into #2526's park_source.
With none of the new variables set: the stage size is rounded up only when the
park stage is on, the extra code is trace output behind STRATA_PARK_TRACE and
the join calls.
Reporter's numbers (linxuhao, CMP 170HX): park at a 3K context 64-75 -> 21-40 ms
(all park switches together, A/B/A conversation check identical, greedy 11 of 11
identical). Theirs, not measured here.
Original change by linxuhao (PR #3779, commit 56088e71).
Problem: parking the outgoing conversation copies its K/V device to host and
deep-copies its checkpoints before the next prompt can start: 213 ms at a 3K
context, 334 ms at 13K on a PCIe Gen2 x4 card (the reporter's CMP 170HX).
Change, opt-in and CUDA only (off on HIP, gfx906 and Metal):
- STRATA_PARK_ASYNC=1: conversation_kv_save copies the device K/V device to
device into a VRAM stage (only while 1.5 GB stay free beside it; otherwise the
save copies as before), and a background thread moves the stage into the host
buffers in 4 MiB pieces through two pinned buffers. conversation_park_join
waits for it: every reader of a parked image's K/V, the cache before it frees an
entry, and the image itself (SavedConversation::pending) before its buffers go.
ConversationBuffer::resize_for_overwrite leaves the added bytes unfilled for
that copy; the buffer's segments use an allocator that value-initializes only
when a fill value is given, and resize(n, value) is unchanged for every other
caller.
- With it, the checkpoints this request drops right after the park (all of them
when another state comes in, else the ones off this prompt's path) move into
the parked image instead of being copied, and a background allocation refills
STRATA_CKPT_POOL for the next save. Only the main session's checkpoints move,
not an idle batch slot's (the park_source of #2526 serves both).
- conversation_snapshot_save takes dropped checkpoints' storage (the existing
STRATA_CKPT_POOL) for its copies; with the pool off, nothing is taken.
- Unset: park_drops is null, no storage is taken, the K/V copies are the
synchronous ones; the added work is the K/V size sum for the stage request and
a few join calls.
Differs from the PR as submitted: the PR's join in conversation_file.cpp
(put_payload) is not ported; the SAVE/RESTORE handler of generate.cpp joins the
park first (see the previous commit, which carries the PR's 33a8146a fix).
Hand-merged into the park_source / ParkSource refactor of #2526.
Not covered: if the background copy fails after resize_for_overwrite, the
parked image keeps unfilled bytes; the failure is logged. A restore of such an
image would read them. No test of the park path with a failing copy.
Tests: the PARK_ASYNC path has no unit test in this commit (it needs a card with 1.5 GB free for the
stage); the reporter's A/B/A conversation check covers it, see the checklist.
Reporter's numbers (linxuhao, CMP 170HX): park 213 -> 64-75 ms (3K), 334 -> 68 ms
(13K); greedy answers identical on their 11 prompts and the A/B/A conversation
check. Theirs, not measured here.
Original change by linxuhao (PR #3779, commit 3e0c141c).
Problem: on a slow host link a turn checkpoint copies the whole GDN state to
the host on the request path (118 MB at 1.64 GB/s on a PCIe Gen2 x4 card, 72 ms;
an independent RTX 3090 on PCIe 3.0 x16 showed 73 ms to 62 ms with the switch).
Restoring a parked conversation copies it back.
Change, both opt-in and CUDA only (off on HIP, gfx906 and Metal):
- STRATA_CKPT_ASYNC=1: the GDN state is copied device to device into a VRAM stage
(one buffer the size of the state, allocated at the first checkpoint save, plus
a pinned host buffer of the same size; if either allocation fails the save falls
back to the synchronous copy), and a background thread moves it in 4 MiB pieces
into the checkpoint's host vector. Every reader joins first and only for the copy
it needs (restore, snapshot save and restore, the parked copies, the serve loop at
each request start, the SAVE/RESTORE session handler).
- STRATA_CKPT_VRAM_SLOTS=N (with ASYNC): the last N saved GDN states stay in VRAM,
tag-checked, only allocated while 1.5 GB stays free; a restore of a checkpoint
whose tag a slot holds copies device to device.
- STRATA_TRACE lines carry a steady-clock timestamp; STRATA_PARK_TRACE prints the
timing of the save steps.
- Unset: the code adds a mutex-guarded no-op join per checkpoint save, restore and
snapshot, a tag field that stays 0 and a few clock reads; the copies are the
synchronous ones as before.
Differs from the PR as submitted: the PR put a join into conversation_file.cpp
(put_checkpoint), which broke the conversation_file_test and
session_save_reclaim_test builds without CUDA and matched only the copied vector,
not the one the copy in flight writes, so a SAVE within one checkpoint copy after a
turn could write an incomplete GDN state (found by bitaccesscomau; fixed in the
PR's last commit 33a8146a). This commit carries that fix: conversation_file.cpp
stays as it is and the join runs first in the SAVE/RESTORE handler of
generate.cpp. The commit 33a8146a is therefore not a separate commit here.
One more join, found by running the new ctest entries: conversation_snapshot_sources (the SAVE
path of a session file) saves the running state with the background copy and the caller writes
it to the file at once, after the handler's join has already run. It now joins the copy of
that checkpoint before it returns; without it a session file written with
STRATA_CKPT_ASYNC=1 could hold an incomplete GDN state (conversation_snapshot_test failed
at "both files read" before the join).
Tests: conversation_snapshot_test now joins after each save and, with
STRATA_TEST_BIG_GDN=1 (a GDN state over 1 MiB, so the async path is taken), runs as
the new ctest entries conversation_snapshot_test_ckpt_async and
conversation_snapshot_test_ckpt_vram_slots: every checkpoint, capture and restore
must give the same bytes as the plain run.
Not covered: no test destroys a checkpoint while its copy is in flight (readers
join; the cache's eviction paths do not); the reporter saw 3 of about 35 engine
starts stop answering after a park with evictions, on the paths with the
switches off, and could not reproduce it in 15 reruns. The VRAM stage has no
headroom check of its own (an RTX 3090 with 147 MiB minimum free showed no failure
in 42 of 42 saves; the allocation failure falls back silently).
Reporter's numbers (linxuhao, CMP 170HX): turn checkpoint 71.4 -> 0.9 ms,
restore 114-126 -> 28-47 ms. Independent data (bitaccesscomau, RTX 3090 24 GB,
PCIe 3.0 x16, release defaults, 4 pairs): checkpoint save 73.2 -> 62.4 ms, tiny-prompt
time to first token median ratio 1.038, outputs byte-identical. Theirs, not measured
here.
Original change by linxuhao (PR #3779, commits 0d512925 and 33a8146a).
Problem: the prompt path's DeltaNet recurrence is a large part of the prompt time
of a GA100 card.
Change, opt-in and rounding-level (the output is not the FP32 kernels' bits):
src/prefill/gdn_tc.cuh holds the chunked WY-form recurrence with every product
on the tensor cores (f16x3 splits with power-of-two scaling, one block per value
head) and its prep kernel; kernels.cu dispatches it before the STRATA_GDN_CHUNKED
branch for prompt chunks of 128+ tokens when STRATA_GDN_TC is set.
Where it runs and where it does not:
- gdn_rec_tc returns "not supported" and the existing kernels run unchanged on a
card below sm_80, with fewer than 48 SMs, or with less than 122 KB of
shared memory a block (the kernel needs 124,944 B). That excludes sm_86, sm_89
and sm_120 (99 KB opt-in): the code is compiled into those builds and never
launched there. It runs on sm_80 and sm_90. A failed scratch allocation
(60.3 MB a device) also falls back.
- Device code for cards below sm_80 compiles to trapping stubs behind
STRATA_GDN_TC_SM80; the whole header sits under !__HIPCC__.
- Unset, the dispatch costs one cached flag test per call.
Tests: gdn_rec_parity gains the tc variant (T = 1..4099, 8192, 32768, six stress
inputs against gdn_rec_kh_kernel, gate 1e-4; "not on this card" where it cannot
run), --selftest-frag (each tensor-core helper on one warp against a double
reference) and --bench-tc. The new ctest entry gdn_rec_parity_selftest_frag runs
the helper self-test, which any sm_80+ card can run (77 below sm_80). The full
tc variant can only run on an sm_80 or sm_90 card.
Reporter's numbers (linxuhao, CMP 170HX): recurrence 105 -> 34 ms at 3.3K tokens
and 1,705 -> 547 ms at 52K; prompts +3.9 % (4K), +6.0 % (16K), +6.7 % (52K) over
the same base; within 2.3e-6 of the FP32 kernel in gdn_rec_parity; teacher-forced
KL against the bitwise base mean 0.0157, median 0.0011, max 0.098, top-1 100 %;
1 of 5 conversation checks identical and decode -2.6 % in their pair (they
think draft acceptance moves with the rounding; not checked). Theirs, not
measured here.
Original change by linxuhao (PR #3779, commit f1cabc99).
Problem: the NT = 256 kernels still decode every 32 values to int8 and scale
the int32 products in FP32.
Change, both opt-in and both rounding-level (the output is not bit-identical to the
int8 kernels; they are the PR's "rounding-level" switches):
- STRATA_PF_FUSED_F16=1 (implies the NT = 256 set): the decoded weights times
their block scales and the activations times theirs as F16, m16n8k16 F16
products into FP32 accumulators. The k order inside a 16-value group is
permuted the same way for both operands.
- STRATA_PF_FUSED_WS=1: native_ws_kernel, producer warps decode into an F16 ring
in shared memory and consumer warps run the same F16 products (named barriers),
so it has the F16 path's rounding. One block per multiprocessor; about 101 KB of
shared memory at its defaults, so it is set up on sm_80 / sm_90 cards and
says "not available" and keeps the 512-thread kernels on an sm_86 / sm_89 /
sm_120 card (99 KB). Compile-time knobs STRATA_WS_PROD / _SLOTS / _NB / _KS / _HTAB.
- Both take a WideBatch like the NT = 256 kernels, so STRATA_PF_BATCH above 128
reaches them. CUDA only (under !__HIPCC__); the Turing pass compiles the F16
instances against __trap() stubs and never runs them (dev_info stops before the
NT = 256 setup on Turing).
- With neither variable set the kernels, their launches and their shared-memory
sizes are as before (the F16 template parameter defaults to false).
Differs from the PR as submitted: when STRATA_PF_FUSED_Q2N=1 is on as well
(#3809, after the PR was written) the WS setup also sets up the Q2_0 gate/up
instance.
Tests: prefill_fused_iq_test_f16 and prefill_fused_iq_test_ws (ctest) run the
fused-experts test (double-precision reference and MMQ) with the switch set. The
test now skips (77) when a requested NT / F16 / WS kernel set is not set up on the
device, so a card that cannot run WS does not report a pass for the 512-thread
kernels.
Limits of the test for these two arms only: with STRATA_PF_FUSED_F16=1 or _WS=1 the
test uses the limits it already has for STRATA_PF_F16 (F16 products): fused against the
int8-rounded model rel RMS 5e-3 (2e-3 otherwise) and against MMQ 5e-3 (1e-3 otherwise).
Measured with F16 on an RTX A4000 (sm_86, z440), over the 11 pairs: against the int8-rounded
model rel RMS 2.85e-3 to 2.90e-3 and worst row 5.9e-3 to 8.1e-3; against MMQ the same;
against the FP32 reference 1.438e-2 to 1.480e-2 with MMQ at 1.435e-2 to 1.478e-2 (the
fused path stays within 1.003x of MMQ's rel RMS and 1.03x of its worst row). The rounding is the F16 rounding of the
weights times their scales and of the activations times theirs (an 11-bit mantissa, about
2^-11 relative per operand), which the int8-rounded model does not have; it is not a
different summation order. The same arm on an RTX PRO 5000 (sm_120): up to 2.90e-3 (rel RMS), worst row up to 8.1e-3.
The default path and the NT=256 arm keep the old limits and their printed errors are
unchanged: with no switch set the test prints identical output (52 lines) from the base
tree and this tree, on an RTX A4000 (sm_86) and on an RTX PRO 5000 (sm_120).
Reporter's numbers (linxuhao, CMP 170HX): one MoE layer of 3,256 tokens 8.27 ms
(NT = 256) to 7.90 ms (F16) and 7.65 ms (WS); prompts +2..+6 % over the
256-thread kernels, 1 of 11 greedy answers changes. Teacher-forced KL against the
bitwise base on their 20-prompt set: F16 / WS mean 0.0062, median 0.00006, max
0.076, top-1 100 %. Theirs, not measured here.
Original change by linxuhao (PR #3779, commit 579cbfbb).
Problem: a layer split with --batch pipelines the slots in groups, and the pipelined groups run no MTP drafts.
--batch-mtp with serial windows leaves all but one card waiting. A multi-stream deployment wants both.
What changed: STRATA_PIPED_MTP=N (with --batch-mtp) keeps the pipeline and drafts in it. A group's window carries
each of its slots' rows: the slot's token and up to N MTP proposals (at most --spec - 1; the first always, each
further one while the draft layer was at least --spec-min-p sure of the one before). The last stage judges each
slot's proposals on their own. The number of rows to keep is known only then, and by then the earlier stages run
other groups' windows, which reuse the scratch the commit replays from. So a small save graph copies those rows
right behind each window (Verifier::batch_launch_rows), and every stage queues the commit of the kept rows after
the verdict (Verifier::batch_commit_rows). The slot drafter catches up on its own rows and drafts the next
proposals as an asynchronous chain (MtpDrafter::chain_launch). Unset, nothing changes.
Changes on top of the PR as sent (review):
- The verifier window size is worked out where the verifiers are built, from the slots that fit and the final
groups, not earlier from --batch as asked. Unset it is the old expression.
- STRATA_PIPED_MTP is decided again once the slots that fit are known. It says it is off, and why, when fewer than
2 groups are left, when STRATA_BATCH_SELF_COMMIT is on, or when a group's window would hold more than
kVerifyMaxT rows. The start line names groups and slots correctly.
- batch_groups::resolve keeps its old mtp_dropped rule; only mtp_piped is new, and it also applies with
--batch-groups given (the PR's fix 1). The "--batch-groups 1" test case now expects "not dropped".
- --remote-expert-opt alone no longer blocks --batch-mtp on a layer split: the object it guards is only made with
a helper expert cache (the PR's fix 2).
- docs/BATCHING.md rewritten with the numbers measured here.
Tests: batch_groups_test (the PR's cases, four "unset is as before" cases); builds on CUDA sm_86 (Linux), SYCL
(the sycl tree has its own verify.cpp and generate.cpp and does not include this code) and MSVC. Model runs:
see docs/BATCHING.md and the gate scripts 4121-*.sh.
Not covered: AMD (HIP) was not built or run; the draft layer on the first stage; helper expert caches.
2.0 showed copy and code defects on the RTX 3060 (IQ3_XXS); 1.5 showed none in 19 tasks and keeps the speed
(story +15%, code +37% vs off). The 61-task check decides 1.5 vs a 1.0 fallback.
+6.9% Qwen IQ3_S decode on Strix Halo Windows, prompts unchanged; token ids identical in 4 starts x 5 prompts.
Linux gfx1151 and other cards unchanged; STRATA_DEFAULTS=026 restores it. Docs label the DeepSeek Strix numbers by build.
The Windows guide says what 0.2.7 does and how to turn the switch back on, backed by five fixed greedy prompts
(895 ids) run four times with the switch on, off, on, off: identical ids. STRIX_HALO.md and the 0.2.7 release notes
get a line. The DeepSeek 4K prompt figures of 2026-10-25 (78.7, 87.2) stay and are labelled with their build; a table
of the 0.2.7 candidate changes one thing at a time (chunk size, DS4_DEFAULTS=026).
Interleaved A/B on a Strix Halo Windows PC (Qwen IQ3_S, engine 0.1.51): chat decode 43.6 tok/s with the fork,
46.6 without (+6.9%), prompts unchanged. Linux gfx1151 and every other architecture keep their table.
STRATA_SH_STREAM=1 turns it back on, STRATA_DEFAULTS=026 keeps v0.2.6's table.
arch_default_env(arch, defaults026, windows) is a pure rule; arch_defaults_test checks both platforms.
Ported PRs (authors kept) and our fixes, plus the owner's part-2 items: the restored TROUBLESHOOTING entries with a
link check, #3895 (no shared-expert fork calibration on Windows HIP with --kv-resident; a warning on a hand-set
STRATA_SH_STREAM=1), #3930 (docs), #3421 (Docker stops when models are present and MODEL_PATH is unset), #3570
(launch --show-key reads setup's config), #3311 (engine paragraph only) and #3029 (Strix Halo Windows guide,
re-measured on 0.2.7).
Structure from the guide in PR #3029 by HaiYunChuang (what the PC reaches, memory and the BIOS, the configuration, what is on by default, traps),
rewritten for the 0.2.7 install and with every number measured again: the Terminal and the gfx1151 engines built from f-027-int2 (3e468f5de), a
local signed test feed, strata launch as a standard test user on the NucBox EVO-X2 (Adrenalin 26.10.1), tools/strata_bench.py over a tunnel.
Qwen IQ3_S: 44 tok/s chat, prompts 755 / 821 / 792 tok/s at 4K / 16K / 32K. DeepSeek 2.00 bpw: 12 to 14 tok/s chat, prompts 214 to 342 tok/s.
An interleaved A/B of STRATA_SH_STREAM on Windows gfx1151 (three pairs): off is 6.9% faster in chat decode, prompts unchanged.
What launch chose and logged is quoted instead of a hand-made config. The first guide's tuning rows (engines 0.1.41 to 0.1.47) are not repeated.
Co-authored-by: HaiYunChuang <[email protected]>
Part 1 fixes and 12 ported PRs (authors kept), and the owner's part-2 items: the Monitor readings strip with
local currency (#3628), approval notification + opt-in minimise to tray (#3477), opt-in done sound (#3586),
app port setting with a busy warning (#3402) and interface size up to 200% (#3970). Host tests: only the two
known base failures on Linux; connector-setup and the new tests pass on Windows.
Problem: Gemm::native dequantizes each dense weight to FP16 in front of its
GEMM on the compute stream, so the dequant never overlaps the kernels queued
ahead of it.
Change, opt-in (CUDA only, always off on HIP): with STRATA_DEQUANT_SIDE=1 the
prompt path takes a second GEMM scratch (64 MiB more of its region, counted in
bytes_needed and carried through relayout) and Gemm::set_dequant_side hands it
to the Gemm. native() then dequantizes the next weight on a non-blocking side
stream into the scratch the compute stream read last but one. Four events order
every write and read of each scratch (free -> dequant -> ready -> GEMM -> free),
so each product sees the same bytes as in the serial form. The row-sliced path
(a weight larger than the scratch) stays serial. Metal answers false and keeps
the serial form.
Differs from the PR as submitted: if the side stream, its events or the
setup fail, the PR failed the prompt-path init. Here one line is printed and the
dense weights are dequantized in front of their GEMMs as before. Unset, nothing
changes (gemm_dequant_side() is one cached flag; the scratch, stream and events
are not created).
Test: new gemm_dequant_side_parity (ctest, GPU, synthetic, 77 without a device):
eight Q4_K / Q5_K / Q6_K weights of the engine's shapes (one larger than the
scratch, so it takes the sliced path) run three rounds through Gemm::native
serially and with the side stream, with and without a spin kernel queued ahead of
each GEMM; every output must be identical bit for bit.
Not covered: dense weights read by the side stream are the resident ones; a weight
that arrived by an upload on another stream would need its own event (the prompt
path has none that do).
Reporter's numbers (linxuhao, CMP 170HX): measured with STRATA_PLE_COPY_STREAM and
STRATA_EMBED_VRAM, the four PLE / embedding / dequant switches together give
+2.2..+2.6 % prompts. Theirs, not measured here.
Original change by linxuhao (PR #3779, commit 741fc6ab).
DS4_DEFAULTS=026 behind the DeepSeek speed-ups Off toggle, the 12 GB NVIDIA route-prior row, the Advanced-only
Qwen '0.2.6 behaviour' row (STRATA_DEFAULTS=026), and the app-ui Quit test fix. Terminal suites pass; host suite
failures on Linux also fail on the base.
Owner decision for 0.2.7: the tables of PR #3453 (the cells where an RTX 4070 and an RTX 4080 community run gave the same rows count, and
the one-column Q3_K row) ship on for sm_89 cards, untested on Ada by us. The output is bitwise the same with them off (a table only
changes how many rows a warp takes); no end-to-end gain was seen on a 4090 or 4080 in the report, so the reporter tests 0.2.7 and reports.
The rule is a pure function in sm120_series.hpp: mmvq_il_89_default(hip, cc, v026) is true for cc 89 only, false in HIP builds and under
STRATA_DEFAULTS=026 (defaults_026.hpp, as 38c9080db did for the other defaults); mmvq_il_89_resolve() adds the variable: "1" and "0" win,
unset or other text is the rule. native_mmvq.cu reads it once. arch_defaults_test checks cc 89 on, every other cc off, HIP off, 026 off,
"0" off, "1" wins over 026, the empty text. mmvq_il_parity expects the on-tables by default and the old ones with =0 or 026.
Docs: MMVQ_IL_TABLE.md, DETAILS.md and the 0.2.7 release notes (a line that says it is not measured on an RTX 40 card, with the off switch).
French catalogs for the app (3,528 strings + fix/tune/tools catalogs) and the served web page (247 keys),
README.pl.md from the current README (#3355, Jakub Luwierski), Polski in every README language row, and the
README.md language row's double-encoded names fixed. Tests: 21 host files 180 pass on p1006, Terminal and web
i18n checks on l620.
serve/web/locales/fr.json (247 keys, plurals one/other, same terms as the app catalog) and the French line in serve/web/i18n.js. tools/web_i18n.py and serve/test_web_i18n.py (node and python) pass.
Ports the Polish translation from #3355 and the Polski entry in the language row of every README. The pull request translated the older long README, so README.pl.md is rewritten from the current README.md (logo, centred language row, DeepSeek speed tables including Strix Halo, Gyro, coffee button). README.md's own language row had garbled names (double-encoded text); it is written out correctly.
Co-authored-by: Jakub Luwierski <[email protected]>
fr.json, fix-fr.json, tune-fr.json and tools-fr.json (3,654 strings, vous, plain short words), the Francais entry in the language picker, every locale test and gap check now covers fr, a french-catalog test (spacing before ? ! : ;, apostrophes, short labels, no leftover English), scripts/fr_sync.py to list and merge strings added later, and scripts/language-shots.mjs for screenshots of the app in one language.
Wide/narrow CSS (#3176), page translations with a language choice, opt-in KaTeX math (#2972, #2974),
sandboxed svg/html preview (#1628), Load/Unload in the header (#3550), and one chat-history design
(#1787, #2615, #3823, #4004). Tests: browser suite on the owner PC, node + pytest serve suites on boston
(457 passed, 2 skipped).
Problem: on a GP100 die the verify window's gate/up kernels for the IQ2_S,
IQ3_XXS and IQ3_S expert formats are not held by memory bandwidth but by each
block's serial chain (unpack the codebook indices, the emulated dp4a, the
16-lane reduction); the same kernel with every dot product removed still takes
about two thirds of its time.
Change: with STRATA_EXPERT_NIBBLE=1 the engine, after the expert cache is
filled and every expert is resident, transcodes the gate/up blocks of every
fully resident IQ2_S / IQ3_XXS / IQ3_S layer (n_embd 2560, n_ff 640) into slots
of 4-bit codes, 16 bytes per 32 weights, with the integer scales and fp16 d kept
as they are; the down rows stay GGUF. The decode gate/up reads those slots with
kernels that run the production integer chain and float tail in the production
order (R4S / R4P / R4SV / R4PX by format and group size); the prompt path's MMQ
gather rebuilds the GGUF blocks from a slot on the fly. At load every item is
round-tripped and every slot's rebuilt GGUF blocks are compared with the staged
blob before the cache is sealed (ExpertCache:🦭 a later fill_slot stops the
engine). New: include/strata/core/expert_nibble.hpp (slot contract, switches,
per-layer table), nib_gu_proto.cuh (STRATA_NIB_GU_PROTO=1 variant), the
transcoder, hash and rebuild kernels and the 4-bit gate/up kernels in
iq_kernels.cu, generate.cpp (the fill, the VRAM accounting, STRATA_EXPERT_NIBBLE_
RECHECK), iq_nibble_parity and native_expert_bench --nibble.
Review changes against the PR:
- An option the switch cannot work with (--peer-device, --kv-grow, --vram-elastic,
remote caches, STRATA_MMQ_DIRECT, ..., a card that is not cc 6.0, a build
without the kernels) now prints a warning and the start goes on with the usual
GGUF slots, where the PR refused the start. What is known only later (an expert
out of VRAM, a loan, the prompt chunk) still refuses, since slots are
converted by then.
- The checks this switch adds to expert_source.cpp and prefill.cpp are compiled
out of every build that has no 4-bit kernels, so the default builds add nothing
to the hit split or the prompt loop.
- iq_nibble_parity gets --synthetic-only (every gate/up format x both down
types on random experts, no model file), skips with 77 on a card that is not a
GP100, and the synthetic and --header runs are ctests.
- The PR's docs section is rewritten for these changes and its measurements are
labeled as the author's.
Default path (switch unset): the table is empty, nibble::slot_bytes(l) is
expert_layout().blob_bytes(l) and nibble_max_slot_bytes() is max_blob, every
sizing the PR touched returns what it returned before, and the new branches in
generate.cpp (guard_raw before the adaptive tier's copies, the profile fill's
test), expert_cache.cpp (one bool in the fill calls and admit) and verify.cpp
(a capture-time layout choice, compiled out without the kernels) are not taken.
Proof on the kernels: in the sm_60 build (CUDA 12.9) the SASS of all 446
kernels iq_kernels.cu had before is identical with and without this change,
as is that of the 603 in native_mmvq.cu, 29 in qsa_select.cu and 10 in
moe_mmq.cu; 21 kernels are new.
Tests: iq_nibble_parity --header passes on z440 and boston (the host half of the
contract; also a ctest). The GPU parts (--synthetic-only, a model shard) refuse a
card that is not a GP100, so they were only compiled here (sm_60, CUDA 12.9,
P1004: iq_nibble_parity and native_expert_bench build; the whole tree builds).
sm_86 build without the SM60 option (z440): exit 0, the 4-bit paths compiled out.
Not covered: no GP100 run. The model gate is
phaseA-tests/gh027/gates/3455-expert-nibble.sh: iq_nibble_parity --synthetic-only
and on a model's layers on a P100, then identical replies with the switch on and
off (a layer is converted only when every expert of every layer is resident, so
the rig needs the pack to fit whole), then the decode A/B. The contributor's
numbers (four GP100 dies, Swift 1.5 IQ3_S, 47 of 48 layers converted, +12.7 GiB
VRAM, decode 47.3 -> 51.9 tok/s, +9.7%; prompt reading 2.3-3.0% slower; replies
of 15 requests equal) were not re-measured, and the enabled path was written
against an older base than this branch's.
Problem: on a GP100 die (compute capability 6.0: no dp4a, no tensor cores) the
prompt pass's routed experts run as MMQ launches per 16-expert group: a
gate/up, a swiglu, a quantize and a down launch per group, each group's experts
gathered and padded to the group's largest row count, every MMQ tile decoding
its weight blocks again. With every expert resident the per-group overhead, not
the weight bytes, is a large share of a prompt read on this card.
Change: with STRATA_MMQ_WS=1, a layer whose routed experts are all resident on
this GPU runs quantize_scatter, then gate/up as ONE weight-stationary launch
over all its experts (read where they lie in the expert cache), ONE
swiglu_quant over all its rows, then the down as ONE launch. New
src/prefill/moe_ws*.cu(h): a CTA holds 128 (or 256) weight rows of one expert
and runs them against up to 64 of that expert's activation rows, a thread per
weight row, so each weight block is decoded once per pass; gate/up for
IQ2_XXS, IQ2_XS, IQ2_S, IQ3_XXS and IQ3_S, down for Q2_0 and IQ4_NL. Every
product keeps MMQ's int8 values, scales and per-32-value epilogue in its K
order, so GU, H and the down rows are MMQ's bits. ws::faster carries the
author's measured ws / MMQ time ratios per format and picks a layer only where
both products win at its rows per routed expert. prefill.cpp decides per layer
after quantize_scatter (all experts resident, this GPU alone, no peer or CPU
share, not the fused layout, not STRATA_MMQ_DIRECT, no Hadamard, no NVFP4 tail,
no FP4 activations, aligned, not STRATA_DBG_NAN); every other layer keeps its
path. STRATA_WS_FAST=1 (second opt-in, same bits) selects a down kernel for the
IQ4_NL 32-token tier with fewer instructions per pass.
Gating: the kernels and STRATA_PREFILL_WS exist only in the CUDA build with
STRATA_EXPERIMENTAL_SM60 (not HIP, not Metal; prefill.cpp links stubs that
answer mode 0 elsewhere). At run time ws::mode() is 1 only with STRATA_MMQ_WS=1
and every visible GPU at compute capability 6.0. Unset, nothing is allocated
(the offset and chunk buffers, under 1 MiB per die, exist only when the mode is
not 0), the arena layout and the mapped table are the ones before, and the
per-layer test short-circuits on the cached mode.
Not ported: moe_ws_down_bench (needs a model shard) and its CMake target.
Added: ws::set_mode (tests only) and tests/cuda/prefill_ws_parity_test.cpp, a
synthetic layer-level check: random experts of all 5 gate/up x 2 down formats,
random routing, the per-group MMQ (mmq::Context per expert) against ws::gate_up
+ swiglu_quant + ws::down, GU, Hq and Dm compared byte for byte, the down also
alone on the reference H and with STRATA_WS_FAST on. It gives its verdict on a
compute capability 6.0 card only (the kernels follow MMQ's sm_60 SASS); other
cards run it for information with --any.
Tests: prefill_ws_parity_test on boston (2x RTX A6000, sm_86, CUDA 13.1, gpu lock,
SM60 build, --any because it is not a GP100): 40 cases (all 5 gate/up x 2 down
formats, four routings each), 0 declined; the largest GU and down-alone error
against that card's MMQ is 5.2e-07 of the largest value (float rounding: MMQ's
sm_86 epilogue is not the sm_60 one the kernels follow), the full chain's Dm
differs up to 1e-3 where int8 rounding of H flips on those rounding
differences, and STRATA_WS_FAST output equals the default kernel's in all
cases, 0 differing bytes. The byte-for-byte verdict against MMQ is the P100's.
sm_60 compile of the whole tree (CUDA 12.9, P1004): exit 0; sm_86 build without
the SM60 option (z440): exit 0. SASS of the 10 kernels moe_mmq.cu had before
this change is identical in the sm_60 build.
Not covered: no GP100 run here. The contributor's numbers (prompt reads 31-37%
shorter on four GP100 dies with every expert resident: 240 tokens 2,156 -> 1,364
ms, a fresh 26K read 41,231 -> 28,555 ms; one run per arm) were not re-measured.
The model gate is phaseA-tests/gh027/gates/3752-mmq-ws.sh.
Problem: the token-embedding table of a native pack lives in mapped pinned host
memory, so every prompt's scattered row gather crosses the PCIe link. On a Gen2
x4 link that costs more than the table's size in VRAM is worth.
Change, opt-in: with STRATA_EMBED_VRAM=1 the table is copied into the current
device's VRAM when the table plus 512 MiB stay free (322 MiB for an IQ4_XS
table). The gather kernels read the same bytes through dev_ as before. If the
VRAM is not there or the copy fails, the table stays in mapped host memory as
before and one line says so. The start-up line now says where the table is
("in VRAM" also for the two existing VRAM fallbacks, which it called mapped
host memory).
With the variable unset nothing changes (one getenv at load).
Not covered: a layer split across several devices places the table on the
device that is current when it loads, as the existing VRAM fallback does. No
unit test (it moves the same bytes to a different memory; the gather kernel is
unchanged).
Reporter's numbers (linxuhao, CMP 170HX): measured together with
STRATA_PLE_COPY_STREAM and STRATA_DEQUANT_SIDE on a 51.8K-token prompt, the
embedding step went from 191-201 to 70 ms. Theirs, not measured here.
Original change by linxuhao (PR #3779, commit 952dd784).
Problem: with STRATA_PLE_COMPACT the raw-row upload sat on the compute stream
before layer 1, which idles for the DMA (4.7 MB per 3K-token chunk, 2.9 ms on a
Gen2 x4 link).
Change, opt-in, only with STRATA_PLE_COMPACT=1: the raw rows go up on a
non-blocking stream of their own once the previous chunk's dequant has read the
device buffer (ple_raw_free event); the compute stream waits for the upload
(ple_copied) only at the dequant. Same bytes, same kernel. The stream is drained
when a run ends so no upload is left in flight from a buffer the next run
refills.
Differs from the PR as submitted: if the stream or the event cannot be created,
the PR failed Prefill::init. Here it prints one line and the rows go up on the
compute stream as with STRATA_PLE_COMPACT alone. With the variable unset
nothing changes (the stream and the events stay null).
Reporter's numbers (linxuhao, CMP 170HX): PLE time of a 51.8K-token prompt 95-103
to 47 ms, measured together with STRATA_EMBED_VRAM and STRATA_DEQUANT_SIDE
(+2.2..+2.6 % prompts for the four PLE/embedding/dequant switches together).
Theirs, not measured here.
Original change by linxuhao (PR #3779, commit 0ba47e61).
Problem: the prompt path reads 16 PLE rows a token, dequantizes them on the
host and uploads 10,240 floats a token. On a slow host link (PCIe Gen2 x4) that
upload is on the critical path of the first layer.
Change, opt-in with STRATA_PLE_COMPACT=1: for an IQ4_NL table held in memory
(--ple-io ram / mmap) the prompt path copies the raw 90-byte rows (1,440 bytes
a token) into the same pinned buffer, uploads them and runs
ple_iq4nl_dequant on the device (d times the code's value, one multiply, the
split-half nibble order of iq4nl_dequant_row). Any other table (Direct I/O,
pageable fallback, another format) and a failed allocation take the host path
as before. With the variable unset nothing changes: the new code is behind one
cached flag, and the extra members of the Prefill state stay null.
Two things differ from the PR as submitted:
- PleTable::gather_raw_iq4nl writes a row past the table as scale 0 and code 8
in every nibble. The PR wrote zero bytes, which the device decodes to
-0.0f (0 times code 0's value -127), while the host path writes +0.0f for such
a row.
- New test ple_iq4nl_dequant_test (ctest, synthetic, no model, 77 without a
device): the device decode against iq4nl_dequant_row, bit for bit, over every
fp16 scale pattern with random codes, random rows, an odd row count and the
out-of-table row.
Reporter's numbers (linxuhao, CMP 170HX, PCIe Gen2 x4): prompts +1.0..+1.7 %
(4K, 16K) with the switch on. Theirs, not measured here. On a Gen4 x16 card
the gain will be smaller.
Metal gets a stub (the Metal prompt runs through its window and never calls
it).
Original change by linxuhao (PR #3779, commit da818261).
Problem: the prompt path stores H (swiglu of the gate/up rows) as floats and
then quantizes it to the q8_1 rows the down product reads: two kernels and an
H round trip per MMQ group.
Change: swiglu_quant_kernel, a block per row (128 threads, 4 values each,
blockIdx.y the row's 512-value half), computes swiglu_kernel's expression for
each value and quantize_mmq_q8_1<D4>'s result for each 32-value block, so hq
holds the same bytes and H is never stored. swiglu_quant_ok(type) says where
it applies (a q8_1 D4 down type, no FP4 activations). Built only in the CUDA
build with STRATA_EXPERIMENTAL_SM60; HIP and the other builds get
swiglu_quant_ok() == false and a no-op. Nothing calls it in this commit; the
weight-stationary path of the next commit does. prefill.cpp gets the matching
stubs for builds without the MMQ sources.
Tests: prefill_swiglu_quant_test (new, registered under STRATA_BUILD_TESTS in
the SM60 build, skips with 77 without a device) compares the fused kernel with
swiglu + quantize byte for byte for Q2_0, IQ4_NL and Q8_0 down types, split and
interleaved gate/up rows, a zero row and a row with one huge value.
Run on z440 (RTX A4000, sm_86, CUDA 13.0, SM60 build, gpu lock): all 18 cases print
"0 of N bytes differ" (1, 37 and 4113 rows) and the test ends "prefill_swiglu_quant_test:
ok". sm_60 compile of the tree (CUDA 12.9, P1004): exit 0.
This is a partial port of another contributor's prompt-path fusion change;
only the kernel is taken, as in PR 3752.
Problem: on a GA100 card (70 SMs) the 512-thread fused prompt-expert kernels
leave the card idle at every barrier and at every grid tail.
Change, all opt-in:
- STRATA_PF_FUSED_NT=256 sets up a second set of native_kernel instances with
256 threads and two blocks a multiprocessor (WW 2 only), with two-pass weight
loads (the loads of the next k step are issued before the current step's
decode; load_unit_l gives the same words as load_unit). Every output is the
same thread's sum in the same order.
- STRATA_PF_BATCH=N launches up to N experts at once (WideBatch holds 512 blob
pointers, 4 KB of kernel parameters: CUDA 12.1+, Volta and newer; HIP, gfx906
and the Pascal build keep kMaxBatch). Above 128 it needs the NT = 256 kernels.
Below 128 it only changes the launch grouping.
- With neither variable set the 512-thread kernels, their launches and their
load sequence are unchanged: tile_rows(ww, 512) equals the old 256 / ww, the
two-pass code is behind a constant-false TWO_PASS, and the one added test in
experts_native is occ_nt > 0, which only STRATA_PF_FUSED_NT=256 sets.
- If the 256-thread instances cannot be set up (shared memory, occupancy) the
engine prints one line and keeps the 512-thread kernels.
Cost: the NT = 256 instances are new device code in every CUDA build (the
reporter measured 365,632 bytes for sm_80).
Tests: new ctest entry prefill_fused_iq_test_nt256 runs prefill_fused_iq_test
(the fused kernels against the double-precision reference and the MMQ path)
with STRATA_PF_FUSED_NT=256; it exits 77 below sm_80.
Reporter's numbers (linxuhao, CMP 170HX): one MoE layer of 3,256 tokens
8.27 ms with NT = 256; prompts +7.6 % (4K), +4.9 % (16K), +4.3 % (52K) for
NT=256 with BATCH=512 on the PR's own build. Greedy answers identical on their
11 prompts. These are theirs.
Original change by linxuhao (PR #3779, commit 056d36a9). Merged onto the Q2_0
fused path of #3809 (STRATA_PF_FUSED_Q2N=1) without textual conflicts; the PR
predates it, so one line is mine: the NT = 256 setup also sets up the Q2_0
gate/up instance when Q2N is on (without it that instance would have launched
without its shared-memory attribute).
moe_shared_init and moe_scatter_add came with the layer-major walk and had no test.
lm_kernels_parity runs them on synthetic data of the model's width (N = 2560, no
model, a few MiB) against a host reference:
- moe_shared_init: acc = shared * sigmoid(sg), within 1e-5 relative (the device
sigmoid uses __expf);
- moe_scatter_add: the per-token fma chain in row order, bitwise equal to the host
chain and bitwise equal between two launches (no atomics), over groups of one
row, one sub-chunk, several sub-chunks, one row per token and eight per token.
Registered in CMakeLists.txt next to gr_upmix_parity (CUDA builds, not gfx906),
SKIP_RETURN_CODE 77 when no device is visible.
Not covered: the walk itself needs a model (see the 3976 gate scripts).
Problem: an sm_80 card took the shared interleaved-MMVQ row tables, which
were measured on other cards (#3252 reports A100 slowdowns).
Change: kIlRows_80 (2-4 columns), kIlRows58_80 (5-8 columns) and kIl1Rows_80
(one column), looked up under the key 80 only. They were read off
mmvq_il_parity --bench --emit-table with the same 3 % rule as the other tables.
STRATA_MMVQ_IL_SM80=0 maps an sm_80 card to the key 81, which has no table, so it
gets the shared table again (and no one-column table, as before).
A table only picks the rows a warp (0, 1, 2 or 4); 0 means native_mmvq's kernels.
mmvq_il_parity already checks every rows choice against native_mmvq bitwise for
every type and shape, T = 1..8, so any entry of the new tables is covered.
Reporter's numbers (linxuhao, one CMP 170HX): decode +2.7..+3.2 % over the shared
table, Qwen3.8-Flash-Next IQ3_S. The measured card is not an A100; the rows may
want re-measuring there.
Original change by linxuhao (PR #3779, commit eaedca84); the commit message here
is rewritten. The sm_89 table of #3453 is a separate change in the same list.
Problem: sm_80 (GA100: A100, A30, CMP 170HX) ran the per-group dequant kernels
that sm_61 and sm_86 had already left for the wide ones, because
dequant_wide_on() had no sm_80 entry.
Change: dequant_wide_on() answers yes for compute capability 8.0 as well. The
test is the raw (major, minor) of the device, so it is false on every other
card (sm_60, 70, 75, 86 keep their answer, sm_89, sm_90 and sm_120 stay off as
before). STRATA_DEQUANT_WIDE=0 still turns the wide kernels off.
Reporter's numbers (linxuhao, one CMP 170HX, GA100 sm_80 with 70 SMs): the GPU
time of a 3,259-token prompt went from 857 to 792 ms, prompts +6.3..+7.2 % at
3.3K tokens and +3.9 % at 52K. We have no sm_80 card; these are theirs.
Bit identity: the wide kernels are the ones sm_61 and sm_86 already run by
default. dequant_wide_parity compares them with the per-group kernels byte for
byte for FP16, BF16, FP32, the padded stride and a row slice. The kernels have
no sm_80-only code, so the result does not depend on the card; what only an sm_80
card can show is that ptxas for sm_80 keeps the same arithmetic (see the
checklist in phaseA-tests/gh027/gates/3779-sm80-checklist.md).
Original change by linxuhao (PR #3779, commit 48dc535e); the commit message
here is rewritten.
Problem: a request declares tools; the model writes a complete call for a name that is not among them (bash for shell, a dropped
prefix, an MCP name with every "__" written as "_"). Since #3075 such a call stays text, which is the rule we keep, but the client
saw an ordinary answer with finish_reason stop and nothing else, so the agent turn ended with raw XML and the model never learned
of its mistake. Reporters on OpenCode, Pithagoras, OpenClaw, LibreChat, Open WebUI and Pi hit it.
What changed (declared-only stays: nothing undeclared is ever returned as a call, finish_reason is unchanged):
- serve/frontend.py OutputParser: a declared name that contains "__" is also read when written with every "__" as "_"
(mcp_playwright_browser_navigate for mcp__playwright__browser_navigate); the call goes to the client under the declared name.
Same rules as the other aliases: only when unambiguous, and a tool declared under that spelling wins. A reordered or renamed
tool (server1_label_values for label_values_mcp_server1, bash for shell) is not guessed.
- serve/server.py: when a block went back as text for an undeclared name, the reply carries
"strata_rejected_tool_calls": [{"name": ..., "reason": "undeclared"}] (the names the parser already collected, up to five): at the
top level of a Chat Completions reply and in its last streamed chunk, and in the message_delta event and the message of
/v1/messages. It is absent when nothing was rejected, so every other reply is unchanged. The text itself is untouched.
- The server window line says the response carries the names; the count goes to totals["tool_calls_unread"] and to
/metrics/prometheus as strata:tool_calls_unread_total.
- docs/DETAILS.md: the alias, the marker, the counter.
Tests: serve/test_tool_name_alias.py (collapsed spelling at several chunk widths, a declared spelling and a plain tool unchanged,
a tool declared in the collapsed spelling wins, two tools with one collapsed spelling are not guessed, reordered/renamed names stay
text and are named), serve/test_server.py ForcedToolChoice (the field in Chat Completions whole and streamed and in /v1/messages,
absent for an ordinary answer and a declared call, the collapsed name is a call; the counter). The whole serve/ suite: 1055 tests,
two failures that also fail on the base tree (test_fatal_recovery on this PC, RestartWindow.test_not_alive_until_ready under load).
Not covered: returning an undeclared call as a real tool call (the owner keeps declared-only); a block for a DECLARED name that came
back as text because it was malformed (counted, no name to list); the /v1/messages field with a real client; a stderr or log-file copy
of the warning.
Problem: a decode window's QSA select runs one CTA per query for the top-k
(block_topk_wide_kernel past ~135K cells) and the warp-shuffle block scorer
(block_scores_multi_kernel), in each QSA layer. Both grow linearly with the
context: on one GP100 die at 300K cells a top-k call takes 0.39 ms with 1-5 SMs
busy and a scores call 0.22 ms (two queries). At short contexts the same work is
small and an extra kernel is pure overhead.
Change: qsa_select.cu (inside !__HIPCC__ && STRATA_EXPERIMENTAL_SM60) gets
block_topk_multi_* (six kernels over several CTAs per query, one per 2,048
blocks of capacity, at most 32, exchanging through caller scratch: no clusters,
no spin waits, capturable) and block_scores_dx_kernel (the multi scorer's
per-head sums built in registers as the same 32-leaf xor-butterfly tree, no
__shfl_xor_sync, the tail block stays block_scores_tail_kernel's). New public
qsa_topk_multi_scratch_ints, qsa_block_topk_long and qsa_block_scores_long
forward to qsa_block_topk / qsa_block_scores outside the build gate or their
conditions. verify.cpp keeps a second set of window graphs (exec_[sel_long_][T])
for windows that reach STRATA_SELECT_LONG_AT cells (default 65536); the choice
is made before a capture and handed to the next stage of an overlapped layer
split, capture_all captures both, pl_launch picks by the window's position.
The ids and the score bits are the replaced kernels'. STRATA_TOPK_MULTI=0 and
STRATA_SCORES_DX=0 turn either half off inside the long graphs.
Review changes against the PR: in a build without the multi-CTA kernels
STRATA_SELECT_LONG=1 now says so and is ignored (the PR would have kept two
sets of graphs for nothing there); Metal gets stubs for the three new calls
(the SYCL tree has its own verify.cpp and qsa_select copy).
Default path: unset, select_long_on() is a cached false, topk_scratch_ is not
allocated, sel_long_ stays 0, and every window uses exec_[0][T], the graph it
used before; the index change is the only difference in verify.cpp.
Merge note for f-027-gh-eng (#3702 evict_window_graph, not in this branch's
base): its exec_ / exec_nr_ / launched_ accesses need the same [sel_long_]
first index.
Tests: src/kernels/qsa_select_long_parity.cpp (new; registered with
STRATA_SCORES_DX=1 STRATA_TOPK_MULTI=1 in the SM60 test block, and a second
case with STRATA_TOPK_MULTI_N=3; skips with 77 without the kernels) compares
qsa_block_scores_long with qsa_block_scores (nq 1-8, n_bid 8 .. 131072, tail
blocks of 0-3 cells, every block 0..n_bid memcmp) and qsa_block_topk_long with
qsa_block_topk (nq 1-16, contexts 1,000 .. 524,288, random scores, all-equal
scores and 7 distinct values for ties across CTAs, selected ids of every query;
the multi scratch is junk-filled and must have been written). z440 (RTX A4000,
sm_86, CUDA 13.0, gpu lock, build with -DSTRATA_EXPERIMENTAL_SM60=ON): "172
cases, 0 differ -> PASS" with one CTA per 2,048 blocks and with N=3. sm_60
compile of the whole tree (CUDA 12.9, P1004): exit 0.
Not covered: no GP100 run here (the kernels' last word is the P100, and the
speed claim). The contributor's numbers (four GP100 dies, every expert resident,
--pipeline-windows 3: 400 tokens at 115K context 51.0 -> 52.5 tok/s, +2.9%;
prompts below 64K unchanged; replies and state hashes equal on 15 requests) were
not re-measured. The model gate is phaseA-tests/gh027/gates/3820-select-long.sh.
Everything the Chat view shows (header, tabs, message box, settings, toasts,
tool blocks, the sidebar, the history and Load/Unload texts) now goes through the
translation catalog: serve/web/locales/{de,es,ja,ko,pt,ru,zh}.json beside en.json
(written from the English text in the code by tools/web_i18n.py). Plurals follow
the language (Russian has few and many, Japanese and Korean only one form).
serve/test_web_i18n.py fails when a catalog lacks a key, has a stale one or
changes a {placeholder}. A ninth language is one file plus one line in
serve/web/i18n.js. The Monitor, Images and About views are still English (the
language setting says so); they are the next step.
Browser tests: a language pick translates and is kept, /?lang= is for one visit,
an unknown code stays English, all seven catalogs load without an error.
docs/DETAILS.md: a section "The web page" (every switch, what the preview can and
cannot reach, the languages, the tests), the Sampling drawer's new name, and the
credits (KaTeX and the contributors of the PRs ported into the page).
Five overlapping proposals, one set of controls (Chat settings > Chats, all off
by default; off, the page is the page from before: one chat, New chat clears it
with an Undo, Send is off while an answer is written, no new request field).
- Saved chats list (#1787): the chats kept in this browser (IndexedDB, one record
per chat, names of attachments only), a list with day groups, search over
titles and text, rename, delete with Undo, collapse; a private window or full
store keeps the list in memory and says so once. Nothing goes to the server.
The list has the New chat button, so the composer's one is hidden while it is on.
- Edit and rewind messages (#2615 + #3823): under each message, Regenerate (a new
answer to the same question), Edit answer (its text only: thinking and tool
calls go, no request) and Rewind to here. Rewind asks, as #3823 proposes: "Edit
and send again here" hides this message and the ones after it, puts the
question and its attachments in the box and keeps one Undo bar ("2 messages
hidden"); "Branch from here" starts a new saved chat with the messages before
it and leaves this one alone. A branch keeps a title that says where it came
from ("Branch of ...", also at the very first message) and links back. One
control does what #2615 had as Edit on a question and as Remove from here.
- Send while answering (#4004, for the web page): "Wait" (default, as before),
"Send after" (the message waits and goes when the answer is done) or
"Interrupt" (the answer stops where it is, what is written stays and is marked
interrupted, the message goes at once; a running tool finishes first). One
message waits; Take back returns it to the box; Stop keeps it for you to send.
The pure logic is serve/web/history.js, the store serve/web/chatstore.js, the
page's part serve/web/chats-ui.js. Tests: serve/test_web_history.js (Node, with
the IndexedDB stub and cases of #1787), serve/test_web_history.py, and 17 browser
tests (tools/webtest/browser_test.mjs). Screenshots: tools/webtest/shots.mjs.
Also: the Sampling drawer is now Chat settings (its button says so), a wide
window lets a message use the column (#3176 widened only the column).
Ported by hand and merged: #1787 by Scotty Hudson, #2615 by Joyer Huang (uppet),
issues #3823 and #4004 by xvcezrqpbxtpzblrt (their proposals, for the desktop app,
carried to the web page).
Co-authored-by: Scotty Hudson <[email protected]>
Co-authored-by: Joyer Huang <[email protected]>
Co-authored-by: xvcezrqpbxtpzblrt <[email protected]>
The layer-major walk is a copy of run_impl's attention half with the experts moved
out. It does not carry four things run_impl does, and when one of them is asked for
the walk now declines (the prompt is read chunk-major, and the one-time "layer-major
off for this prompt" line names the reason):
- the DFlash2 drafter's prompt taps (tap_layers_): the walk would leave the taps
stale and the drafter would read old rows;
- STRATA_KVA (approximate late layers): the walk would silently read exactly;
- a stop_at_chunk callback (the pipelined batch reader that yields at chunk
boundaries): the walk reads every token, and stopped_at() must stay -1;
- a pending force_chunk (a --prefill-pipe plan): it is left set for run_impl.
The walk also resets stopped_at_ like run_impl does at the start of a run.
With STRATA_PREFILL_LAYER_MAJOR unset none of this is reached.
Not covered: no model run in this commit.
Only NVIDIA CUDA has run the layer-major walk. On a HIP build (wave32 or the
gfx906 compat layer) lm_prepare now declines with a reason, so a prompt takes the
chunk-major path and the one-time "layer-major off for this prompt" line names it.
The code still compiles everywhere the shared prefill.cpp does; nothing else
changes. With STRATA_PREFILL_LAYER_MAJOR unset nothing is reached.
Not covered: HIP and SYCL were not built or run (SYCL has its own prefill.cpp).
run_impl never clears a later stage's hand_in_, so after one short prompt (read
chunk-major) every later prompt was declined with "a layer split's chunk hand-off
is in use". lm_prepare_chain now declines only when a hand-off is really in flight
(a stage's next_run_ is valid) and otherwise clears the stale hand-in pointers.
A prompt longer than the common super-chunk is split into the same number of
super-chunks of near-equal size (32K at a 28K fit: 2 x 16K, not 28K + 4K). That
keeps a stage's residual rows in VRAM and lets a split's stages overlap on every
super-chunk. STRATA_PREFILL_LM_EVEN=0 gives the old largest-first split. It only
acts when STRATA_PREFILL_LAYER_MAJOR is set.
The split was declined whenever the model has PLE layers. The PLE block (layer 1)
reads the token history, which only the first stage has, and a later stage never
reaches it. So the split is declined only when the first stage does not hold both
layer 0 and layer 1.
Problem: layer-major prefill declined every layer split, so a split model kept
re-reading each layer's streamed experts once per 8192-token chunk.
Changed:
- lm_prepare_chain sizes every stage with the same super-chunk (the smallest any
stage fits, up to four rounds until they agree) and gives them back together;
- a stage runs only its layers [LB, LE); a later stage's rows arrive in its pinned
R_host;
- a stage before the last writes its final rows into the next stage's R_host on
the copy stream, then runs the next stage on a thread (std::async) while it reads
its own next super-chunk. There is one hand-off buffer: a stage's final pass waits
until the next stage has finished the previous super-chunk;
- layer_major_mid() asks the last stage, the one that reports the rows;
- run() drains the pipeline after the walk and clears the mid flag on every stage.
Not covered: only two stages were tried by the reporter (RTX 3060 + RTX 5060 Ti);
the peer-GPU tier and the stage helper have no layer-major form and decline.
run_layer_major called mmq::iota on ids_identity, which is null when no layer is on
MMQ; it now skips that. The QSA shapes come from core::qsa_shapes(g) as run_impl
does, instead of being filled in by hand.
lm_prepare required ids_identity and tok_dev even when no layer is on MMQ. Only a
model with an MMQ layer needs the identity table, so the check now asks for it only
then. Without this a model whose layers all take the FP16 path was declined.
Problem: lm_prepare declined every model whose experts are not all on MMQ. All 48
layers of UD-Q4_K_XL take run_impl's FP16 path, so layer-major never ran on them.
Changed: a layer outside the MMQ plan dequantizes each routed expert to FP16 once
per super-chunk and runs two FP16 GEMMs per entry on that entry's FP16 input rows
(a new per-super-chunk buffer mixed_h, the bits run_impl's gather reads), then
adds the group's rows per token with moe_scatter_add as the MMQ path does. The
buffers each path needs are checked separately, and a missing one gives a decline
reason that names it. The summary line counts the groups on the FP16 path.
Not covered: no model run in this commit.
Problem: the layer-major walk (#1670) was written for an older base. It read the
hyper-connection matrices as bf16 only, accepted expert formats its MMQ pass does
not carry, and called the QSA indexer with the older argument lists.
Changed (Norhiro's port of vizdatom's walk, kept as one step):
- the HC projections are dequantized per half when STRATA_HC_Q8 holds them as Q8_0
with no bf16 copy, and released after the GEMMs that read them are enqueued;
- lm_prepare declines (the prompt then runs chunk-major) for a layer that needs the
WMMA tiles, an APR (Hadamard-folded) layer, and an NVFP4 layer;
- STRATA_PREFILL_DIRECT is not used by the walk.
Not covered: no model run in this commit.
A super-chunk of S tokens is read layer by layer: each 512-token sub-chunk runs the attention half,
the FFN half hyper-connection read, the router and the shared expert in order (GDN / QSA / PLE state
advance as in the default path); then the layer experts run once for the whole super-chunk - each
routed expert staged once, its rows of every sub-chunk in one MMQ group, the results added per token.
Expert traffic drops by about S / 512 (a 12.9K prompt: ~509 GB -> ~79 GB on ryzen).
- STRATA_PREFILL_LM_TOKENS: the super-chunk target (default 8192), shrunk to fit free VRAM minus
STRATA_PREFILL_LM_MARGIN_MIB (default 96).
- STRATA_PREFILL_LM_R=vram|host|auto: the residual rows of the super-chunk in VRAM, or in pinned RAM
with the copies overlapped on the copy stream (auto: VRAM only when the whole target fits there).
- STRATA_LM_PREFETCH=0: no prefetch (default on: every streamed expert of a layer goes to the stager
when its attention halves start; the ones the routing skips are skipped if not started).
- Not for a layer split, a peer GPU, image prompts, the fused layout or a prompt under two chunks.
- on_chunk still reports every sub-chunk in order; layer_major_mid() tells serve not to checkpoint
before a super-chunk end (the state is already at that end).
Port to 0.1.43.1: the copied run_impl body follows the new base. Every blob the stager or the copy stream reads later
comes from blob_stable() (blob() may hand out a recycled STRATA_IO_PREFETCH buffer); the token ids go up through tok_up,
the QSA step records and the PLE rows' pageable fallback through up_ring (#1712, #1996); the PLE gather threads pin to
--aux-cpus; bf16x2 takes m.f16_io; the K8V4 append is upstream's, which writes the host copy and the staging pool by
halves. The indexer passes the INT8 pooled keys as the original does, in the QSA port's order (pooled_i8 and
pooled_scale after block_pos, the int8 qsa_block_scores(_tc) overloads); STRATA_IDX_INT8 stays opt-in and unsupported on
the prompt path, as in the original. stage_one calls the stager's pass() before wait(): with batched claims a routed job
after about a ring's depth of skipped prefetch jobs waited forever. lm_prepare reads the fused layout from m.fused_bufs
and declines while STRATA_DBG_FEAT, STRATA_DBG_ROUTE_OVERRIDE or STRATA_PREFILL_DUMP_R_ALL is set. serve's near-tail
checkpoint (--prompt-cache-tail) is not taken mid-super-chunk, so under layer-major it is taken only when the last
super-chunk is a single sub-chunk.
From vizdatom's #1670, original commit .
moe_shared_init writes the shared expert part of moe_combine before the routed experts are known;
moe_scatter_add adds one expert group rows into a per-token accumulator, per token in a fixed order
(deterministic, no atomics).
From vizdatom's #1670, original commit f0173382d0646907917edf99e59617c1412d898e.
Chunks below stream_all_min() stage each layer's unpinned experts on the stager's few threads, and with a mapped
file tier each of those is a chain of 128 KiB page faults, a shallow queue for the drive. With STRATA_PREFILL_WARM=1
(and a source that warms: the GGUF in place, or experts.bin with STRATA_BIN_WARM=1) a background thread hands the
source the layer's whole list at once before the stager starts, and the next layer's list from the previous chunk
(less what is on the GPU or pinned) while this layer computes. It only warms pages: the stager copies the same
bytes. At most four lists queued; a thread that falls behind drops the oldest. Off by default: the A/B arm.
Port to 0.1.43.1: the warmer thread pins itself with aux_cpus::pin_current_thread() first, like the other helper threads since --aux-cpus (it would otherwise inherit the host thread's CPU).
From vizdatom's #1670, original commit cabe33c4f585d4c9dd0af1fc9f014d00e2920042.
issued is a watermark, so a job nobody waits for could be passed while its thread was still writing
the pinned buffer the next job on that buffer was filling - a corrupt expert blob. A job now also
waits until the job kRing before it has marked itself ready. skip(j) lets a job the routing turned
out not to need be marked ready without reading it (the layer-major prefetch). The default walk
consumes every job in order, so neither changes it.
Port to 0.1.43.1: the stager now claims batched runs and sleeps in issued.wait, so the ready[i - kRing] wait
sits in the run's per-job wait loop after the issued wait, and a skipped job is left out of the run's gather
(no memcpy, prefill_copy or copy_blobs entry); its ready flag and `active` stay with the run's single store and
decrement. New pass(j): skipped jobs are never issued, so the layer-major walk moves the watermark to the
routed job it is about to wait for (store plus notify_all, which wakes sleeping threads); without it a routed
job after about a ring's depth of skipped ones waited forever. The default walks never call it.
From vizdatom's #1670, original commit 149b899548fbc98977219e6fd4ee7866384e61f6.
One button in the header of every tab: Load while the model is unloaded, Unload
otherwise. It calls POST /load and POST /unload (the server already had both,
for its own page only). Unload asks first and never interrupts a turn: the
button is off while an answer is being written, an image is made or a request is
queued. The pill and the Monitor say Unloaded / Loading / Unloading, a message or
an image on an unloaded model shows Loading, and a poll that started before the
POST cannot flip the button back (the POST's state is held until a later poll
agrees). On a phone the pill's text is the first thing the header row gives up
(it measures the row, not a fixed width), so Load/Unload and the theme button
stay on the screen from 320 px.
Browser tests (tools/webtest/browser_test.mjs): unload with its question, load
back, declining, not while answering, a message that loads, the header at 320 to
640 px.
Ported by hand from PR #3550 by Sakurai Momoka (sakuumomo); the texts now go
through the page's translation catalog.
Co-authored-by: Sakurai Momoka <[email protected]>
A complete ```svg or ```html block in an answer gets a Preview button. The
block's own code is drawn in an iframe (srcdoc) that is cut off from the page:
sandbox="" (an opaque origin: no access to the page's DOM, storage, cookies or
server, no scripts, forms, popups, downloads or navigation of the page), a CSP
in the frame's document (default-src 'none': no network, fonts, frames or media;
styles inline, pictures as data: only; no <base>, no form target), no referrer
and no permissions. A well-formed svg is shown as an <img> with a data: URL: an
image runs no script, loads nothing and animates in every browser. The frame
takes the page's light or dark colours and is built again when the theme
changes. A page (html) can also be run WITH scripts, one block at a time, after
a warning that is shown every time: sandbox="allow-scripts" only (still an
opaque origin), the CSP allows the block's inline script and no address; closing
the preview ends it.
What the sandbox flags alone do not stop is a link or <meta refresh> inside the
model's html loading a web page into the frame. The browser test shows it does
(tools/webtest/browser_test.mjs, with and without the header). So the page is
served with Content-Security-Policy: frame-src 'none' (nothing else: a srcdoc
frame inherits the page's policy, and the script preview needs its inline
script), and a <meta http-equiv=refresh> in a previewed page is dropped.
tools/webtest holds the mock server (the page's own files, scripted answers,
no model) and the browser tests; serve/test_web_preview.js checks the documents,
flags and frames, serve/test_web_preview.py the header and that the page never
listens for a frame's postMessage.
Ported by hand from PR #1628 by thoives.
Co-authored-by: Thomas Ivesdal-Tronstad <[email protected]>
A PC whose CPU lacks the AVX-512 set that setup checks for gets the native Q2_0 pack. On a CUDA card of sm_80 or newer its
Q2_0 / Q2_0 prompt experts run through MMQ, although the fused int8 kernel for that format (#2145) is in the binary and set
up only on a Turing card (b6443f3ae).
STRATA_PF_FUSED_Q2N=1 sets that kernel up on any sm_80+ card and takes it for the Q2_0 / Q2_0 layers, under its own variable
so that STRATA_PF_FUSED=1 keeps its bits. STRATA_PF_FUSED=0 and STRATA_PF_FUSED_NATIVE=0 still turn it off. A failed setup
leaves the occupancy as it was and the layers on MMQ.
- moe_fused_iq.cu: DevInfo.q2n; dev_info() sets up Q2_0 gate/up when the switch is set; gu_covered() and native_supported()
take the pair only when q2n is set (a Q2_0 gate with another down type is refused, as on Turing).
- tests/cuda/prefill_fused_iq_test.cpp: --unset-pf-fused leaves STRATA_PF_FUSED unset (the engine's default) and skips below
sm_80; CMakeLists.txt: the ctest entry prefill_fused_iq_test_q2n runs the Q2_0 / Q2_0 pair with the switch alone (exit 77 =
skipped without an sm_80+ card; CUDA branch only).
- docs/DETAILS.md: one section in the author's measured numbers (RTX 5080) and the A4000 caveat below.
Unset (the default), nothing changes: dev_info() reads the variable once and sets nothing up, gu_covered() and
native_supported() return what they returned. Read line by line against base: the Turing path (t75) and the HIP path are
untouched. Set, the prompt arithmetic changes (first-token KL against FP16 0.0110, MMQ 0.0112, the author's figure) and the
fused path's smaller buffers let --prefill auto pick a larger chunk, which is most of the author's 16K gain; the generated
text can change.
Not verified: an RTX A4000 (sm_86). The kernel was taken on every CUDA card in #2145 and then kept to Turing after an A4000
no longer matched the old output and read 4K prompts 4.5% slower; the PR does not repeat that run. Merge note: #3277 and #3779
change dev_info() and native_supported() in the same file.
Tests: prefill_fused_iq_test_q2n and prefill_fused_iq_test pass on an RTX A4000 (sm_86, synthetic data); on the Q2_0 / Q2_0 layer the
fused kernel is 1.76x / 1.30x / 1.24x the MMQ path at 2048 / 4864 / 8192 tokens with the same relative RMS against FP32. Builds on CUDA
sm_86 (Linux) and sm_120 (MSVC). Not covered: no model run; the engine-level A4000 check is the gate.
Chat settings > Render math (off by default). When it is on, \( \), \[ \], $x$
and $$ $$ in an answer are drawn as formulas by KaTeX 0.19.0, which is vendored
in serve/web/vendor/katex (script, style, 20 woff2 fonts, MIT licence and a
PROVENANCE.json with the npm integrity and the sha256 of every file, so the page
needs no network). Nothing of KaTeX loads until an answer holds a formula.
The shared core (math.js, from #2972) keeps the text of every message exactly as
the model wrote it: formulas are swapped for placeholders before the Markdown
runs and drawn over them afterwards, so history, copying, the Markdown export
and the requests never see the drawing. Code, link targets, prices ($5 and $10)
and a formula still being streamed stay text. A formula that fails, is over
16 KiB or times out stays as its source text.
Safety: KaTeX runs with trust off (no \href, \url, \includegraphics, \htmlClass:
they are drawn as their own name), maxExpand 1000, maxSize 20 and a fresh macro
table per formula; it builds DOM nodes itself, so no HTML string from a model
reaches the page. A formula already drawn is placed again at once while an
answer streams, so it does not flicker back to source.
serve/test_web_math.js runs the real vendored KaTeX against hostile formulas;
serve/test_web_math.py checks the vendored files against PROVENANCE.json and
that nothing outside this server is addressed. serve/web/vendor is -text in
.gitattributes so the hashes hold on a Windows checkout.
Ported by hand from PRs #2972 (core) and #2974 (KaTeX) by CC-David-CC.
Co-authored-by: David <[email protected]>
The served page had no way to translate its text: every string was English in
the HTML and the script. Now the English text stays in the code next to a key
(tr("key", "text", vars), data-i18n="key" in the HTML), a language is a catalog
serve/web/locales/<lang>.json, and a catalog that is missing or has no entry for
a key leaves the English text. The language is the user's choice (About >
Settings), kept in this browser; nothing is chosen from the browser's language,
because a page translated in part would mix two languages. /?lang=de shows one
for a visit.
The server hands out /web/locales/<lang>.json and /web/vendor/katex/** from
serve/webfiles.py: whitelisted names and types, no backslash, colon, percent or
dot-dot, and the resolved path must stay inside the folder (also against a link
out of it). tools/web_i18n.py reads the English text out of the code, writes
en.json and checks every catalog for the same keys and {placeholders}; the
check runs once the catalogs exist.
Only the part of PR #3311 by gopinath87607 that is about this engine: a periodic mark is saved at a chunk boundary of the batched prefill and the read carries on
(src/program/generate.cpp, periodic_checkpoint), so the interval is not a piece-size dial; what cuts a read into runs is the re-read, the root, a message
boundary and the start of the last turn; --prompt-cache-every 0 skips the periodic and the near-tail marks. The wall-time numbers and the comparison with the Maya
checkout belong to another engine and are left out. New section in docs/PROMPT_CACHE_TAIL.md, linked from the option's row in docs/DETAILS.md.
--show-key only knew a running launch or a preset. A model set up by setup.py (START-HERE.bat, ./setup.sh) keeps its generated key in strata-<model>.json, so the command
said there was no key and people read the JSON by hand. With no launch and no --preset, it now looks at the run configs of the installed models: one model with a key
prints that key (stdout is the key alone; a hint line with the model and the file goes to stderr), several models ask for --model NAME (any case, the size or the full
id), a name that matches nothing says so. --preset never falls back to a setup config, and no key is printed while the choice is open. Tests:
tests/terminal/test_launch_show_key_setup.py.
A volume or bind mount from the 0.1.x image keeps its models in /data/models. The entry point moves a folder that is part of the volume, but a folder the person
mounted there is left alone, and a failed move leaves the files too; with no MODEL_PATH the start then did not see them and downloaded the model again
(tens of GB) without a word. Now, when .gguf files are still in /data/models and MODEL_PATH is not set, the container stops with code 2 and names the folder,
says what to do (MODEL_PATH to the folder with the shards, or the files into /data/Strata-data/models) and that nothing was downloaded.
ALLOW_DOWNLOAD=1 goes on anyway with the same text as a warning. docs/DOCKER.md lists the setting. The other points of the issue (the key, the removed
variables, the status loop) are the owner's design questions and are not changed here. Tested by running the guard function with sh (tests/terminal/test_docker_files.py);
the whole container was not run (no docker on this PC).
Prefill on one card, decode on another, the session handed over in RAM through the engine's
own session file (SAVE/RESTORE). The engine's new request form GEN 0 prefill=1 reads a prompt
as a request does, up to where the first verify window starts, and stops with the session at
n - 1 tokens, which is the place a continuing request resumes from. Opt-in; without
split_serve nothing changes.
Idea and manual setup by @MarcoTool in #4000.
Problem: the strata-dsv41 packages were built for sm_86 and sm_120 (+ PTX 120). An RTX 4090 user was stopped at setup on 0.2.4.1; since
b750d757f an sm_86 package is accepted on an sm_89 card (cubin same-major rule), but the engine had no sm_89 image of its own, and the docs said
nothing about RTX 40 or about building the engine for a card the packages do not cover. The reporter built strata-dsv41 for 86-real;89-real
themselves and ran the 2.00 set at 40.2 tok/s on an RTX 4090 48 GB (theirs, --dspark on).
What changed:
- packaging/build_dsv41_cuda.sh and build_dsv41_windows.bat: the default architectures are now 86-real;89-real;120, so the DeepSeek CUDA 13
packages list sm_86, sm_89, sm_120, compute_120 (Windows and Linux alike). The CUDA 12.9 line in the script header says ARCH="86-real;89".
- CMakeLists.txt: a build of strata-dsv41 with no -DCMAKE_CUDA_ARCHITECTURES defaults to 86-real;89-real;120 (the Qwen engine's default
stays 120, and the warning now names the architectures it picked).
- strata_control/engine_component.py arch_native(): true when a package holds machine code for the card itself. resolve() and
feed.select_engine() use it as the last tie-breaker, so with otherwise equal packages the one with the sm_89 image is taken on an RTX 40;
an sm_86-only package still fits (no refusal).
- setup.py: the "engine not available" stop now points at the build recipe (docs/DEEPSEEK41.md); the developer line names the
architecture to pass. The card list already said RTX 30, 40 and 50.
- docs/DEEPSEEK41.md, docs/MODELS.md: RTX 40 coverage, the reporter's measurement quoted as theirs, no claim of our own (we have no Ada card),
and a section "Building the engine for your card".
Tests: tests/terminal/test_engine_manifest.py (arch_native; the build scripts and the CMake default name sm_89 and both OSes agree; the
package list they give is sm_86 sm_89 sm_120 compute_120; an sm_89 card is accepted and prefers the package with sm_89). tools/test_setup_dsv41.py
unchanged and passing.
Not covered: no run on an Ada card (gate script in phaseA-tests/gh027/gates/3935-ada-dsv41.sh).
One bullet from PR #3930 by NorwinYu: on two identical RTX 2080 Ti over a dual-socket Xeon E5-2696 v4 a plain split moved decode from 35.2 to 51.0 tok/s and an
11.6K prompt from 672 to 908 tok/s, because the CPU pool per window fell from 9.52 to 0.79 ms (STRATA_DECODE_TIMING=1). The page's own 5080 + 3090
figures, where the second card is on par, came from a fast CPU; the bullet says so and that it is one measurement. No code changes: setup already splits
across two cards. Shipped without a hardware run of ours; the reply asks owners of two older cards on slow CPUs to compare and report.
The chat, About and Monitor columns grow with the window (the chat and the
composer to 1100 px, About to 1100 px, Monitor to 1920 px), the metric cards
wrap with auto-fit, and under 640 / 560 / 480 px the controls get bigger
touch targets, the composer bar wraps, and long words, code and tables stay
inside the viewport.
Ported by hand from PR #3176 by Matthias Nott (mnott); the CSS is his.
Two paragraphs from PR #3895 by Vop3p, put under the table row they explain: the fork pays only when it has a CPU-pool wait to hide (4,859 slots on one
RX 6900 XT, GPU-reach wait 20.7 of 39.1 ms; with every expert resident it is 43.4 against 62.9 tok/s on an R9700), and the Windows RX 6900 XT fault with
the fork plus --kv-resident, its bisection and its limits (one PC, kernel not isolated). The text now says what the code does about it and asks RX 6000 owners on Windows to test.
On one Windows RX 6900 XT STRATA_SH_STREAM=1 together with --kv-resident failed the engine on its first request (verify: unspecified launch failure).
setup --calibrate (tools/calibrate.py) no longer measures the fork on Windows when KV streaming is on (the fork is off by default on AMD, so
nothing changes unless the person sets it), strata tune's never-pick list holds the combination (strata_control/tune_knobs.py), and a config that
sets STRATA_SH_STREAM=1 with --kv-resident on Windows HIP gets one warning line when the engine starts (serve/server.py sh_stream_warning). Nothing is refused or
removed: the variable still reaches the engine. Shipped without a hardware run, from one report; the reply to #3895 asks RX 6000 owners on Windows to test
and report. Tests: tests/terminal/test_sh_stream_windows.py.
The 0.2.5 rewrite dropped about 25 entries: the Mac quarantine steps, strata doctor, sending logs, the first-start freeze, the graphics memory the app keeps free,
Smart App Control, UPDATE.bat and git pull (#1276), the build tools and the gcc/glibc compile problems, the engine checksum, the engine stopped unexpectedly,
slower than the tables, the empty-reply and Turing switches, --expert-cache and --resident-budget-gib, the page-locked OOM, the NVIDIA 0x141 reset, CUDA 13.2 on
RTX 50, the AMD section (KFD, the build list, 0xC0000005, the driver reset, pinned allocations, verify timeouts) and the body of the verify-timeout entry, which
the rewrite had left empty. Each is back with the 0.2.7 facts: the app's reserve defaults (native-resources.mjs), the Drafting setting, --vision on|off|gpu|cpu
in the Terminal and --vision no in a source checkout, Intel Arc's CPU reader, and the Terminal route next to the source-checkout one.
Every pointer resolves: setup.py's driver-reset pointer, the server's Smart App Control message, DETAILS.md's STRATA_KEEP_EMPTY_TURNS / STRATA_TOPK_STREAM line,
OLDER_GPUS.md and MAC.md. tests/terminal/test_troubleshooting_page.py checks the entries, those pointers and the page's own links and anchors.
Problem: below sm_70 the prompt reads each QSA layer's attention through the
decode kernels (qsa_prompt_attn_batch returns false without tensor cores): 32
queries a call, every chunk's partial results (817 KB a query) written to
scratch and merged by a second kernel.
Change: src/kernels/cuda/qsa_pattn.cu keeps the contributor's default variant
only, ps_score_x_kernel (q in registers, the chunk's K staged with 16-byte
loads, two cells at a time through a select-free reduce-scatter) and
ps_value_d4_kernel (four dims per thread, merged into the output as the merge
kernel does). Every float operation of attn_chunk_kernel_pre75 and
attn_merge_kernel is kept in its order, so the output is the decode kernels'
bit for bit. The PR's other variants (per-chunk, fused persistent, bench split
codes), pattn_bench and the variant argument are not ported; pattn_bench needs
a STRATA_QSA_DUMP file from a model run.
Gating: compiled only in the CUDA build with STRATA_EXPERIMENTAL_SM60 (HIP and
Metal link a stub that answers false; prefill.cpp keeps the call out of the
Metal build). At run time only with STRATA_PATTN_SM60=1, on compute capability
6.x, int8 K/V, head_dim 256, 12 query heads per KV head, at most 33 chunks.
Unset, the only cost on the default path is one cached int test per QSA layer,
and only when qsa_prompt_attn_batch has already declined. Any refusal or a
failed launch returns false and the usual 32-query loop runs (one warning, no
exit; the PR called exit(1) on a launch failure).
Tests: src/kernels/qsa_pattn_parity.cpp (new, registered in CMakeLists.txt in
the experimental sm_60 test block, skips with 77 when the kernels are absent)
compares the kernels with the Daily's chunk kernel plus attn_merge_kernel on
random int8 K/V: a shuffled page table with ~3% unresident pages and a run of
64 unresident pages (whole chunks masked, some queries empty), three selection
shapes, caps 64 / 130 / 2051 / 2112, widths 0..cap, n_q up to 40 with a
scratch that holds only 2 decode-layout queries (several batches), junk in the
scratch and output. On a Pascal card it also requires the checked entry to take
the shapes and qsa_decode_attn_batch to match. The kernel entry that skips the
compute-capability test (qsa_pattn_batch_any_arch) exists for this test only.
Results: z440 (RTX A4000, sm_86, CUDA 13.0, build with
-DSTRATA_EXPERIMENTAL_SM60=ON, gpu lock): "qsa_pattn_parity: 468 cases, 0
differ, 29724792 nonzero output values in all -> PASS". sm_60 compile of the
whole tree (CUDA 12.9, P1004): exit 0.
Not covered: no P100 run here (the Pascal-only checks of the test and the
speed claims wait for the gate, phaseA-tests/gh027/gates/3943-pattn.sh). The
contributor's numbers (43.3 -> 30.8 ms a 1,536-query chunk per QSA layer at
26K context, 2-3% less time on 8K-150K token prompts on four dies) were not
re-measured; compute capability 6.1 is covered by the same switch and was not
measured by anyone.
The Speed group gets a row for Qwen on a discrete CUDA card of 13 GiB or less: "Prefers the parts of the model already on the graphics card, which makes answers
much faster on 12 GB cards. Turn off if answers look worse than before." On (recommended) by default; Off writes STRATA_ROUTE_PRIOR=0, On removes it. The
DeepSeek speed-ups row, when Off, writes DS4_DEFAULTS=026 and the six DS4_*=0; On removes all of them, and a file that holds only the six (an earlier test
build) shows as Off. The Qwen "0.2.6 behaviour" switch is Terminal-only and is not in the app. New strings fx82 and fx83 in the eight fix catalogs.
- DeepSeek speed-ups Off now writes DS4_DEFAULTS=026 (the engine's master switch back to the 0.2.6 defaults) plus the six DS4_*=0 for older engines; On removes
all seven. A file with only the six (an earlier test build) still reads as Off and On removes them (`legacy` entries).
- New switch "Faster answers on 12 GB NVIDIA cards" (the residency-biased routing default): on by default, Off writes STRATA_ROUTE_PRIOR=0. Applies to Qwen
Flash-Next (no arch) on a CUDA build with a discrete NVIDIA card of 13 GiB or less (not a chip that shares system memory), the engine's own rule.
- New Terminal-only switch "Qwen: 0.2.6 behaviour" (STRATA_DEFAULTS=026, off by default), a row of the Advanced table for every Qwen model, for
troubleshooting; the app never shows it. DeepSeek has DS4_DEFAULTS instead.
tests/terminal/test_speed_switches.py covers the new defaults, applicability, the seven DeepSeek entries and the legacy six, the Advanced rows and the
comparison with the app's table (the Terminal-only switch is left out of it).
/v1/chat/completions on the CUDA and HIP engine accepted logprobs and
returned an empty choices[0].logprobs.content. Now "logprobs": true with
"top_logprobs" 0 to 20 (and the legacy "logprobs": N) returns one entry
per token with its alternatives, streamed or not, and "allowed_token_ids"
returns a distribution renormalised over exactly the ids the caller named.
Engine (src/program/generate.cpp): the GEN line takes logprobs=K and
allowed=a,b,c. After each "T id" the engine writes one "LP <logprob>
[id:logprob ...]" line from the verify window's head logits before
sampling, penalties and temperature. The SYCL engine already wrote LP
lines for logprobs=K; it now reads allowed= as well, with the same line
format. A request without logprobs reaches none of this: both call sites
are behind "req_logprobs >= 0", a local int of the request, so the default
path prints what it printed before and does no extra work per token.
Changes from the PR: the call sites are guarded instead of calling the
function for every token, the function is static, the token index is
checked against the row, and the Chinese comment is in English.
Server (serve/server.py): LP lines are read before the token is yielded
and belong to the request that asked (tagged with its cancel Event), the
entries spell the reply, json_schema replies return no logprobs rather
than a mismatching set, an allowed id past the vocabulary is a 400, and
logprobs on a --parallel server is answered for max_tokens 1 only (a
slot's BT lines carry no LP line). serve/structured.py: _extract_json is
now extract_json (the logprobs alignment uses it); the two uses added by
the #2893 retry follow the rename.
Left out: the PR's bench and result files (none in the diff). #3332 (/v1/completions) and #4056
(logprobs on it) build on this change; this one needs nothing from them.
Tests: serve.test_logprobs and serve.test_logprobs_parallel (30 tests with
the PR's), serve.test_structured, serve.test_responses: 94 tests OK.
Not covered: no model ran here. The PR author reports 28 of 28 on an RTX
5080 with MTP and on a GTX 1080 Ti; the layer-split pipelined decode path
(the first call site) was not run by anyone, and the LP values were not
compared with a CPU softmax of dumped logits.
Co-authored-by: Leonardo Murillo <[email protected]>
Review of the first port: it registered kIlRows_89 / kIl1Rows_89 for every
compute capability 8.9 card by default. Bit-identity of an interleaved-kernel
choice to native_mmvq is a property of the compiled code, and the release
engine builds its own sm_89 cubin that no card of ours can run; the issue also
saw no end-to-end decode change on a 4090 or a 4080, and the 4070 and 4080
runs agree in 53 of the 75 cells of the 2-4 column table (33 of them non-zero). So the default
change has no measured benefit and an unproven bitwise claim on the card it
changes.
Change: sm_89 answers the shared table, with no one-column row, exactly as
before, unless STRATA_MMVQ_IL_89=1 is set (read once, like the neighbouring
switches). With it set the cells from the two community runs apply. Nothing
else in the lookup changes; sm_86, sm_120 and the others are untouched.
Why it is bitwise: native_mmvq_il_kernel keeps a separate accumulator set per
row (acc[v][i][c]) and sums each row over the same chunk order and the same
warp_sum, so the rows-a-warp count R only changes how many rows a warp walks,
not the order of any row's float additions; the plain-versus-interleaved
equality is what mmvq_il_parity measures (memcmp against native_mmvq for rows
1/2/4, T 1-8, odd row counts). That test passes on sm_86; it has not run on an
Ada card.
Tests: mmvq_il_parity now checks the unset behaviour (sm_89 equals the shared
table, Q3_K and the one-column row answer 0) or, with STRATA_MMVQ_IL_89=1, the
4070/4080 agreement. A second ctest, mmvq_il_parity_89table, runs the table
check with the switch set and no kernel case. On z440 (2x RTX A4000, sm_86,
CUDA 13.0, gpu lock): mmvq_il_parity and mmvq_il_parity_89table both pass, and
the binary prints "mmvq_il_parity: OK (every case, T 1-8, rows 1/2/4 and the
table, bitwise)" with the switch unset and set.
Model gate owed on an sm_89 card: phaseA-tests/gh027/gates/3453-sm89-il.sh
(mmvq_il_parity on the card, then identity of the answers with the switch on,
then a speed A/B that decides whether the default flips).
Co-authored-by: rodi4eg <[email protected]>
With --batch-groups above 1 on a layer split the pipelined batch loop never starts a round of the adaptive VRAM tier (the
serial batch windows start one every --adapt-every windows). While the slots are busy the tier only moves when they go back
to the solo path, so a fresh engine keeps its start-up profile for minutes.
STRATA_PIPE_ADAPT_EVERY=N (opt-in; unset, 0 or not a number is off): after N group windows pump starts no new group on the
first stage, lets the groups in flight finish, then runs one round as batch_step does between serial windows, adapt() and
apply_pending(true), so nothing in flight reads a cache slot while experts swap and the new experts have landed before the
next launch. Not while a prompt is read beside the slots (in_prompt_read, its loan of cache slots), not with --adapt-async,
not while the last round's copies are in flight, not with the tier off. The rule is two pure functions in
include/strata/program/batch_groups.hpp (pipe_adapt_every, pipe_adapt_due) with 12 new cases in batch_groups_test. With the
switch set the engine says so once at start and the "strata batch (pipelined ...)" line adds the rounds, the experts
swapped in and the time spent. docs/BATCHING.md documents the switch with the author's table (2x RTX 5090, six streams,
309 to 410 rows/s over eight bursts after a fresh start, two engines per setting).
Reviewed: unset costs one counter increment per group window and one false check where a group starts, and prints no new
line. A due round cannot wait forever: no new group starts, the groups in flight finish, then the round runs. A prompt read
sets in_prompt_read before the reader thread starts (including STRATA_PIPED_CONCURRENT) and clears it after the reader
joined; the drain after the read runs the round if it is due.
Tests: batch_groups_test (the new cases). Not covered and still the author's to show (the PR is a draft): a soak with
prompts read beside the slots, a Linux run, another GPU pair, more than two stages, and exactness settings with the switch
set. The model gate is written down.
STRATA_ENGINE_BACKEND=hip-rdna2 worked for strata update (updater.detect_hardware), but strata update --backend hip-rdna2 offered no package and
strata launch stopped with "STRATA_ENGINE_BACKEND='hip-rdna2' is not one of cpu, cuda, hip, metal, sycl". engine_component.split_backend reads
hip-<variant> as the hip family and its package; detect_backend (the start path), detect_hardware and update --backend all use it. Any other name is
checked as before. Tests in tests/terminal/test_gfx_selection.py.
STRATA_RAM_ADAPT (opt-in, --serve, one GPU, needs --adapt-every) lets the resident RAM copy follow the conversation:
experts the windows keep routing to take the RAM places of unused experts of their layer. Each move re-read the incoming
expert from the file and a layer moved one pair per window, although the incoming expert was usually read for the window
before and its blob still sits in that read's stage buffer.
What changed:
- FileExpertSource::replace_resident_staged(layer, in, out): the same move as replace_resident, with the bytes copied out
of the finished stage buffer of `in` (no file read). The copy runs under stage_mu_, so claim_stage cannot give the buffer
to another blob meanwhile; it needs a non-busy buffer whose key is `in`, and returns false (nothing changed) otherwise,
and the pair then goes the old way. The Foresight hold/unhold around the copy is the one install_resident uses.
- generate.cpp: a layer pairs up to 4 of its most routed outside experts with up to 4 of its least routed ones in the copy
(outgoing count below 0.5, incoming 2 or more), largest difference first. A pair whose incoming expert is staged moves by
copy; `n` now caps only the pairs that need a file read. The log line counts the moves and how many came out of a stage
buffer (ram_staged()).
- docs/DETAILS.md: the RAM_ADAPT bullet in the low-RAM list in plain words and one row of the measured table (the author's
numbers: RTX 3060, IQ2_XS and IQ3_XXS, 120 ShareGPT requests, 3 interleaved runs each).
Unset (the default) nothing runs: the block is guarded by ram_adapt_n > 0 as before. The weights moved are byte copies of the
same blob; which experts sit in RAM changes, as it already does with RAM_ADAPT, so greedy token ids may differ from run to
run once it is on. Changed from the PR: the DETAILS.md hunk was rewritten against the current, shorter bullet.
Tests: file_expert_source_test (new staged cases; the positive case needs unbuffered load and is skipped without it).
Not covered: no model run. Model gate written down: RTX 3060 ShareGPT replay =8 against =1.
The Build section said nothing about it. gemm.cu (create_hipblaslt_state, own_tuning_dir) takes STRATA_HIPBLASLT_TUNING, else the file
<engine folder>/tuning/<arch>-hipblaslt-<version>.txt, else plain hipBLAS. The report measured 826 against 1,043 tok/s at 8K prompts on a
gfx1151 (one run per arm, stated as such) and the log line that tells whether a table was taken. The extra log line the issue suggests for the
no-table case is a small engine change and is not part of this commit.
The first #3218 commit made setup.py admit a Maxwell (sm_52) card only
when a tester set STRATA_EXPERIMENTAL_SM52=1, and then routed it to the
CUDA 12 engine. That engine holds no sm_52 code, and users never set
environment variables to get a fix.
sm60_card() is back to Pascal and Volta (6.0 to 7.0). A PC whose only
NVIDIA cards are Maxwell now gets a warning (maxwell_note) that says no
ready-made engine holds code for it, gives the source-build recipe
(-DSTRATA_EXPERIMENTAL_SM60=ON -DCMAKE_CUDA_ARCHITECTURES=52, CUDA 12.x)
and the one reporter's result, and points to docs/OLDER_GPUS.md. The card
table line says Maxwell runs only from a source build. A Maxwell card
beside a newer card prints nothing new. Naming the card (--gpu, --cuda
12) or any env var does not admit it. The CMake floor from the first
commit is unchanged (opt-in build only).
Tests: tools.test_setup_older_gpus 23 tests OK (a Maxwell-only PC through
setup.main() with and without the old switches installs nothing and
prints the recipe; a Maxwell card beside an RTX prints no note).
Not covered: no engine was built for sm_52 by this commit; the P1004
compile is recorded in the notes.
The report written by BENCH.bat and bench.sh had fields missing or wrong: nvidia-smi's 'CUDA UMD Version:' (current drivers) was not read, the value
was the newest CUDA the driver supports but was labelled like the engine's toolkit, storage always said 'not measured', and Windows 11 showed as
Windows 10. Now the report says 'CUDA (driver supports up to): X; engine built for CUDA Y' (BUILD.json beside the engine), names the storage device
(Get-PhysicalDisk, system_profiler, lsblk -e 7 to skip snap loop devices), adds a 'GPU detail (after the run, near idle)' line with power limit and draw,
PCIe generation and width, pstate and temperature, and recognises Windows 11. The standard report (strata bench run, strata_cli/bench_facts.py) already
read the UMD version, storage and power limit; this brings the legacy tool to the same. Ported from PR #3250 by JoShMiQueL with its tests.
Two 0.1.44.1 changes no page told. TROUBLESHOOTING.md gets the start error 'the engine process ended: ...' (serve/server.py start_exit_note: the signal or
Windows exit code, and for an illegal instruction the AVX, AVX2, FMA and F16C flags the CPU lacks). SOURCE_INSTALL.md says a setup run again
pre-selects the installed size, marked '(installed)', and --yes takes it (setup.py size_tag, installed_size). Written by hand from PR #3243 by
Buddychek, whose hunks no longer fit the rewritten pages; each sentence checked against the two functions.
Review fix on the #4005 port. STRATA_PREFILL_COPY_WAIT_DEDUPE=1 skipped the copy stream's wait on a slot's release
event when the previous entry had waited on the same event. Two cases break that:
- At idx == ring the previous entry (ring - 1) is still in the first round, so its wait was on the event of an earlier
walk. The same slot id can be this walk's first flush, which re-records the event after that wait; skipping the wait for
slot 0 then lets the next copy overwrite a slot that the flush's gather has not finished reading.
- With a ring of one MMQ group (16, the smallest ring) a flush of the whole ring re-records the event right after the
previous slot's wait.
The rule is now copy_wait_dedupe_ok() in include/strata/prefill/copy_events.hpp: idx > ring, ring > group, same event.
copy_events_test models two to three walks with random group sizes and random issuer/consumer interleaving and checks that
a refill always waits on a record at or after the flush that released its slot. The same model with the PR's original rule
(idx >= ring) finds the stale-event case, so the test can fail. The default path is unchanged (dedupe off).
Tests: copy_events_test passes (z440, g++ -std=c++20). Not covered: a GPU run.
Review fix on the #2526 port. At an admission from a slot the port copies every checkpoint of the slot that is not
longer than the copied-back length into the main session's chain. Such a checkpoint is valid there only when its tokens
are a prefix of what was copied back (its K/V cells are then the ones the main session holds) and it has no pictures
(the main session's pictures are cleared at a give-back). The PR did not check either; a checkpoint left over from a
branch the slot no longer holds would sit in the main chain with cells that are not its own.
Both trees (CUDA/HIP and SYCL) now test the prefix and the empty picture list per checkpoint. The checkpoint the
request continues from is copied as before. Nothing changes when every checkpoint is a prefix, which is the normal case.
Tests: compiles on CUDA and SYCL (see the build lines of the branch). Not covered: no model run; the gate is written down.
docs/INTEL.md and docs/INTEL_ARC.md described an SSD expert tier that strata-sycl.sh's default (STRATA_VERIFY_NO_HOST=1) does not have: every
expert must be in VRAM or in the pinned RAM mirror, and the engine refuses to start otherwise (sycl/src/program/generate.cpp, 'REFUSED: ... are
neither in VRAM nor mirrored'). A native (IQ) pack refuses --spec 0 and 1 (sycl/src/program/generate.cpp and src/program/generate.cpp). Ported by hand
from PR #2153 by nekomario28 onto the rewritten pages, each sentence checked against those two messages. The hunk for docs/TROUBLESHOOTING.md is left
out: the entry it changed is no longer on that page.
Adds a minimal handoff configuration (--prompt-cache 1, --prompt-cache-every 0, --prompt-cache-root 0, --conversation-cache-mib 0), what each
option does, that it costs RAM, that it does not make cold prompts prefill in parallel, and how to look for the handoff in the engine log, /metrics
and /v1/status. Ported from PR #3334 by oscarlius (continuing #3035). The four options exist in src/program/generate.cpp and the o.prompt_cache > 0
gate matches the text; the log evidence was not reproduced here (no model run).
metal/src/mtl_rt.mm names MTLGPUFamilyMetal4 (macOS 26 SDK) and MTLCompileOptions.mathMode (macOS 15 SDK) behind @available, a run-time check;
the names must still exist in the SDK the engine is compiled against, so the metal-link job failed on the macOS 14 SDK on every pull request that
touched src/, include/ or metal/. The job now runs on macos-26 (the runner-images repository lists a macos-26 image). Ported from PR #3870
by stuchapin909. A GitHub Actions run cannot be made here: the author ran this workflow on his fork; the first run after the merge proves it.
#3824 changes the same line with macos-15 and an Xcode 26.0.1 path pin, and is not taken.
make_blob quantized its random test rows with ggml-cpu's type traits from_float, which the i-quants do not have: an IQ3_S pair called a null
pointer (an access violation on Windows, SIGSEGV on Linux) before the first timing. It now uses ggml_quantize_chunk, with a flat importance
matrix where the type needs one. A Q2_0 gate/up or down is timed as pool.cpp runs it (activations as an ActQ, Strata's own Q2_0 rows, SwiGLU
in between) instead of with ggml-cpu's Q2_0 dot, and the pair's header line says so. Other formats are timed as before. Ported from PR #3871 by
Licenser.
Built on this PC (MSVC, CPU only) and run: iq3_s/q2_0 0.531, iq2_xs/q2_0 0.336 and q2_0/q2_0 0.214 ms per expert per thread (--mb 512, 1 thread).
Checkpoint.raw mapped every shard for the life of the process. On Windows each faulted page of the ~64 GB of expert bytes is charged to the
process working set, so the pack step looked like a 64 GB RAM demand and was killed by free-RAM watchers on a PC with a model loaded (nothing
leaked: it all came back at once). raw() now reads the tensor's byte range into a fresh buffer through one cached file handle per shard; the
pages go to the system file cache, which counts as available. The author measured the same bytes, 1.2x faster, flat working set, and a
byte-identical pack. raw() is no longer thread-safe (this file uses no threads). Ported from PR #3378 by jerry78424 (the fix for his issue #3377)
with its two tests; I ran the tests (13 pass); the full 102 GB pack was not run.
A GGUF header with an unknown metadata type or an impossible length raised KeyError or OverflowError inside the reader, and setup, which expects
a ValueError, printed a traceback instead of its diagnostic naming the broken shard. Strings, scalars, arrays and tensor dimensions now share
one bounded read; counts are checked against the bytes left in the file before anything is constructed; type ids are checked before their values
are decoded; nested arrays are reported as unsupported; general.alignment must be a positive multiple of 8. The file name is added once to the
message. Ported from PR #2288 by KeyanHu-git with its tests (tools/test_gguf_reader_malformed.py).
Checked against the old reader on the 18 GGUF v3 vocabulary files of the pinned llama.cpp: same metadata, tensors and data start.
When one assistant message made several tool calls and the client returned the results in another order, the call ids were dropped before the
prompt was rendered, so the model saw anonymous results in the wrong order. The ids are now kept while the request is normalized; a run of
results is reordered to the call order only when the call ids and result ids are non-empty unique strings with matching sets. Anything else
(missing, duplicate, unknown or incomplete ids) keeps the received order, and the ids never reach the template. Ported from PR #3395 by
j-luwierski with its seven tests (serve/test_chat_tool_results.py); the hunk was merged by hand beside require_messages.
Problem: an Ada card took the shared MMVQ-IL table, which has no Q3_K row, so
Q3_K shapes ran the plain kernel and the other types used a table measured on
sm_86 and sm_120.
Change: kIlRows_89 (2-4 columns) and kIl1Rows_89 (one column) are registered
for compute capability 8.9. The 2-4 column table keeps only the cells where the
RTX 4070 run (rodi4eg) and the RTX 4080 run (kzhv) gave the same rows count and
sets the rest to 0, the plain kernel; the 4070 and 4080 disagree in 13 of 15
cells of the unmerged tables. The one-column row is Q3_K only, which both cards
gave. The issue's 5-8 column table is not taken: the 4080's changed in 10-14 of
20 entries between two runs of the same card, so there is no kIlRows58_89. The
bench output files of the PR are not ported.
Every choice in a table is bitwise native_mmvq's output (kernel row split only),
so answers do not change; this is speed only. No end-to-end decode change was
seen on a 4090 or a 4080 in the issue, so the gain is the microbenchmark's.
This does change which kernel an sm_89 card runs by default, so the model gate
(identity on sm_89) is written down in phaseA-tests/gh027/gates/3453-sm89-il.sh.
Tests: mmvq_il_parity (registered in CMakeLists.txt) now checks sm_89's table
against the two source runs (check_tables) instead of expecting the shared
table, and still checks bitwise parity of every case and rows choice on the GPU
it runs on (sm_86 on z440). docs/MMVQ_IL_TABLE.md updated.
Not covered: no sm_89 card here, so the table speed is the contributors'
numbers, not ours.
Co-authored-by: kzhv <[email protected]>
When the elastic image encoder failed to restart, its exception left request preparation and the client got a closed connection. It now goes
through the EngineDied path: HTTP 503 with the reason, a later picture tries again, text requests and a later good start are unaffected. An idle
encoder stop is logged even when the cache shrink was refused, and a layer split gets a line saying its encoder VRAM has to be kept free with
--vram-reserve-mib. Ported from PR #2435 by i-Vladimir-i with its tests (chat, messages and responses, streamed and not, plus a real encoder
that exits with code 71 on Linux) and the docs/VRAM_ELASTIC.md text.
On a layer split the later card is left with a few dozen MiB after loading, and each single-request window size is
captured the first time it is used (68, 64, 58, 52 and then 10 MiB free in the report); when one capture's
cudaGraphInstantiate ran out of memory, instantiate_evicting could only free graphs of batch slot layouts, a
single-request run has none, and "verify: instantiate: out of memory" ended the engine.
Now, after the batch layouts, a single-request window's capture frees the graph of the largest other window size (never the
one being captured or a prelaunched one; pick_window_victim) and instantiates again, and says so once: the freed size is
captured again when it is next used. capture_all keeps every size at once and is excluded. Only a failed instantiate does
anything new; with enough VRAM nothing changes.
window_evict_test (CPU, header-only) covers the choice. The real gate needs a model on a 2-GPU split with a thin later card
(listed in the triage file); not changed here: the later-stage reserve of the split search (kWindowMib), which is a default
placement change.
Under WDDM every cudaEventRecord and cross-stream wait is a submission of its own (the author measured ~80 / ~45 us each
on an RX 6900 XT under Windows 11), and the streamed walk paid one record and one wait per expert although a group
gathers after one wait on its last copy. Two tester switches, threaded issuer only:
- STRATA_PREFILL_COPY_EVENT_EVERY=N (2..64): the copied event is recorded every N-th entry, before the issuer waits and on
the last; the compute stream waits on the first recorded entry at or after the one it needs (the copy stream is in order).
- STRATA_PREFILL_COPY_WAIT_DEDUPE=1: the copy stream skips its wait on a slot's release event when the previous slot was
released by the same event.
The same copies into the same slots: the same bytes.
Default path (both unset): read line by line. The issuer records copied[slot] and waits on used[used_of[slot]] exactly as
before (the counters are bookkeeping, cover() is a no-op). Added to the PR so this holds by construction: wait_copied takes
the slot the old code used and, without the sparse switch, waits on copied[slot] at all three consumer sites (the PR derived
the slot from the entry number). The record/wait selection is now three pure functions in
include/strata/prefill/copy_events.hpp, with a host test (copy_events_test, registered in CMakeLists.txt) that models the
issuer and consumer with a full ring and checks that every wait names a live recorded entry at or after the one needed.
Tests: copy_events_test passes (z440, g++ -std=c++20). Not yet: a CUDA build of prefill.cpp in this tree (queued), no GPU
run. Not covered: the Windows HIP / WDDM speed claim and NVIDIA (the gate is written down). Needs the author's numbers
repeated before any default talk.
--host :: failed before the model loaded because Server always opened an IPv4 socket. Server now selects AF_INET6 when the bind address contains a
colon; the ready line, the 'from other devices' lines and the browser address bracket an IPv6 address. The bind is still the person's choice and
no default changes. Ported from PR #3212 by purrsec with its tests (serve/test_security.py, including a real :: bind). setup.py's --host help and
docs/DETAILS.md say :: is all IPv6 interfaces; the README hunks of the PR (they name strata launch --host, not checked) are left out.
A reporter built Strata for two Quadro M6000 (sm_52) by lowering two
floors and saw about 22 tokens a second with Qwen3.8-Flash-Next IQ1.
CMakeLists.txt: the experimental build (-DSTRATA_EXPERIMENTAL_SM60=ON)
now accepts sm_52 and prints a warning for it (Maxwell, one reporter, no
native half precision, CUDA 12.x). Without the flag the build still
stops at sm_75 as before, and the architectures a build uses are not
changed.
setup.py: sm60_card() admits a 5.2 card only when the tester sets
STRATA_EXPERIMENTAL_SM52=1, because the ready-made CUDA 12 engine holds
no sm_52 code and a Maxwell card must not be routed to it by default.
With the variable set, setup warns about the Maxwell card instead of
refusing. Default: a Maxwell card is "not supported" exactly as before.
docs/OLDER_GPUS.md: a Maxwell row and a short section, with the build
command and a request for an answer-text check.
Tests: tools/test_setup_older_gpus.py (20 tests, new Maxwell cases for
default, opt-in and a newer card present). Not covered: no model run, and
the answer text on a Maxwell card is unchecked.
When HEAD and the one-byte range probe both gave no length, download() took an early EOF as a finished file even though the repository pin (or
ModelScope's published size) names the size. The published size now stands in for the length, so the existing resume, retry and parallel code
publishes the file only after it has all its bytes; a size that disagrees with the pin is still refused before the transfer and the SHA-256
check is unchanged. Ported from PR #2287 by KeyanHu-git with its tests (tools/test_setup_download_length.py).
install_own_path treated a file as present only when <file>.done existed, and that mark is written by setup's own download or by verify_sha256,
which a file from another tool (LM Studio) never reached: setup said it was missing and asked for the full download size in free disk. With
--gguf-dir an existing file and its mmproj go to verify_sha256(own=True), which checks size and SHA-256, leaves a mismatching file alone and
writes the mark. A file that is not in the folder is still reported missing. Ported from PR #3606 by ai-bits with its two tests.
Under WDDM cudaMemGetInfo answers for the calling process: memory another process holds on the card (the image encoder,
a browser, another engine) is not in its free figure. The reporter measured, on an RTX 5090, 30,994 MiB free from CUDA
where NVML read 24,348 with 6 GiB held elsewhere, so --expert-cache auto sized the cache too large and shrank it by
guesses.
device_free_bytes() now takes min(CUDA's figure, NVML's figure for the whole card) when STRATA_VRAM_FREE=device is set.
nvml.dll is loaded from the system directory at first use (LoadLibraryExW with LOAD_LIBRARY_SEARCH_SYSTEM32), the card
is matched by its PCI bus id, and a missing nvml.dll, a missing export or a failed nvmlInit leaves CUDA's figure with
one line on stderr. The serve check of "VRAM free with everything loaded" uses the same function. The value can only get
smaller, so the switch can only shrink the expert cache.
Unset (the default), nothing changes: the NVML code is compiled only for _WIN32 and not for HIP, Metal or the no-VMM
build, and the env variable is read once. The serve check's min() is the same value elsewhere (on a unified-memory
system device_free_bytes() is never below cudaMemGetInfo's figure). Added to the PR: the NVML block is also left out
of a SYCL build (SYCL_LANGUAGE_VERSION), whose CUDA names are a compatibility layer. docs/DETAILS.md: one row in the
measured-and-left-off table instead of the PR's long paragraph.
Tests: builds on Linux CUDA (the Windows-only block is skipped), SYCL (P1006) and on Windows MSVC (owner PC build of
the final tree). Not covered: no run with a model; the Windows gate is written down.
resolve_rope accepted nan, inf, zero and values below 1 and handed them to the engine. It now stops with a message that names --rope-scale and the
valid range, before any configuration is written; valid factors and the automatic scaling are unchanged. Ported from PR #3393 by j-luwierski
(follow-up to #2950) with its tests; the --help text gets the range.
tools/check_cuda_spills.py reads the resource usage the compiler wrote into the kernel libraries (cuobjdump
--dump-resource-usage: STACK and LOCAL per entry function and per sm_) and fails on any Strata kernel outside its ALLOWED
table, which carries the measured allowance of the kernels that hold such an array on purpose; ggml's vendored kernels
(no namespace) are skipped and counted. ctest cuda_spill_guard runs it on eight libraries, skips (77) without cuobjdump or
a demangler, CUDA builds only (HIP has no cuobjdump); check_cuda_spills_unit runs tools/test_check_cuda_spills.py
(stand-in demanglers are sh scripts, so not on Windows).
Changes against the PR: (1) a demangler that exits 0 but prints a blank name used to make the kernel's key empty, which
reads as vendored, so a spilling Strata kernel was skipped; that is now "no verdict" (77) with a test. (2) The unit test is
registered. (3) The ALLOWED table was re-measured on f-027-int: sm_120 with CUDA 13.0 on Windows (MSVC) and sm_86 with the
CUDA 13.0 objects built on Linux (cuobjdump 12.9); allowances are only raised (new DFlash2 gemm8_kernel and
rope_table_kernel, native_gu_swiglu_kernel on sm_86, dequant_kernel / expdec_gu_kernel / prompt_attn_i8v3_kernel /
native_gu_swiglu_kernel on sm_120), none lowered or dropped. Result: 0 violations on both architectures; a test shows the
same object fails once one allowance is lowered.
Ported from PR #2683 (the tracking issue is #2682).
A json_object or json_schema answer that holds LaTeX such as \alpha is
rejected (502) because \a, \g and the like are not JSON escapes. New
opt-in: "repair_latex_escapes": true in response_format (and in
text.format on /v1/responses). Only after the strict parse failed, the
server doubles each backslash that does not start a valid JSON escape and
parses once more with the same duplicate-key and NaN rules, then checks
the schema as before, and prints one log line. Without the option, and
for any answer that parses on the first try, nothing changes.
Escapes that are valid JSON (\b \f \n \r \t \/ \" \ and a well-formed
\uXXXX) are left alone, because they cannot be told from LaTeX: a \beta
next to a \alpha comes back with a backspace. Truncated output, arrays,
NaN and duplicate keys still fail.
Tests: three new tests in serve/test_structured.py (default still 502,
opt-in 200 and stream, failures stay failures, helper cases, Responses
format passthrough). serve/test_structured.py 13 tests OK.
With STRATA_DECODE_TIMING=1 the serial line listed `verify + commit/emit + draft` after `ms/window =`, while the adaptive
wait before verify and the join after the draft were timed but printed after the sum, so the named parts did not add up
to ms/window. The serial line now reads `adapt wait + verify + commit/emit + draft + adapt join + other`, where `other` is
the decode time minus the named parts (not a timed step). The pipelined decode keeps its old line, byte for byte, and
nothing is printed with the timing off or with no windows. Diagnostic only: no decode path changes.
docs/DETAILS.md: a short paragraph on the new line (the hunk was rebased by hand onto the current file).
Tests: compiles on CUDA (the format string was counted against its 22 arguments); no parser in the repo reads
`ms/window = verify`. Not covered: no run of the line itself (it needs a decode).
The quick speed check has its own GPU probe, separate from strata_cli/bench_facts.py, and tried nvidia-smi then rocm-smi only. On a Radeon with
amd-smi alone it wrote the server's vendor stub and 'GPU driver: not read'. The same amd-smi probe as #3519 is added in the shape this file
uses, and a bare vendor name from the server no longer replaces a card the probe named. Ported from PR #3630 by l33tm4st3r.
The three new tests are POSIX-only (symlinks, /sys); run on z440 (Linux, Python 3.12): OK. tools/test_strata_bench.py on Windows: OK.
A batch slot holds the reply it decoded. The main session was parked with the conversation's prompt only (the
admission's copy), so when the next admission or a yielding read overwrote the slot, the reply lived nowhere: the
conversation's next turn found its prompt-read state and read the whole reply again (the author measured 255 of a
256-token reply re-read on four 200K chats over two slots).
What changed:
- park_current is split into park_source (one budget / RAM-admission / snapshot path) and two callers: park_current
(the main session, output line and byte accounting as before) and the new park_slot (an idle slot's ids, checkpoints,
sessions and draft K/V). The ParkSource passes what a park captures; the main session's retained K/V (#1503
take_reuse_for) is taken only for the main session, never for a slot.
- park_slot runs before copy_to_slot at a slot admission and before a BYIELD read overwrites a slot. It does nothing
when the live main session or a parked conversation already holds that state (ConversationCache::holds /
conversation_covers, new, with unit tests), for a slot with pictures, or without --batch-mtp when the drafter is on.
- When a slot is admitted into the main session its checkpoints come along (the shorter ones too), so the chain keeps its
branch points and the parked copies a turn back are superseded (#342) instead of taking an entry each.
- Same refactor in sycl/src/program/generate.cpp.
Changed from the PR: parking is an optimization like a checkpoint (#3350). The PR ended the request (ERR) or the batch
(batch_fatal) when park_slot's snapshot failed; here a failed snapshot, a failed device sync and a host allocation
that runs out are logged, the slot is overwritten as before and the request goes on. The slot's extra checkpoints that
come along at an admission are copied under a bad_alloc guard (the one the request continues from is copied as before).
Tests: conversation_cache_test (new holds() cases and the four-conversation replay at policy level) and
conversation_split_failure_test. Not covered: no model run (the batch path itself needs one); the gate is written down.
The Responses validator still refused anything below 1 after the chat
path made max_tokens of -1 and 0 mean no cap (documented in
docs/DETAILS.md). It now accepts -1 and 0 as no cap; values below -1,
floats, booleans and strings stay a 400. Nothing else changes: the
server already passes max_output_tokens or 0 to prepare(), which gives
the answer the room left in the context.
Tests: OverHttp.test_max_output_tokens_zero_and_minus_one_mean_no_cap
(stream and non-stream, status completed) and
test_max_output_tokens_bad_values_are_400. The first fails without the
change. serve/test_responses.py 43 tests OK.
Problem: with STRATA_Q8_SM60=1 the main model's Q8_0 matrices run on the GP100
Q8_0 kernel, but the MTP drafter's own Q8_0 draft head and projections kept the
generic kernels.
Change: with STRATA_MTP_Q8_SM60=1 (on top of STRATA_Q8_SM60=1) the drafter packs
its Q8_0 projections as packed copies, as the main model does (the prompt path's
draft_kv still reads the GGUF layout, so a copy is kept), and packs the draft
head's subset in place when every column count fits the kernel's shared-memory
planes and the data is 16-byte aligned (otherwise a packed copy). The in-place
pack writes the GGUF blocks back if the device write fails. Release unregisters
the entry without a cudaFree when the data was packed in place. New
native_q8_0_sm60_pack_quiet and native_q8_0_sm60_pack_in_place; HIP returns
false; metal/src/engine/stubs_0145.cpp gets matching stubs that answer
"nothing packed".
Replies do not depend on the drafts (they are verified); only the draft rounding
changes, because the Pascal kernel sums in another order.
Default path: mtp_q8_sm60() is false unless the env is set, so the packing calls
are skipped. The destructor now walks the tensor list once at teardown, not in a
hot loop. The SYCL tree has its own sycl/src/core/mtp.cpp, so it is not touched.
Tests: compile-only on sm_60 (CUDA 12.9) on P1004. Model gate (identical replies
with the flag on and off, decode speed on a P100) is written down in
phaseA-tests/gh027/gates/3751-mtp-q8-sm60.sh.
Not covered: no GPU run here; the contributor measured +4.4% on four GP100 dies
with all experts resident, one run per arm, not on a single streamed P100.
#1337 made a failed engine start in a later calibration step a candidate
that loses. An engine that started and then exited during its own rate
measurement still failed the whole run. measure_candidate() now treats
both the same: the candidate is dropped and recorded in
report["failed_starts"], the earlier results stay. It is used for the CPU
worker step, the adaptive expert tier step and the AMD shared-expert
fork. An operator cancel (CalibrationCancelled) is still raised. The first
start (steps 1 to 3) still fails the run.
src/core/verify.cpp: when adapt_gate_wait times out after 20 s, the
message now says which layer was reached (atomic and copy signal), that
the copy stream is not drained by the release, and dumps the
STRATA_VERIFY_TRACE breadcrumbs when that is on. Diagnostics only, #2188
stays open. The watchdog label is set only once a wait actually starts,
so a gate that is already open costs nothing extra (changed from the
PR, which set it on every call).
Tests: tools/test_calibrate.py 33 tests OK (3 new death tests).
Not covered: engine compile of verify.cpp is checked in the build step.
Problem: on a GP100 die (cc 6.0, no dp4a, no tensor cores) the verify window's
routed down projection for the native packs' IQ4_NL and Q2_0 rows (n_ff 640)
is latency bound. native_down_multi_kernel reads each weight as 2-byte loads,
one item at a time, straight from global memory.
Change: native_down_ws_kernel copies a block's rows (IQ4_NL 32, Q2_0 64, 11,520
bytes) into shared memory with 16-byte loads, then runs the same row dots on the
copy: same sub-warp, lane, items, float expressions and order. Only the integer
sum inside an item changes form (vmad byte selectors, prmt table lookups), the
same int8 x int8 products, so every output is native_down_multi_kernel's (expert
mode 0) bit for bit.
Gating: compiled only in the CUDA build with STRATA_EXPERIMENTAL_SM60 (not HIP,
not Metal). At run time only with STRATA_DOWN_WS=1 on a device of compute
capability exactly 6.0, IQ4_NL / Q2_0 down rows at n_ff 640, and not with
STRATA_OLD_IQ_MMVQ or STRATA_NO_SUB16_GU. Unset, the only change on the default
path is one cached boolean test per native_expert_grouped call.
docs/OLDER_GPUS.md lists the switch. The contributor's microbench numbers (one
PH402 die, Swift 1.5 IQ3_S, one-token window: IQ4_NL 72.6 us mode 8 and 54.5 us
mode 0 become 42.3 us; Q2_0 53.5 / 65.4 us become 38.6 us) were measured on a
four-GP100 rig, not here.
Tests: compile-only on sm_60 (CUDA 12.9) on P1004. The contributor's bitwise
check (native_expert_bench hash against modes 0 and 8) is not run here: it needs
a GP100 GPU run and the model's layers. The gate script is
phaseA-tests/gh027/gates/3867-down-ws.sh.
Not covered: no end-to-end measurement on a single P100 (16 GB cannot hold the
47 GB pack resident).
ROCm 7 and newer ship amd-smi and no rocm-smi, and the amdgpu product_name is empty on gfx1201, so a Radeon AI PRO R9700 run was filed
under 'unknown' with 'runtime: not measured'. bench_facts reads the market name from amd-smi static --asic, the VRAM from the amdgpu sysfs
file of the PCI address amd-smi list reports, the ROCm version from the install (or amd-smi -h), and keeps the probe's card name when the
server only says 'AMD Radeon'. Ported from PR #3519 by l33tm4st3r with its tests and the BENCHMARK_PROTOCOL.md text.
Changed while porting: the amd-smi branch also took macOS (os.name is posix there), which would have hidden the Apple chip; it now skips
darwin, and a test keeps the chip.
The "This computer" card said only "Fits fully in memory." for DeepSeek, with no word of the ~265 GB of disk. For a DeepSeek version it now says "Needs about {N} GB
of free disk space. What does not fit in memory is read from your SSD." (the spill line stays beside it when part of the model is read from the SSD), and the
selected version's line reads "{N} GB RAM + VRAM, plus about {disk} GB of disk". {disk} is not hard-coded: the catalog (tools/strata_mcp.py) gives each set
its `disk_gb`, the set's whole download (model files, n-gram table, drafter, metadata) plus the pack and room to work (dsv41_fit.DISK_HEADROOM_GB), so 2.00 and
2.25 differ. Same card and row markup, no design change; every other model keeps its card. Both sentences are fx80 and fx81 in the eight fix catalogs.
The Terminal's DeepSeek description makes no "fits fully in memory" claim (its lines already state the download and the free disk), so it is unchanged.
Older converters write the Engram scalars as engram.key_length and
engram.pad_id, and omit the sparse-attention geometry. The packer now
reads the old names only when the canonical engram.head_dim and
engram.pad_token_id are absent, and a new --config-json option fills the
omitted geometry from the source model's config.json. A value that
disagrees with metadata already in the GGUF stops the pack with a clear
message. The weights are never rewritten and a set that already has the
canonical names packs exactly as before.
Tests: LegacyModelMetadata (2 new, 5 in the file) in
tests/terminal/test_pack_gguf_engram.py.
Not covered: the author reports the older Q2_K set still needs dense
Q2/Q3 and Engram Q2_K tensor support in the engine; this does not add it.
The final response.completed / response.incomplete event and a
non-streaming /v1/responses reply now carry the same top-level timings
object that /v1/chat/completions has. The server already put timings in
the done dict; the Responses assembler dropped them.
Tests: UsageAndStatus.test_responses (stream and non-stream).
Unset timings leave the event unchanged; response.failed has none.
test_iq_pack imports without llama.cpp's gguf-py and skips the classes that build GGUF fixtures with a named reason; the MCP discovery tests use
a free port, not whatever Strata serves on 8080; the six conversation-cache tests add the repo root to sys.path so they run from the repo root
as the suite does. Ported from PR #3236 by Buddychek (tests only, no shipped code).
The 0.2.5 rewrite of the page dropped the 'Windows AMD: the driver resets' entry (v0.2.4.2 had it), although setup.py still tells a Windows gfx12
card owner to read it and tools/test_setup_amd.py (TdrPointer) checks for it, so that test failed on main. Restored from v0.2.4.2 with the
log and config locations worded for a source checkout.
Three 0.1.42 changes no page told. Ported by hand from PR #3244 by Buddychek (the PR conflicted with the rewritten pages), each sentence
checked against the code: /metrics/prometheus needs the API key like /metrics (serve/server.py _authorized), the prompt-speed row is
(prompt_tokens - reused) / prompt_ms (apps/host/server/monitor-dashboard.mjs), STRATA_VCVARS is read by setup.find_vcvars. The compiler
text now says Visual Studio 2022 (2019 is refused since 0.2.5, #3566) and went to docs/SOURCE_INSTALL.md.
conversation_detail::private_stream() and order_after_queued() kept one non-blocking stream and one event per device in
thread_local storage and never destroyed them. The layer split's later stages run each prompt chunk on a fresh
std::async thread (STRATA_HANDOFF_PIPE off, the default), and a checkpoint part is saved on that thread, so every
periodic checkpoint of every long cold prompt left a stream and an event behind on each later card: a few MiB of VRAM
per long prompt that never came back (#3470: the later card's free VRAM drops 2-6 MiB per long cold prompt, no plateau),
until cudaStreamCreate failed with "out of memory" (#3350: "conversation snapshot running-state copy: out of memory",
engine exit) on a card that was already nearly full.
A thread now hands its handles to a process-wide pool when it ends and the next thread takes them, so the number of
streams and events is the number of threads copying at once. Same stream, same flags, same contract; only the lifetime
changed. copy_streams_created() / copy_events_created() count what the process made.
conversation_copy_streams_test (CUDA, no model, skips without a device): 200 short-lived threads in a row create at most
one stream and one event; ten rounds of six concurrent threads at most six more; the same on a second device.
A conversation checkpoint is an optimization, like parking, but the writer treated a failed copy as fatal: "conversation
snapshot running-state copy: out of memory" (cudaErrorMemoryAllocation from the snapshot copy: the private copy stream's
first creation or the driver's staging on a nearly full card, not host RAM) made the prefill fail and the engine exit,
21 times in 4 h for one reporter and 16 in 34 h for another, each exit a reload and the in-flight requests lost.
Now, after a failed save, checkpoint_failure_is_survivable() clears the non-sticky allocation error and synchronizes the
device: when that is clean the checkpoint is skipped with a line on stderr and the request goes on (a layer split's stage
part is left empty, which already means "no checkpoint at this length"; the message-boundary / turn / root checkpoints
after a prompt chunk do the same). A device that is unwell (a sticky error, a GPU that stopped answering) still ends the
request exactly as before. Same edits in sycl/src/program/generate.cpp.
STRATA_TEST_COPY_FAIL_AT=N (tests only, like STRATA_TEST_SLOW_COPY_US): the N-th snapshot copy of the process fails with
cudaErrorMemoryAllocation, so the case can be made without a full card. Unset, nothing changes.
Not changed: "verify: instantiate: out of memory" (graph capture), which the reporter saw once.
CMakeLists.txt tested "NOT DEFINED CMAKE_CUDA_ARCHITECTURES" after enable_language(CUDA). Since CMake 3.18 that call
fills an unset variable with nvcc's default (sm_75 for CUDA 13), so the sm_120 default never applied: a hand build with
no option was an sm_75 build. The default is now set before enable_language, unless the user gave
-DCMAKE_CUDA_ARCHITECTURES or the CUDAARCHS environment variable, and the configure step prints a warning that says what
to pass for another card (a recommendation, not a refusal).
With -DCMAKE_CUDA_ARCHITECTURES=native, the one NVFP4 W4A4 unit (mmq_nvfp4_w4a4.cu) stayed on plain `native`, where
ptxas refuses the FP4 MMA on an RTX 50. `native` is now replaced by CMAKE_CUDA_ARCHITECTURES_NATIVE (the detected
architectures) before the 12x -> 12xa rewrite, as a list, so a mixed PC works too. docs/SOURCE_INSTALL.md says what
`native` and the other values build.
Release and setup builds pass their architectures explicitly and are unchanged. Configure matrix, CUDA 12.9 on Linux:
none -> 120 (+ the warning), CUDAARCHS=86 -> 86, 89, "75;86;120" (w4a4 120a only for the 12x entry), native -> detected.
Ported from PR #4029.
--adapt-decay took any number. Above 1 the adaptive tier's usage counts grow without end (inf after a few hundred
adaptations) and the swap gains turn NaN, which breaks the sort that orders the swaps; a negative value flips the counts.
The value is now checked where it is parsed (include/strata/program/adapt_decay.hpp, also in sycl/): a finite value in 0..1
is used exactly as before (0 and 1 included, the range the calibration profile already allows); anything else, NaN and inf
too, runs with the default 0.7 and says so on stderr. A warning instead of the PR's exit 2: a risky pick is reported,
the engine still starts.
adapt_decay_test (25 checks) covers the range, the fallback and finite counts after 100000 adaptations.
Ported from PR #1284 (the check; the refusal became a fallback).
The sandbox of a test start was deleted at once, so the PID records that show why a start failed were lost; only the last 64 KiB of the engine
log stayed. A failed start now also keeps run.json (without keys), the host, child and ready JSON records of the runtime (tokens cut to their
first 8 characters) and the host and server logs in .updates/logs/test-start-<version>-failed. The last three failed starts are kept.
Best effort, never hides the failure. Tests: tests/terminal/test_updater.py (FailedStartRecords).
The manual build in docs/INSTALL.md uses CMake's default generator (Unix Makefiles on Linux) in build/, and setup --build always asks for
Ninja, so cmake stopped with 'Does not match the generator used previously'. cmake_build now reads CMAKE_GENERATOR from the folder's
CMakeCache.txt; when it is not Ninja it says so in one line and removes the cache and CMakeFiles (no other file). A Ninja folder or a new
folder is untouched. Tests: tools/test_setup_build_dir.py.
A cmake -P script has no cmake_minimum_required, so CMake 3.x used the OLD behaviour and stopped at 'if given arguments ... IN_LIST: Unknown
arguments' (ctest ggml_tq_overlay with CMake 3.31). Checked with pip cmake 3.31.6 and 4.4.3 against the pinned llama.cpp: both print
'ggml_tq_overlay_test: ok'. A static test keeps the policy before the first IN_LIST in every cmake/*_test.cmake.
The script ended with a grep pipeline, so a failed compile exited 0 (and, before the ocloc check was added at the end, a good build exited 1).
A build.sh && next-step chain then ran after a failure. The status of cmake --build is kept and the script exits with it. The test runs the real
script with a stub cmake on PATH (good build: 0, failed compile: 1; the old script exits 0 on the failed compile).
tools/test_setup_golden normalized strata.exe but not strata-vision.exe, so all 25 baseline entries failed on Linux (48 subtests). VEXE
and HVEXE are mapped to <VEXE> and <HVEXE> like EXE, and the baseline records <VEXE>. test_nvfp4_local_pack imports pytest, which is not in
requirements.txt; under python -m unittest discover the module is now skipped with a reason instead of failing to import.
Substitutions were ordered by the length of their regular expression, and a user name wrapped in word bounds outranked the Strata or home
folder that contains it, so a report kept /home/<user>/Strata instead of <strata>. They are now ordered by the text they replace. Ported from
PR #3619 by ctr-137 with its test (Linux and Windows layouts).
The page said that starting a model never downloads and that nothing checks for updates in the background. Since 0.2.4.2 strata launch with a
DeepSeek model checks the signed feed (10 s at most, silent without a connection) and replaces an older installed DeepSeek engine package.
Ported from PR #4028 by pschaub (wording from issue #4022), checked against strata_cli/launch/runner.py upgrade_dsv41_engine.
engine_env put the ROCm library folders first on PATH, which only Windows' loader reads. On AMD Linux strata --families exited 127 on
libhipblas.so.3, so setup falsely said the engine does not run Qwen3.6. On Linux and elsewhere the folders now go first on LD_LIBRARY_PATH
(an existing value is kept); Windows keeps PATH. Ported from PR #3305 by xxxliqu1dxxx (resubmission of #2914), with its tests.
metal/setup_mac.py returned right after the engine step on --update while the model questions had been asked, so choosing a model the Mac did
not have ended with no download and no word. The update now names the missing model (or image encoder) and points at ./setup.sh --setup; an
update for a model that is installed prints what it printed before. Ported from PR #3848 by demetree, with its two tests (mocked; not run on a Mac).
adopt_user_defaults() set the token's default DACL with FA (FILE_ALL_ACCESS). That default also applies to new process objects, and the file
mask has no process query or set-information right, so a child could not be reopened by PID: the HighQoS helper timed out and
procs.start_token could not record the child. GA lets Windows map the mask to the object being created; the principals are unchanged.
Ported from PR #3428 by kossum (QoS reopen test). Test from PR #3399 by midhatn (a live child is identified and recorded) added; both fail
without the change and pass with it.
Co-authored-by: MIDHAT NASHAR <[email protected]>
_start() checked cfg['exe'] with Path.exists(), so a run config whose engine is component:strata-engine was refused as a missing file
before the server could resolve and verify it. Ported from PR #4027 by jubeater. A component reference is no longer tested as a file; model
files and literal engine paths are checked as before. ensure_engine_for, the Windows HIP runtime repair and the HIP architecture lookup
skip the source-build engine folder for a component. Eight tests (tools/test_setup_component_start.py).
Ported from PR #4011 by SuperDistance. project_version() read CMakeLists.txt with Path.read_text() and no encoding, which raised
UnicodeDecodeError on Windows code pages such as cp936. The test makes read_text default to each of 14 Windows code pages. The same crash was
reported in #3235 and #3414.
.git is not in the build context, so package_terminal.py had no commit to put in the archive and the documented source build stopped.
Ported from PR #3644 by robertoetcheverryr: ARG STRATA_COMMIT / ENV STRATA_BUILD_COMMIT as in docker/Dockerfile, and the two 'From source'
commands in docs/DOCKER.md pass --build-arg STRATA_COMMIT=$(git rev-parse HEAD).
Added: .dockerignore leaves out the folders docker/entrypoint.sh creates in a mounted data/ (install, state, home, Strata-data). A dangling
data/install/third_party/llama.cpp link in a clone that had been mounted made the Terminal stage fail with a copytree error (seen in the
review thread of #3644). Not built with docker here (no docker on this PC).
docker/build.sh staged only top-level tools/*.py, so the Terminal stage stopped at 'installer tool missing: tools/dsv41/tune.py'.
Ported from PR #3974 by hurui200320: the pathspec is tools/**/*.py, the no-.git fallback finds nested files, and the context test reads
INSTALLER_TOOLS and INSTALLER_SCRIPTS from packaging/package_terminal.py. Fixes the failure that images built from v0.2.4.1 and v0.2.4.2 had.
The portable lifeline watcher built a child's identity from `ps -o lstart=,stat=` and kept the whole line, so the token
changed with the process state (R while it starts, S asleep). The watcher compared its own reading with the token taken
at start, decided it was not the child it was told about and exited without ending it (test_lifeline "the child outlived
a killed server (watcher)", #3407). The identity is now the start time alone: field 22 of /proc/<pid>/stat on Linux
(a zombie has none), `ps -o lstart=` elsewhere; strata_control/procs.start_token drops the state column the same way.
A launch that is not a real Popen is not watched.
Telemetry._loop no longer ends on the first failing sample (a machine whose GPU or psutil source is unreadable): it
prints one line and keeps sampling, and NoLeakedSampler checks the thread itself instead of counting threads.
New test: the token is the same while the child runs (R) and sleeps (S); on the old code it fails on Linux
('...R' != '...S').
Ported from PR #3590.
A declared namespace.member tool (Codex MCP: mcp__filesystem.write_file) was accepted only as namespace__member (#3556).
Qwen3.8-Flash-Next also writes namespace=member (the <function= tag's equals sign ends up in the name), namespace_member
(one underscore dropped) and the bare member. Each is now an alias of the declared tool when exactly one declared tool
produces it and no declared name is the same; a name that matches nothing stays text (#3075), two namespaces that share
a member are not guessed, and a declared flat tool always wins. Requests without namespaced tools are unchanged.
serve/test_tool_name_alias.py (10 tests, 5 of the first 9 fail without the change; the tenth pins that a flat tool named
like the underscore alias wins), docs/DETAILS.md names the spellings.
Ported from PR #3972.
With --batch and conversation parking, a restore keeps the restored K/V (ConversationCache::retain) so the next park of
the main session copies only what changed. The reuse tracked a token count (unchanged_tokens), not whose tokens, and
copy_from_slot (a batch slot giving its conversation back to the main session) replaced the main session's K/V without
dropping the retained image. The next park reused the earlier conversation's K/V for the new one's prefix: a restore of
the new conversation then answered from the other one's history.
retain() now records the ids of the conversation the K/V belongs to; a park calls take_reuse_for(live), which reuses only
the prefix live shares with those ids (nothing when they share none or the ids are unknown), and one stderr line is
printed whenever a reuse is cut. copy_from_slot drops the retained image before it replaces the main session's K/V.
Same edits in sycl/src/program/generate.cpp. The guard only cuts reuse: at most one full copy of a parked conversation's
K/V, the saved bytes are otherwise identical; no flag, no default change.
Ported from PR #1503 onto the current tree (the first-stage ids sit after the stages field so the existing aggregate
initialisers keep their meaning). conversation_cache_test gains the cross-conversation cases of the PR and the logged
sequence (restore P, a slot hands Q to the main session, park Q): another conversation never gets the K/V, a diverging
one only the shared prefix on every stage, a slot hand-back drops it, unknown identity is never reused. Removing the
ids limit makes the first case fail.
A command on the allowlist used to run without a card with any option except -delete/-exec/-execdir/-fprint/-fls/-ok. Write modes (sort -o,
tree -o, uniq IN OUT), program-execution modes (sort --compress-program, rg --pre, git grep -O), date -s and tail -f got through, and so did
git config -e, git branch --set-upstream-to and git remote ... forms.
Each command now has a spec of the options and operands that only look (short and long, spelled in full); every other option, abbreviation
or attached form asks. git subcommands have the same specs, a branch/tag name is taken only with a list option, git config only reads with
--get. date takes only +FORMAT operands, uniq at most one operand, hostname none. less and more leave the list. Quoted texts are checked as
operands too. `du -sh` no longer trips the 'sh' word check.
Tests: apps/host/tests/gate-readonly.test.mjs (deny and allow cases for options, operands, spellings, quoting; every listed command asks for
an unknown option).
sample() applied the repeat / frequency / presence penalties to lg[id] for every token id of the last penalty_last_n
tokens of the history. A row shorter than the vocabulary (the MTP draft head picks among the first N token ids,
--draft-vocab N) made a history token with id >= N an out-of-bounds read and write on the host. A token that is not in
the row takes no penalty; the loop skips ids < 0 or >= lg.size(). A full-vocabulary row is bit-identical.
The loop and Sampling move to include/strata/qwen35/sampling.hpp (header-only) so a CPU unit test can run them:
src/qwen35/sampling_test.cpp (qwen35_sampling_test) checks nothing is written past a short row, that in-row tokens
take the penalty, and that a full row matches the unguarded loop bit for bit over 108 penalty/window mixes. Without the
guard the test dies with an access violation.
Co-authored-by: yehya <[email protected]>
cache_settle()'s store preload (DS4_SETTLE_STORE) read 2,243 hot experts into free store slots after every chunked prompt: 4,713 ms of a 4K prompt. Measured on the box (code fixture, prompt + 256 tokens, interleaved, 3 runs a side): with it on the decode is 13.9/14.4 s, with it off 14.2 s - no benefit - while the 4K prompt goes 31.3 -> 26.6 s (130.89 -> 153.78 tok/s). The swap loop above it is what the decode uses (it holds the CPU lane at ~5-7 k experts a run against ~14 k without), so that keeps its budget; DS4_SETTLE_STORE=1 restores the preload.
On the RTX 5070 (--agent-long, 22 tasks) split 36 kept agents at 6/10 (exact 4 and 7) and extraction at 585/606 facts (exact
585) for +11% prompt speed at 14K and +18% at 30K; the split-24 default passed 0/10 agent runs and found 556 facts, a tail of
8192 with split 24 found 574. KVA stays off unless STRATA_KVA=1. docs/KVA.md gives the numbers and the reasons; kva_test
checks the opt-in's split 36 / tail 2048 and the overrides. The geometry gate is unchanged.
The app toggle will write STRATA_KVA=1 for Qwen models in general; the split layer, the tail and the measured quality apply to
Qwen3.8-Flash-Next's shape only (48 layers, 512 experts). Any other shape (Qwen3.6-35B-A3B, ...) now logs "KVA configured but
NOT applied: this model's shape (L layers, E experts) is not one KVA was measured on" and reads exactly; the routine reasons
(short prompt, continuation) still come first. STRATA_KVA_ANY_MODEL=1 allows it, with one warning. kva_test and docs/KVA.md
cover both.
KVA was measured on the Flash-Next shape (48 layers, 512 experts) and the engine declines any other shape with a log reason, so the row would do nothing
for Qwen3.6-35B-A3B. Setup writes "arch" in a run config only for the families with an engine path of their own (Qwen3.6-35B-A3B: qwen35moe; DeepSeek:
deepseek41); every Qwen3.8-Flash-Next file (the original, Swift, Coder, Unsloth, Gyro, NVFP4) has none. The row is therefore shown for a model without an
"arch" on CUDA or HIP, in the app's Native engine settings and in the Terminal's Advanced table. The Radeon RX 9000 and DeepSeek rows are unchanged.
Tests: a Qwen3.6 config gets no KVA row (CUDA and HIP), the six Flash-Next family names do, and through the host fixture a model with an arch has no switch row.
The quality runs found lost records and invented rows in long extraction tasks and defects in agent loops with a ~10K
system prompt when most of the prompt was approximated. Opt-in now: unset (or 0) nothing runs, the draft layer included.
Switched on, the tail is 8192 tokens (STRATA_KVA_SUFFIX overrides), the safest configuration measured. A model other than
Qwen3.8-Flash-Next's geometry gets a one-line warning instead of a refusal. Kept: the untouched draft layer (cell floor
behind STRATA_KVA_DRAFT_FLOOR=1), the decline reasons, STRATA_KVA_SKIP=moe, the tests. docs/KVA.md says why it is off and
how to turn it on; kva_test checks unset = off and STRATA_KVA=1 = on with the 8192 tail.
The Speed row now follows the shared table's default: Faster long prompts reads "Off (recommended)" / "On" and its new sentence is "Reads very long prompts
faster by approximating part of the work. Off by default: answers about long documents can miss or mix up details." (replaces fx73; "Off (recommended)"
is fx79; translated in zh, ja, de, es, ru, pt, ko). The other two rows are unchanged ("On (recommended)" / "Off"). A STRATA_KVA=0 stored by an earlier
0.2.7 test build shows as Off. Still shown only where KVA applies (Qwen on CUDA or HIP). tests/speed-switches.test.mjs updated: the default launch has
no KVA entry, On writes STRATA_KVA=1, Off removes it, the old 0 shows as Off, and the other two switches are tested as before.
Owner decision (26 October, conditional on the last KVA variant results): KVA ships off. strata_control/speed_switches.py now gives every switch a default
and the environment it writes outside that default: KVA is default off and writes STRATA_KVA=1; the Radeon RX 9000 and DeepSeek switches stay on by
default and write their off entries as before. A launch in the default carries no entry. A file that holds STRATA_KVA=0 (the off switch of an earlier
0.2.7 test build) reads as off, which is what it is now; choosing Off cleans it up.
The Advanced table row says "off by default; set 1 to turn on" with the new sentence (`--set STRATA_KVA=1`; `off` or Del puts the default back).
tests/terminal/test_speed_switches.py covers the defaults, KVA on/off/old-zero, the rows and the comparison with the app's table.
The default stays the state-only form (byte-identical). With STRATA_KVA_SKIP=moe the GDN recurrence (gate projection, output
normalization, output projection) and the QSA attention (query side, selection, attention, output projection) run in full
for layers >= split, their outputs go into the residual, and the loop leaves before the MoE half; the expert transfers of
those layers are still skipped. The KVA line says "mode moe-skip". Motivation: the extraction losses of the state-only form
come from late-layer K/V and state built from a stale residual; this keeps the attention halves' contribution in it.
kva_test covers the switch.
fast_attn_enabled() was opt-in (DS4_FAST_ATTN=1); with the prompt chunk at 1024 it is worth far more than when
it was gated: measured on 4x R9700, 2.25 bpw, 8K code prompts, three warm prompts a load, the same ids
(5705e1164a) --
without it 1159 / 1180 / 1195 ppts (median 1180)
with it 1327 / 1354 / 1360 ppts (median 1354, +14.7%)
and after this commit the un-enved default reads 1314 / 1357 / 1369 (median 1357), i.e. identical to setting the
flag, so a deployment no longer needs any environment at all. The QK/PV sums are the matrix cores' order, which
the owner already approved for the prefill; the decode path keeps the scalar kernels. DS4_FAST_ATTN=0 turns it
off (it is a no-op on CUDA, whose dispatch is not compiled).
ring_mb_auto() returned a flat 400 MB on any box whose experts do not fit in VRAM, but on such a box the ring is exactly what lets a prompt queue its likely experts early. 400 -> 1200 MB measured on the box (settle off): the queue 3,200 -> 8,087 experts, the disk's effective rate 1.49 -> 2.44 GB/s, the compute's exposed read wait 11.4 -> 3.5 s, the 4K prompt 148.98 -> 190.98 tok/s; 16K 290.97 -> 315.01, 32K 316.92 -> 325.89. Decode unmoved in an interleaved A/B (17.54/16.39 against 17.61 tok/s). It is now a sixteenth of the VRAM room clamped to 400-1200 MB, so a smaller pool still loads; DS4_RING_MB still overrides.
c.A: the attention slab decides how many times each dense matrix is re-read a prompt. 8 -> 32 MB (STRATA_DS4_A_MB): 32K 325.89 -> 357.78 tok/s, 16K 315.01 -> 328.04, 8K +1.6%, 4K flat, decode unmoved (16.81 against 17.60 tok/s interleaved, noise-limited) for ~0.4 GB of VRAM.
Best measured prompt set with the two new defaults (DS4_SETTLE_MS=0;DS4_SETTLE_STORE=0, which is a prompt-only trade): 4K 195.18, 8K 258.47, 16K 328.04, 32K 357.78 tok/s. With the settle work at its default a 4K prompt is 31.3 s = 130.89 tok/s (was 88.30).
The dense GEMM was 11.1 s of a 4K prompt at hipBLAS's measured 4.97 TFLOPS (M 4096 N 2304 K 5120, the engine's call convention). A blocked WMMA kernel reaches 23.96 TFLOPS at that shape (128x128 tile, BK 32, 2-stage LDS, rocwmma; scripts/wmma2.cu) and the engine's own tuned kernel (BM 128 x BN 256, grouped tile order, raw wmma intrinsics) 20.8-20.9 at the wq_b shape and 38-41 at M 16384. gemm() now calls strata_pf_gemm_f16_ld with hipBLAS as the fallback (rows < 64, N < 512, K % 32, unaligned X: unchanged); STRATA_DENSE_PFGEMM=0 restores hipBLAS for an A/B. Same operands, the kernel's own k order (rounding-level).
Measured on the box (eng-028, 4E224E7F -> 467D7320, 4K prompt fixture p4k, chunk 16384): 46.4 -> 38.1 s, 88.30 -> 107.63 tok/s; with DS4_SETTLE_MS=0;DS4_SETTLE_STORE=0 27.5 s = 148.98 tok/s. 8K 134.66 -> 218.31, 16K 158.6 -> 290.97, 32K 178.9 -> 316.92 (settle off). Decode unchanged in an interleaved A/B (old 15.17/16.73, new 17.38/18.48 tok/s) with acceptance 151/164 = 92% (93/98/100/100/90 per position).
c.A (the attention slab, the reason each dense matrix is re-read 10-16x a prompt) takes STRATA_DS4_A_MB: 32 gives 107.63 -> 110.67, small because the expert reads now dominate.
The Models screen listed the shared sizes for DeepSeek but installed and loaded it at 8K whatever was chosen. Now the choice counts (8K stays the
default for a version that is not installed) and the list is DeepSeek's own: 8K to the limit the installed pack states in ds4.txt (max_position_embeddings,
read by the host as `contextMax`); a pack without it behaves as before (up to 256K). Past 256K the existing note slot under the model shows: "Not tested yet.
A context this long takes graphics memory from the model and reads long prompts slowly." (fx78 in the eight fix catalogs). Qwen's choices are unchanged.
src/lib/deepseek-context.ts is the app's copy of strata_control/dsv41_context.py; tests/terminal/test_dsv41_context.py compares the two.
The model reads 1,048,576 positions natively. Setup and `strata launch` offered DeepSeek the shared list that stops at 512K and dressed anything past
256K in Qwen's "rope scaling" words. Now:
- strata_control/dsv41_context.py: the choices are 8K, 32K, 64K, 128K, 256K, 512K and 1M (labelled "512K" and "1M"), limited to the pack's own limit
(max_position_embeddings in its ds4.txt) when it has one; a size past it (2M, or 1M on a pack that says 256K) is the one refusal; past 256K a plain
note says it is not tested yet and what it costs.
- setup.py: --context is checked before any download and again against the pack once it is built; the install tool refuses the same sizes.
- Terminal: the context step lists DeepSeek's own sizes (and the note beside them), a typed size is checked, `--context 512k` / `1m` and 524288 / 1048576
are accepted, plan and launch carry the note as a warning; Qwen's list and wording are unchanged.
- Engine: read, no change needed. strata-dsv41 takes --max-context as given (ds4_main.cpp) and sizes its buffers from it (ds4_model.cu); nothing clamps it
below the pack's limit, and the rope settings come from the pack. A test keeps it that way.
- docs/DEEPSEEK41.md: the cost (graphics memory per position taken from the expert cache, slow reading of very long prompts) and that 512K / 1M are untested.
Measured on a 5070 (IQ3_S) the cell floor cut decode after a 14K / 30K approximated prompt by 22 / 34 percent (drafts
accepted 100 -> 83 / 73 percent): a draft layer that reads only the exact tail loses its context. The approximated
chunks' draft cells are usable, so by default nothing about the draft layer changes: not the window, not the cells
built, not the attention. The floor machinery (MtpDrafter::set_cell_floor, window_ids_floor, KvaFloorRegistry) stays
behind STRATA_KVA_DRAFT_FLOOR=1 for research. kva_test now really covers window_start with floor 0, the registry and
wants_cell_floor.
The first default-on build set --mtp-window 2048 once per process whenever KVA was on, so short prompts (where KVA never
applies) lost draft context and decode speed. Now the draft layer keeps its window (32768 by default) and
MtpDrafter::set_cell_floor() names the first draft cell built from a real row: a KVA read raises it to the end of its
approximated chunks (they are neither built nor read), the draft attention starts at max(floor, pos - window)
(window_ids_floor, window_start.hpp), and KvaFloorRegistry gives every state a request continues from (live, checkpoint,
parked) the floor of its own tokens. A session KVA did not approximate has floor 0 and attends exactly as with
STRATA_KVA=0; with STRATA_KVA=0 or a process KVA is ruled out for the draft layer's code is 0.2.6's. The tail is 2048
whatever the window. Unit tests: window_start with floor 0 equals the plain window, the registry, wants_cell_floor.
- locale-gaps: the sentence added in 72c37ea2d (RTX 30/40/50, R9700, Strix Halo; "the line below says what to expect") had no key. fx77 in the eight fix catalogs.
- packaged-control: findControl was given an env without STRATA_COMPONENTS_DIR, so it fell back to the real per-user components folder and found the
updated Control installed on the PC (read only). The test now passes a scratch components folder.
Three rows under "Speed" in the native settings, in the existing row markup with the existing Select (On (recommended) / Off): Faster long prompts
(Qwen on CUDA or HIP; off writes STRATA_KVA=0), Faster answers on Radeon RX 9000 / AI PRO R9700 (Qwen on a gfx1200/gfx1201 card; off writes
STRATA_PCIE_FRAC_DEFAULT=old) and DeepSeek speed-ups (DeepSeek; off writes the six DS4_* = 0 of the contract). Each has a plain sentence saying what
turning it off does. A row is shown only where it applies, like the DeepSeek Speed row; the host decides from the model's file and the hardware report.
The switch is the model's file "env": off adds exactly its entries, on removes them, so a default launch is byte for byte what 0.2.6 started and the
Terminal, Control and the app reach the engine the same way (serve/server.py child_env). The host now shows and edits STRATA_* and DS4_* names (the
Strix Halo async entry is visible and kept); other names stay untouched. New strings are fx70..fx76 in the fix catalogs, translated in zh, ja, de, es,
ru, pt, ko. tests/speed-switches.test.mjs covers the default, each switch off and on again (same file), where each applies, and the catalogs.
strata_control/speed_switches.py holds the engine switch contract (Faster long prompts: STRATA_KVA=0; Radeon RX 9000 / R9700: STRATA_PCIE_FRAC_DEFAULT=old;
DeepSeek speed-ups: six DS4_* = 0), where each applies (Qwen on CUDA/HIP; Qwen on a gfx1200/gfx1201 card; DeepSeek) and the one-sentence effect text.
`strata launch` shows the environment names of the switches that apply as rows of the Advanced table (default "on", the sentence beside them); typing the
off value (`--set STRATA_KVA=0`) writes it into the run config's env, `off` takes it out again. A default launch carries none of them.
tests/terminal/test_speed_switches.py checks the names, the applicability, the config a launch writes, the rows, and that the app's table
(apps/host/server/native-settings.mjs) says the same.
The tail follows the draft layer: with KVA on and no --mtp-window given, the draft window becomes 2048 cells and the
fully computed tail 2112 (a given --mtp-window N sets the tail N + 64; a window of every cell, or STRATA_KVA_SUFFIX, is
handled and named). Layer splits, batch slots, --peer-device, pictures, the DFlash2 drafter, other model geometries and
the Metal build read exactly, with the existing "NOT applied: reason" line (quiet for continuation and short reads unless
STRATA_KVA=1). The reasons live in KvaConfig::decline and are unit tested (kva_test). Later chat turns continue from the
live session or a checkpoint and read their new tokens exactly over the cells the first read left; docs/KVA.md says so.
The slab rule (8 MB of q/o per device, 256 rows here) was set to keep the dense dequant's VRAM small; with a
1024-token chunk it dequantizes q_b/wo once per 256 rows - four times a chunk, 32 times an 8K prompt.
Measured on 4x R9700, 2.25 bpw, share 0.77, chunk 1024, 8K code prompt, ids 5705e1164a:
256-row rule 1204 / 1236 / 1250 ppts, decode 77.7 tok/s
chunk's 1024 1324 / 1353 / 1363 decode 78.1 (+9% prefill, decode unchanged)
The 1024 rows cost 268 MB a device (1% of the card). So high-share runs take min(chunk, 1024) rows and the
old rule stays elsewhere; DS4_ATTN_SLAB still overrides. The un-enved default verified at 1335 ppts /
77.8 tok/s with the ring at 253 MB.
With the 1024-token prompt chunk (the default where the cards hold most of the experts, abfe7d813) a big
staging ring is no longer the trade it was: every MB it takes is a tenth of an expert the decode keeps.
Measured on the same box, 8K code prompt + six 128-token decodes a load, ids 5705e1164a / 81d895366c:
2000 MB 1243 ppts / 73.2 tok/s
1000 MB 1287 / 77.5
500 MB 1314 / 78.5
256 MB 1324 / 78.4
128 MB 1338 / 78.3
So high-share runs take clamp(room/device * 0.008, 128, 512) MB (248 MB here) and the old deeper ring stays
for the cases it was measured for (share < 0.5, where the cards stream most of the experts). The un-enved
default verified at 1339 ppts / 78.1 tok/s, 42 s of summed GPU time in a 36 s pass.
prefill_chunk_default() returned 1024 only for a single-card run (own_default); a multi-card run whose cards
hold most of the experts fell through to 2048 (HIP skips the two 4096 branches) - and the rule's own datapoint
for 1024 was a 4 x R9700 run ('1024 370 tok/s, 8192 214'). Measured again here, 2.25 bpw, 4 cards, 8K code
prompt, --dspark auto, ids 5705e1164a, 4 prompts a load:
512 1110 / 1137 / 1142 ppts (median 1123)
1024 1229 / 1257 / 1270 (median 1243) <- this commit's default
2048 689 / 695 / 696 (median 692) <- what the engine picked before
8192 690 / 690 / 695 (median 690) <- what the 0.2.7 configs passed
Decode unchanged (73.2 against 73.2 tok/s medians, --dspark auto). So the owner's default 8K prompt goes from
692 to 1243 ppts (+80%) with no flag; --prefill-chunk still overrides.
DeepInfer's rule: verify a draft while the product of the confidences so far stays above the cut, instead of
only each draft's own floor. The 0.2.7 notes left it off ('not measured faster on code: H100 / owner PC'),
Project Maya runs 0.5 on a Radeon 8065S (+6.5% decode, the same tokens).
Measured on 4x R9700, DeepSeek 2.25 bpw, six 128-token decodes an arm in one load, same ids
(81d895366c): 71.7 -> 76.9 tok/s (medians) with STRATA_DS4_DSPARK_CUM=0.5, and a second pair 74.1/73.6;
the default with the rule on (no env) verified at 74.5 tok/s (6 decodes). CUDA keeps 0 (the rule was flat
there). STRATA_DS4_DSPARK_CUM still overrides either way.
The prompt path copies every streamed expert from the RAM copy into the pinned staging before its DMA (the
file's own note: 6.8 GB/s through the staging against 24-28 GB/s probes). Registering the copy lets it DMA
in place. Three interleaved pairs on 4x R9700, 2.25 bpw (share 0.72): the second 8K prompt of a process
12.0 / 11.9 / 11.9 s against 28.8 / 13.8 / 12.3 s unpinned; decode 35.42 / 35.50 / 34.9-36.0 against
35.43 / 35.45 / 34.9-36.0 tok/s, i.e. no cost; ids unchanged. Costs 23 s of load (registering a 50 GB
copy) - once a process, which for a server is the right trade. Auto only when the expert share is >= 0.5
(the CPU lane is then a minority of the decode, so the pinned pages' TLB cost cannot bite); DS4_COPY_PIN=0/1
forces it, and it stays off where the 12 GB cards' decode is CPU-lane-bound (the -15% measured there).
The RAM->staging memcpy runs on the IoQueue's threads; on a warm 8K prompt (stream stage 14.3 s, 12.8 GB a
device) they are busy 4.1 s, i.e. ~3 GB/s a device - a real share, and the thing to check in the 50-95 s
first-prompt runs (where the same counters are what to read first). DS4_PROF only.
The first prompt after a load is 2-9x slower than the second on the 4x R9700 2.25 bpw cell and it is not
the disk (0 MB read during it), the RAM store (removing it is worse), the dense GEMM shapes (q_b+rope
1.46 s) or the compute waiting (0 ms). The lead is the RAM->pinned-staging->DMA path (the file's own note:
6.8 GB/s through the staging against 24-28 GB/s probes); DS4_COPY_PIN=1 measured 47.9 -> 31.4 s on the
first prompt and 28.8 -> 12.0 s on the second, decode unchanged. This commit only adds the counters
(staging / DMA issue / slot wait) to the existing prompt stream line under DS4_PROF; no behaviour change.
No behaviour change on the 4x R9700 cell (126.8 GB of room / 4 = 31.7 GB a device, 8% -> 2000 MB, the value
measured in 76177ec34); on other card counts it is the per-device share that was meant.
The prompt path streams the experts the cards cannot hold. ring_mb_auto() gave 16 MB whenever the cards
held >= 1/1.3 of the experts, which is exactly this 2.25 bpw cell (126.8 GB of room, 156.3 GB of experts,
share 0.769): the stream then had no depth and the expert tiles waited 1.7 s inside a 5.2 s stage. A share
of the VRAM room (5%, 400-2000 MB) when any expert spills, 16 MB when they all fit (the 0.2.6 rule, kept
for the decode it was chosen for).
Measured, 4x R9700, 2.25 plain pack, 6 experts, --dspark off, 8192-token code prompt, ids 5705e1164a:
8K warm 632 -> 675-678 ppts (+6.8%, 5 prompts a run, 3 runs), agent decode 35.9 -> 35.3 tok/s (-1.7%,
4 runs). The profile of the warm pass: expert stage 5185 -> 4034 ms (kernels 3509 -> 2645 ms, the same
kernels now that the weights arrive in VRAM ahead of them, waits 1676 -> 1389 ms), everything else equal.
Also in this commit (opt-in, defaults byte-identical except the F16 activation path which is bit-identical):
- SetArgs::x16: gate/up take the layer's F16 copy of the activations (rows_f16 rounds exactly as the tile's
own smem store did) - half the activation bytes, 26% faster gate/up in the standalone bench
(box/expert_bench.cpp), +0.3% at the pass level (the stage is stream-bound).
- HIPK_CFG 6..11: tile variants for the expert GEMMs (64x128, 32x128, 128x256, 64x128/4 waves,
128/128+down 256, 64x256). 64x128 (cfg 6) is 21% faster than 128x128 in the bench and +0.9% in the pass.
- HIPK_DBG 1/2/3: switch the MMA / the decode off in the tiles (the breakdown in 09-marlin-kernel-diary.md).
- ds4_iq_hip.inc: a UTF-8 BOM on a source file removed.
It now ships on at 2.0 for a card of 13 GiB or less: +37.6% story / +35.2% code
decode over five interleaved pairs on an RTX 3060 12 GB, and it stays off on
bigger cards where it measured neutral to negative (l620 -1.9%/-0.5%, z440
-4.2%/+11.0%). It changes which experts run - the owner approved that trade
for 12 GB cards - and STRATA_ROUTE_PRIOR=0 turns it off.
The design was HIP-only: 80 resident blocks, a column slice of D each, two
grid barriers on an atomic counter, a barrier timeout so a card that cannot
hold all 80 blocks resident turns a hang into a refused start-up check. Ported
by giving grid_sync a CUDA branch (volatile generation counter + clock64() for
the timeout, scaled to SM clocks), guarding the gfx-arch refusal to HIP and
adding a masked-shuffle helper.
On the RTX PRO 5000 Blackwell 72 GB it passes its own check - '80 blocks on 110
multiprocessors, 0 token counts timed out, max relative difference 1.8e-07
against the default read' - and measurably does NOT win: story 201.4 -> 190.6
tok/s (-5.4%), code 245.6 -> 249.6 (+1.6%) over 3 interleaved pairs. That
matches the AMD note in the same file (an R9700 measured it neutral to 1%
slower), so it stays opt-in (STRATA_HC_PERSIST=1) and is not a default. The
port is kept as the starting point for a CUDA-tuned block mapping.
The router's top-10 is picked again with a logit bonus for the experts already
in VRAM (route_prior.cu); the weights stay the router's. Measured on an RTX
3060 12 GB, IQ3_XXS, 5 interleaved pairs of fresh engines: story 44.2 -> 60.9
tok/s (+37.6%), code 47.3 -> 64.0 (+35.2%); =1.0 gives +22.6% / +24.6%. Three
more pairs against the switch itself read +23.6% / +36.9%.
It changes which experts run, so it is a default only where the expert cache
cannot hold the model, and the owner approved that trade for 12 GB cards. It
is NOT a default on a bigger card: on an RTX PRO 5000 72 GB it measures
-1.9% / -0.5% and on 2x RTX A4000 -4.2% / +11.0% (3 pairs each); both were
re-run after this change and are unchanged (201.7 / 246.9 and 87.1 / 107.1
tok/s). STRATA_ROUTE_PRIOR=0 turns it off and any explicit value wins.
A draft for the owner: the one engine default that changed (the wide
hyper-connection up read on a 12.0 card, +1.9% story / +0.7% code decode on
the RTX PRO 5000, 5 pairs, bitwise), the four settings that are faster where
the PC supports them (PLE in RAM, the English draft vocabulary, --prefill-pipe
on a two-card split, q4_0 KV on 12 GB with its precision cost), and the list
of what was measured and rejected so it is not retried. No version number is
claimed: the owner decides what else 0.2.7 carries.
STRATA_GR_UP_WIDE (one warp a row, N/2 = 1,280 blocks instead of 160)
measured on the RTX PRO 5000 Blackwell 72 GB, IQ3_S, every expert in VRAM,
5 interleaved pairs of fresh engines: story 197.6 -> 201.3 tok/s (+1.9%),
code 243.5 -> 245.1 (+0.7%). A third run against the switch (default on vs
STRATA_GR_UP_WIDE=0) reads +1.7% / +0.8%. The produced token ids were
identical in all 6 pairs, and fused_gr_bench's per-T hashes are the plain
read's. The switch stays: =0 off, =1 on any card, STRATA_SM120_PERF=0 the
whole 12.0 set. Other cards are untouched.
setup writes DS4_ASYNC_T=4 into the config env for an integrated Radeon: the engine's mailbox path is sized for cards that hold
most of the experts and costs this box 31% on code (15.9-16.3 -> 20.1-21.3 tok/s), 16% on agent with identical accepted tokens,
9% on chat; the 2.25 set (no drafter) is unchanged. The same tokens come out. Discrete cards keep the engine default.
The engine's windowed decode uses the mailbox ("async") path for windows up to DS4_ASYNC_T (8 by default). That default was sized
for cards that hold most of the experts; on the 128 GB gfx1151 box the 6-row verify window is faster on the synchronous path.
Measured 2026-10-25 with the 2.00 bpw set and the shipped 0.1.51 engine (greedy, 256-token sets, three interleaved reps):
- code 15.87-16.33 -> 20.14-21.28 tok/s (+31%);
- agent, with identical accepted tokens a round (5.07): 15.93 -> 18.48 (+16%);
- chat 10.33 -> 11.30 (+9%);
- 2.25 code unchanged (11.2 -> 10.9): the drafter is unloaded there, so the extra syncs have nothing to hide.
The same tokens come out; only the scheduling changes, and the drafter's own single-token windows (T=1) stay on the async path.
dsv41_config_env(cards) writes DS4_ASYNC_T=4 into the generated config for an integrated Radeon only (a discrete card keeps the
engine default); STRATA_DSV41_ASYNC_T=n overrides the value and =8 restores the default. Tests:
tests/terminal/test_dsv41_async_t.py (5 cases). No engine rebuild; the signed dsv41 package is unchanged.
Docs: the STRIX_HALO and DEEPSEEK41 measured tables and the README row now read 19-21 tok/s code, 18 agent, 11 chat.
The trace origin was thread-local, and the prompt's later stages run on their
own thread, so their timestamps could not be compared with the first stage's.
One process-wide origin makes the hand-off visible.
Found with it on z440 (Q2_0, split 24, 16,383-token prompt, 8K chunks): stage
0 starts at 0 ms and 3,267 ms, stage 1 at 3,268 ms and 5,996 ms, read ends at
8,775 ms - the two stages do overlap chunk-for-chunk, and each takes ~2.7 s
per 8K chunk for its 24 layers. A split 30 run leaves stage 1 at 3.09 s a
chunk with 18 layers, so the per-chunk cost is not its layer count.
Same diagnostic as the request-step timestamps: with a layer split the chunk
line now says which stage is starting a chunk and when, so a prompt's stage
overlap can be read off the log. STRATA_TRACE only; no behaviour change.
Measured with it on z440 (Q2_0, split 24, 16,383-token prompt): serial reads
run stage 0's two 8,192-token chunks 3,332 ms apart and the whole read takes
8,788 ms; --prefill-pipe 1500 reads four 4,096-token chunks 1,582 ms apart and
the read takes 8,375 ms, so the stages overlap only partially at best.
The trace lines named the step a request reached but not how long it took to
get there, so a stall between two marks was invisible. Each line now carries
the time since the first traced step (thread-local). A diagnostic only: no
step, no order and no output changes without STRATA_TRACE set.
Found with it on z440 (Q2_0, layer split 24, 16,383-token prompt): the batched
read takes 8,758 ms while stage 0's own GPU timeline is 5,374 ms - stage 1 is
the slow one - and the expert-cache refill after the prompt costs a further
592 ms of the request.
Measured on the 128 GB box with the 2.00 bpw set: 4K prompt 78.7 -> 87.2 tok/s, 8K 108.9 -> 125.1, 32K 155.2 -> 176.9;
decode unchanged (16.53 -> 16.59). setup writes --prefill-chunk 8192 only for an integrated Radeon whose whole pool is at
least 64 GB; the escape hatch is STRATA_DSV41_NO_BIG_CHUNK=1. Also repairs UTF-8 damage in three docs from an earlier
PowerShell edit.
The engine's prompt-chunk rule returns 1,024 when a single card holds half or more of the experts; it was calibrated on
cards that hold most of them (4 x R9700). On the 128 GB Strix Halo (share 0.62, one shared pool) the bigger chunk wins:
measured with the 2.00 bpw set, engine 0.1.51, 2026-10-25: 4K prompt 78.7 -> 87.2 tok/s, 8K 108.9 -> 125.1, 32K 155.2 ->
176.9. Decode is unchanged (16.53 -> 16.59): the chunk buffers take ~5 GB from the GPU expert tier (81.9 -> 76.6 GB) and the
RAM store's hit rate rises (68.6% -> 76.8%) to compensate.
dsv41_prefill_chunk_args(cards, ram) writes --prefill-chunk 8192 for an integrated Radeon only, and only when the whole pool
(the RAM Windows sees plus the BIOS carve-out) is at least 64 GB; a discrete card or a smaller unified PC keeps the engine's
choice, and STRATA_DSV41_NO_BIG_CHUNK=1 turns it off. The chunk changes the prompt GEMMs' summation order, not the model: the
repo's chunk-size KL check reads 1024 0.1705 / 2048 0.1692 / 4096 0.1688 / 8192 0.1695. Test:
tests/terminal/test_dsv41_prefill_chunk.py (5 cases).
Also repairs UTF-8 damage in README.md, docs/STRIX_HALO.md and docs/MODELS.md from an earlier PowerShell edit (the middots,
em dashes and arrows had been double-encoded; no text changed). No engine rebuild is needed: the existing signed dsv41 package
accepts the flag.
The up projection of the hyper-connection read runs 160 blocks of 8 warps
(each warp takes 8 rows) on a 12.0 card, so 160 blocks of 192 SMs and ~6.7
warps an SM: the read is latency-bound at about 0.5 TB/s. This kernel takes
one row per warp and 2 columns x the 4 streams a block, so the grid is
N/2 = 1280 blocks (53 warps an SM). Every row's dot, xor tree, epilogue and
the cross-stream sum are gr_up_multi_kernel's, in its order, so the outputs
are bitwise the same; the token streams of both arms were identical in all
6 pairs measured.
Measured on l620 (RTX PRO 5000 72 GB, all experts in VRAM, IQ3_S, 3
interleaved pairs, fresh engine each): story 197.4 -> 201.3 tok/s (+1.9%),
code 243.0 -> 244.7 (+0.7%). Off by default (STRATA_GR_UP_WIDE unset); the
q8_1 tail of S26 needs the 16-column blocks, so the wide kernel is only used
when no q8_1 image is written.
The Windows AMD DeepSeek engine (feed member dsv41, windows-x86_64-hip-rdna3, gfx1151) ships with these
changes: the 0.2.6 copy-all rule is off on an integrated GPU (it copied 53 GB into RAM beside the GPU's 82 GB
in one 128 GB pool and an 8K prompt stalled; the in-place path is the 0.2.5 soak path), a Windows HIP package's
rocm/bin is on the engine's library path (setup, run config, app), the unified-memory fit plan counts the BIOS
carve-out, and --feed reaches the DeepSeek engine download. Docs, app text and tests included.
Measured on the Strix box (2.00 bpw, Faster, DSpark auto): 4K prompt 78.7 tok/s, 8K 108.9, code 17.6 tok/s,
chat 11.0. Clean-user end-to-end from a signed feed: install, download, pack, smoke, serve, chat, tool call,
agent round trip: 5/5.
strata launch --feed passed the feed to setup, but setup's parser had no --feed (getattr(a, 'feed', None)
was always None), so the flag was refused with 'Unrecognized arguments' and a local or non-default feed could
never serve the DeepSeek engine (found by the clean-user test on strix, 2026-10-25). It is now the same
option strata update has: a URL or a folder.
Found by the clean-user test on strix: 'strata launch --feed DIR' read the local feed for its own engine
check and then setup went to GitHub for the DeepSeek package, so a local (or any non-default) feed never
served the DeepSeek engine. setup's get_dsv41_engine/upgrade_dsv41_engine take the feed, install_dsv41
passes --feed, and runner.setup_args hands --feed to setup (a test in test_launch_dsv41).
The unified-memory plan counted only the RAM Windows sees (64 GB on the owner's box) and ignored the 64 GB
BIOS carve-out the GPU owns: 128 GB in one pool. uma_total_gb() adds the carve-out (or the GPU figure less
its shared part), so the plan now says about 109 GB of the 2.00 set's 132 GB stay in memory and 23 GB are read
from the SSD; the docs and the test fixtures use the 128 GB box. The 'measured' text says 128 GB unified too.
The Windows AMD DeepSeek engine package (feed member dsv41, windows-x86_64-hip-rdna3, arch gfx1151) is built from this
branch and installed as its own package; these are the code changes it needs.
- ds4_model.cu: on an integrated GPU the CPU lane's experts are no longer copied into RAM by the auto rule. The GPU's
expert tier and that copy are the same shared memory: on the 64 GB Strix Halo the GPU held 82 GB of experts, the rule
saw 62 GB 'free' and copied 53 GB, and an 8K prompt did not finish in 11 minutes. Found 2026-10-25 on strix; the
corrected build does the same prompt at 108.9 tok/s (4K 78.7), code answers 17.6 tok/s, chat 11.0. DS4_COPY_ALL=1
still forces the copy for a test.
- dsv41_engine.py: lib_dirs()/start_env() put the package's BUILD.json lib_dirs on PATH/LD_LIBRARY_PATH (a Windows HIP
package keeps hipBLAS/rocBLAS in rocm/bin, or a clean PC fails with 0xC0000135); the upgrade test-start uses it.
- setup.py: the dsv41 engine's library dirs come from the package (so the run config carries rocm/bin), the unsupported-card
text names Ryzen AI Max on Windows, and the manual package hint names the .tar.gz the release page actually has.
- runner.py: the same library dirs for the app/Terminal path.
- dsv41_fit.py: a unified-memory plan for the official IST-DAS sets (one pool, what stays in RAM, what is read from the SSD,
measured 14-16 tok/s on code, 10-11 on chat, 4K 78 / 8K 109), never a refusal; tools/strata_mcp.py uses it for the app
catalog's on-this-PC line.
- Docs: STRIX_HALO.md (DeepSeek section with speeds, driver 26.10.1, memory behaviour, limits), DEEPSEEK41.md, MODELS.md,
AI_REFERENCE.md, README.md, packaging/README.md (the Windows AMD package recipe).
- Tests: tests/terminal/test_dsv41_fit.py (7) and 4 new tests in test_dsv41_engine.py (lib dirs, feed selection for gfx1151).
The Windows commit limit decides how many experts an integrated GPU may hold (place_experts). With AMD driver 26.10.1 the limit
went from 74 GB to 133 GB on a Strix Halo, the engine put 9506 experts (86 GB) on the GPU instead of 8105, every byte of the
GPU's address space was gone, and the 32K prompt's later allocations spilled into RAM: free RAM fell to 0 and the engine died
("rocBLAS: No devices found"). The experts now leave 4 GiB + 0.2 MB a token of context above 16384 (DS4_UMA_GPU_RESERVE_GB sets
it, tests); only an integrated GPU on Windows is affected.
Strix Halo (gfx1151, 2.00 pack, shipped profile), driver 26.10.1:
32K prompt: killed (min free RAM 0) before, 113.2 tok/s with min free 13.6 GB now; 64K prompt (70000 context): 122.2 tok/s, min free 14.4 GB
4K / 8K prompt: 62.8 / 83.9 tok/s (64.9 / 86.2 with the 9506 experts: -3%), min free 13.4-13.8 GB
KL, code segment, 999 positions: 0.1020 (0.1028 before)
A smaller reserve (3 GiB + 0.1 MB a token) gained 0.0% on 4K / 8K / code decode and left 0.9 GB less free at 64K, so it was not taken.
R9700 1 card 2.00: 21.30 -> 21.82 tok/s (+2.4%), 4 cards 35.39 -> 37.00 (+4.5%), 3 pairs each, same generated ids. DS4_QUANT_SINGLE=1 / DS4_QUANT_NODEDUP=1 go back to one launch an item / no sharing.
The 512-bit i-quant rows (IQ2_XXS/XS/S, IQ3_XXS/S) assembled their grid words from scalar loads (~30 uops a 64-value chunk). Where cpu_gather_fast()
(Intel Sapphire Rapids and later) they now load them by one gather, 32-bit for IQ3 and 64-bit for IQ2 (STRATA_IQ_GATHER=1/0 force it; Zen 4 stays scalar:
its gathers are microcoded). The same words are loaded, so every output bit is unchanged. One token of an IQ3_XXS / IQ3_S expert takes the exact 512-bit rows (ggml-cpu's
single-token AVX2 dot bit for bit) with the gather, after a first-use bit check against this ggml; STRATA_NO_IQ512X1=1 turns it off.
Xeon 8468, native_gu_rows (gate+up of an expert), us: IQ3_XXS 1 token 295 -> 219, 2 tokens 462 -> 300, 4 tokens 558 -> 389; IQ3_S 415 -> 305, 603 -> 342, 562 -> 296;
IQ2_S 2 tokens 377 -> 235, 4 tokens 437 -> 327 (one IQ2_S token stays on ggml: 239 against 234). Output checksums identical with and without.
Test iq512_gather_parity (both lookup modes): exact rows == ggml vec_dot at 1-4 tokens, engine rows within float rounding.
After 'Update Strata' installed a new app version, the host that asked for it was ended by the installer, so the job stayed saved as running and the next start turned it into a red 'Interrupted operation' alert in Settings > Update and Diagnostics. The job now records the app version it started from, and a start under a different version marks it done ('Strata is updated to X').
strata update and the app's check reused the feed used last, including a loopback address or a folder from a test of the update feed; every later check on that PC then said 'No internet connection'. Only an https address is remembered now.
Without --adapt-async on the command line, the expert arena on one CUDA GPU (--serve, adaptive tier on, no --batch, no layer
split, no pipelined windows, no peer or remote tier) starts on the blocking tier, ignores 64 steady decode windows, measures 64,
and when the tier's wait is at least 5% of a window runs 64 windows on the asynchronous tier, going back (for the session) only if
that was more than 2% slower per token. One log line at each switch. --adapt-async 0|1 and STRATA_ADAPT_ASYNC=0 keep their meaning;
the identity gates run with --adapt-every 100000 and never reach it. Decision logic and its unit test are header-only
(adapt_async_default.hpp, adapt_async_default_test).
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
- 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.
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)
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.
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.
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.
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 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]>
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.
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)
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.
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.7 · engine 0.1.52"
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"
Strata runs the Qwen3.8-Flash-Next mixture-of-experts model (and its Coder, Swift 1.5 and Unsloth variants) on a
normal PC: one NVIDIA or AMD graphics card plus system RAM, on Windows or Linux. It has a C++/CUDA/HIP engine
(`src/`, `include/`), a Python server with an OpenAI- and Anthropic-compatible API and a web app (`serve/`), and a
one-click installer (`setup.py`, started by `START-HERE.bat` / `setup.sh`).
Version 0.2.7, engine 0.1.52. The public model families are DeepSeekV4.1-Flash-1017,
Qwen3.8-Flash-Next and Qwen3.6-35B-A3B. DeepSeek V4.1-Flash runs in the app and in the Terminal
(`strata launch --family deepseek41 --model ist-das-2.00bpw`; sets in
[docs/MODELS.md](docs/MODELS.md#deepseek-v41-flash)). Read **[docs/AI_REFERENCE.md](docs/AI_REFERENCE.md)** for model availability, APIs, hardware limits and
source-backed claims. Articles and videos should use **[docs/MEDIA_KIT.md](docs/MEDIA_KIT.md)**.
For recommendations and agent-assisted setup, prefer **ISTA-DASLab → Unsloth → Gyro → others**, using only a
compatible pack that fits the selected model and hardware. ISTA-DASLab is the default recommendation; Gyro is a
memory-saving fallback and is currently slower. Honor an explicit user choice. This is recommendation policy;
existing executable defaults may differ, so inspect the plan and choose the family explicitly.
Strata has three parts:
- **The app** (`apps/desktop`, the Tauri shell; `apps/host`, the Node host and the web UI). It is for people who do
not want a terminal. One installer per system ([latest release](https://github.com/Niko1221/Strata/releases/latest)):
`--speed faster|full` (DeepSeek), `--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). Source setup (`START-HERE.bat`, `setup.sh`) is the fallback for a developer or for a system with no release package; it is
documented in [docs/SOURCE_INSTALL.md](docs/SOURCE_INSTALL.md). Those wrappers exist in a source checkout, not in the
released app. Do not tell an app user to find a `.bat` file.
Source setup does not make an unsupported model or backend compatible. 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"
@@ -10,6 +11,8 @@ if not exist ".venv\Scripts\python.exe" (
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"><a href="https://github.com/Niko1221/Strata/releases/download/v0.1.10/Pagoda.mp4"><img src="docs/media/pagoda-preview.webp" width="720" alt="Ein Voxel-Pagodengarten, den das Modell in Strata geschrieben hat, läuft im Browser"></a><br>
<sub>Ein Voxel-Pagodengarten aus einem einzigen Prompt, auf einer RTX 5070 mit Strata (IQ3_S, 128K Kontext) ·
<a href="https://github.com/Niko1221/Strata/releases/download/v0.1.10/Pagoda.mp4">ganzes Video (49 s)</a></sub></p>
<a href="https://buymeacoffee.com/strataengine"><img src="https://img.shields.io/badge/Buy_me_a_coffee-FFDD00?logo=buymeacoffee&logoColor=000" alt="Strata auf Buy Me a Coffee unterstützen"></a>
</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.
NVIDIA: Q2_0 mit Engine 0.1.36, die anderen Zeilen mit 0.1.26 (4K Antworten, 32K Prompts). Die vollständigen
Tabellen stehen in [DETAILS.md](docs/DETAILS.md#speed-measured). Eine Karte mit mehr VRAM ist schneller: Eine
RTX 3090 (24 GB) sollte etwa 100-140 Tokens pro Sekunde schreiben. Lange Chats und andere Karten:
[Tempo jedes Modells](docs/MODELS.md#how-fast-is-each-size), [Ergebnisse aus der Community](docs/COMMUNITY_BENCHMARKS.md).
<p align="center"><a href="https://buymeacoffee.com/strataengine"><img src="https://cdn.buymeacoffee.com/buttons/v2/default-yellow.png" alt="Buy Me A Coffee" height="50"></a><br>
<sub>Strata ist kostenlos. Wenn es auf deinem PC gut läuft, hält ein Kaffee die Arbeit daran am Laufen.</sub></p>
## Was du brauchst
| | |
| Anbieter | Auswahl |
| --- | --- |
| **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. |
Set up Strata on this PC for me: https://github.com/Niko1221/Strata - follow docs/AI_SETUP.md in that repository.
In der **Desktop-App** und im **Terminal**. Was nicht in den Arbeitsspeicher passt, wird von deiner SSD gelesen. Darum brauchst du viel freien Speicherplatz.
| Set (IST-DAS) | Download | Freier Speicherplatz nötig |
| --- | ---: | ---: |
| 1.75 bpw | 239 GB | etwa 245 GB |
| 2.00 bpw | 257 GB | etwa 265 GB |
| 2.25 bpw | 274 GB | etwa 280 GB |
Läuft auf NVIDIA RTX 30, 40 und 50 (Windows und Linux) und auf AMD Radeon AI PRO R9700 (Linux). Das Setup lädt die Dateien und die DeepSeek-Engine herunter und startet dann das Modell.
Er prüft deine Grafikkarte, deinen RAM und deine Festplatte und wählt das passende Modell. Dann installiert und
startet er es und sagt dir, wie du deine Apps verbindest. KI-Tools können Strata auch über seinen
[MCP-Server](docs/MCP_SERVER.md) installieren, starten und stoppen.
Gemessen mit dem 2.00-bpw-Set auf Strata 0.2.5. „Ausgabe“ ist das Tempo der Antwort, „Prompt“ zeigt, wie schnell Strata deine Eingabe liest.
### Oder mach es selbst
| Hardware | Ausgabe | Prompt |
| --- | ---: | ---: |
| RTX 5070 12 GB, 64 GB RAM | 4 bis 6 Token/s | etwa 110 Token/s (4K) |
| RTX 3060 12 GB, 46 GB RAM | 9 bis 11 Token/s | etwa 155 Token/s (4K) |
| 1x Radeon AI PRO R9700, 251 GB RAM | 21 bis 25 Token/s | 320 bis 360 Token/s (8K) |
| 4x Radeon AI PRO R9700 | 74 bis 77 Token/s | 400 bis 450 Token/s (8K) |
[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.
[Speicher, Festplatte und Details](docs/DEEPSEEK41.md) · [Versionshinweise](https://github.com/Niko1221/Strata/releases)
**Mac (Apple Silicon, M1 bis M5, macOS 15 oder neuer):** Installiere einmal Apples Command Line Tools (`xcode-select --install` im Terminal; Xcode selbst brauchst du nicht). Öffne dann das Terminal im Strata-Ordner und führe **`./setup.sh`** aus. Es baut die Mac-Engine auf deinem Mac (etwa 7 Minuten), stellt dieselben Fragen, lädt das Modell und startet es. Beim nächsten Mal startet `./setup.sh` sofort. Welche Größe zu deinem Mac passt und wie schnell es ist: [docs/MAC.md](docs/MAC.md).
</details>
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:
<a id="the-other-sizes"></a>
- welches Modell und welche Größe,
- wie viel Kontext (wie viel Text sich das Modell merkt),
- ob es Bilder lesen soll.
Unsicher, was passt? [Wähle ein Modell für deinen Computer](docs/MODELS.md#pick-by-memory).
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`.
## Installation
> **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.
<a id="option-a-the-strata-app"></a>
**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).
### Desktop-App
## Welches Modell soll ich nehmen?
**[Strata für Windows, Linux oder Mac herunterladen →](https://github.com/Niko1221/Strata/releases/latest)**
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.
Installiere die App, wähle ein Modell und leg mit dem Chat los. Es gibt einen Installer pro System. Beim ersten Start lädt die App die Engine für deine Grafikkarte herunter. [Hilfe zum Installer](docs/INSTALL.md#the-app).
| Dein RAM | Nimm | Warum |
| --- | --- | --- |
| **32 GB** | **Coder** | passt in 32 GB und ist für Code gemacht (mit einer 24-GB-Karte laufen auch Q2_0 und IQ2_XS) |
| **48 GB** | **IQ2_XS** (oder Q2_0, das schnellste) | die größeren Größen passen nicht |
| **64 GB** | **IQ2_XS** (empfohlen) oder IQ3_XXS / IQ3_S | alle Größen passen; IQ3_S ist das beste und das langsamste |
| **96 GB oder mehr** | **IQ3_S** oder Unsloths UD-IQ4_XS (~4 Bit) | Platz für die größten Größen, auch wenn alles andere offen ist |
### Optional: mehr Tempo mit einer Grafikkarte, wenn das ganze Modell in den RAM passt
Zwei Opt-in-Einstellungen (von RubenTorney, #1904 und #1905). Sie helfen, wenn du eine Grafikkarte nutzt und dein RAM alle Experten hält (Stratas Arena-Modus, zum Beispiel Q2_0 oder IQ3_XXS auf einem PC mit 64 GB). Öffne deine Konfiguration `strata-<model>.json` im Strata-Datenordner ([wo](docs/INSTALL.md#where-things-are-stored)) und füge hinzu:
- **Schnellere Ausgabe:** `"--adapt-async", "1"` in die Liste `"args"`. Der Experten-Cache wird dann in einem Hilfs-Thread aktualisiert, statt die Antwort anzuhalten. Gemessen auf einer RTX 5070 (Windows, Q2_0): +4-5 % Ausgabe, +7-10 % bei Code und langen Dokumenten. Antworten können leicht vom Standard abweichen (sie bleiben von Lauf zu Lauf gleich).
- **Schnelleres Prompt-Lesen unter Windows:** `"STRATA_PIN_BY_NEED": "1"` im Block `"env"`. Windows kann nur etwa den halben RAM für die Grafikkarte sperren; damit werden zuerst die Experten gesperrt, die die Karte wirklich braucht. Gemessen auf der RTX 5070: +4,4 % Prompt-Lesen.
```json
"args":[...,"--adapt-async","1"],
"env":{"STRATA_PIN_BY_NEED":"1"}
```sh
curl -fsSL https://github.com/Niko1221/Strata/releases/latest/download/install.sh -o install.sh && sh install.sh
```
Starte Strata danach neu (`START-HERE.bat` oder `./setup.sh`). Zum Rückgängigmachen entferne die beiden Einträge.
- **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.
Öffne unter Windows danach ein neues Terminal. Dann:
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).
```sh
strata launch
```
## Wie funktioniert es?
Folge den Anweisungen, um ein Modell herunterzuladen und zu starten. [Terminal-Anleitung](docs/TERMINAL_LAUNCH.md).
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.
</details>
<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>
<p align="center">
<img src="docs/media/terminal-monitor.png" width="900" alt="Strata-Terminal-App mit Modellaktivität, Tempo, Speicher und letzten Anfragen">
<br><sub>Behalte Modell und Hardware im Blick, während es läuft.</sub>
</p>
- **Das Modell ist ein Team aus 24,576 kleinen Spezialisten („Experten“).** Jedes Wort braucht nur 10 davon.
- **Deine Grafikkarte** hält die paar tausend Experten, die am häufigsten gebraucht werden. **Dein RAM** hält alle,
und **dein Prozessor** rechnet gleichzeitig am Rest. **Deine SSD** hält eine große Nachschlagetabelle.
<a id="build-from-source"></a>
<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>
[Aus dem Quellcode bauen](docs/SOURCE_INSTALL.md) · [Docker](docs/DOCKER.md) · [Vollständige Installationsanleitung](docs/INSTALL.md)
- **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.
<a id="what-you-need"></a>
Die längere Erklärung: [docs/HOW_IT_WORKS.md](docs/HOW_IT_WORKS.md). Jeder Teil und seine Zahlen:
[die Details](docs/DETAILS.md#how-it-works) und das [Paper](docs/paper/Strata-Paper.pdf).
| **Apple Silicon · M1 oder neuer** | [Mac-Setup](docs/MAC.md) |
| **AMD Ryzen AI Max · Strix Halo** | [Strix-Halo-Setup](docs/STRIX_HALO.md) |
Das Modell ist [Qwen3.8-Flash-Next](https://huggingface.co/Qwen/Qwen3.8-Flash-Next) vom Qwen-Team. Komprimiert
wurde es von [ISTA-DASLab](https://huggingface.co/ISTA-DASLab/Qwen3.8-Flash-Next-GSQ-RCO-GGUF), UkisAI (Swift 1.5)
und Unsloth. Strata nutzt Teile von [llama.cpp / ggml](https://github.com/ggml-org/llama.cpp). Alle Danksagungen:
[docs/HOW_IT_WORKS.md](docs/HOW_IT_WORKS.md#credits). Strata ist Open Source unter der [MIT-Lizenz](LICENSE). Ein
paar Teile und alle Modelle haben eigene Lizenzen ([welche](docs/HOW_IT_WORKS.md#license)).
<a id="using-it"></a>
## Tools
**Claude Code · Codex CLI · OpenCode · oh-my-pi · pi.dev · Hermes Agent · OpenClaw**, und weitere über die OpenAI- und Anthropic-kompatiblen APIs.
Führe **`strata connect`** aus, um die Einrichtung für dein Tool zu sehen. [Verbindungsanleitung](docs/TERMINAL_LAUNCH.md#connect-your-apps-strata-connect).
Strata ist kostenlos und Open Source. Wenn es dir nützt, kannst du die Entwicklung unterstützen:
**Hilf mit, lokale KI für alle offen zu halten.** Ein Kaffee hilft, Entwicklung und Tests auf mehr Hardware zu finanzieren.
<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: Strata unterstützen" height="48"></a>
[MIT-Lizenz](LICENSE) · [Danksagung und Modelllizenzen](docs/HOW_IT_WORKS.md#credits) · [Datenschutz](docs/SAFETY.md)
<p align="center"><a href="https://github.com/Niko1221/Strata/releases/download/v0.1.10/Pagoda.mp4"><img src="docs/media/pagoda-preview.webp" width="720" alt="Un jardín con una pagoda de vóxeles que escribió el modelo de Strata, funcionando en el navegador"></a><br>
<sub>Un jardín con una pagoda de vóxeles, hecho con un solo prompt en una RTX 5070 con Strata (IQ3_S, contexto de 128K) ·
<a href="https://buymeacoffee.com/strataengine"><img src="https://img.shields.io/badge/Buy_me_a_coffee-FFDD00?logo=buymeacoffee&logoColor=000" alt="Apoya Strata en Buy Me a Coffee"></a>
</p>
Strata ejecuta **[Qwen3.8-Flash-Next](https://huggingface.co/Qwen/Qwen3.8-Flash-Next)** en un PC normal. Es un
modelo de IA grande e inteligente que normalmente necesita un servidor. Conversa, escribe código, entiende imágenes
y funciona con tus aplicaciones y agentes de programación. Nada sale de tu PC.
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
| | |
| Proveedor | Opciones |
| --- | --- |
| **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. |
En la **aplicación de escritorio** y en el **Terminal**. Lo que no cabe en la memoria se lee desde tu SSD, así que hace falta mucho espacio libre en disco.
| Conjunto (IST-DAS) | Descarga | Espacio libre necesario |
| --- | ---: | ---: |
| 1.75 bpw | 239 GB | unos 245 GB |
| 2.00 bpw | 257 GB | unos 265 GB |
| 2.25 bpw | 274 GB | unos 280 GB |
Funciona en NVIDIA RTX 30, 40 y 50 (Windows y Linux) y en AMD Radeon AI PRO R9700 (Linux). La instalación descarga los archivos y el motor de DeepSeek, y después inicia el modelo.
Medido con el conjunto de 2.00 bpw en Strata 0.2.5. «Salida» es la velocidad de la respuesta; «Prompt» es la rapidez con la que Strata lee tu texto.
| Hardware | Salida | Prompt |
| --- | ---: | ---: |
| RTX 5070 12 GB, 64 GB de RAM | 4 a 6 tokens/s | unos 110 tokens/s (4K) |
| RTX 3060 12 GB, 46 GB de RAM | 9 a 11 tokens/s | unos 155 tokens/s (4K) |
| 1x Radeon AI PRO R9700, 251 GB de RAM | 21 a 25 tokens/s | 320 a 360 tokens/s (8K) |
| 4x Radeon AI PRO R9700 | 74 a 77 tokens/s | 400 a 450 tokens/s (8K) |
[Memoria, disco y detalles](docs/DEEPSEEK41.md) · [Notas de la versión](https://github.com/Niko1221/Strata/releases)
</details>
<a id="the-other-sizes"></a>
¿No sabes qué cabe? [Elige un modelo para tu ordenador](docs/MODELS.md#pick-by-memory).
## Instalación
### Deja que tu IA lo instale
<a id="option-a-the-strata-app"></a>
¿Usas un asistente de programación con IA (Claude Code, Cursor, Codex, GitHub Copilot, ...)? Pega esto en él:
### Aplicación de escritorio
```text
Set up Strata on this PC for me: https://github.com/Niko1221/Strata - follow docs/AI_SETUP.md in that repository.
**[Descargar Strata para Windows, Linux o Mac →](https://github.com/Niko1221/Strata/releases/latest)**
Instala la aplicación, elige un modelo y empieza a chatear. Hay un instalador por sistema. En el primer arranque, la aplicación descarga el motor para tu tarjeta gráfica. [Ayuda con el instalador](docs/INSTALL.md#the-app).
<img src="docs/media/app-chat.png" width="900" alt="Aplicación de escritorio de Strata con un chat y una vista previa del código en vivo, una junto a otra">
<br><sub>Chat, archivos y vista previa en vivo en una sola ventana.</sub>
</p>
<a id="option-b-the-terminal-one-line"></a>
### Aplicación de Terminal
¿Prefieres la terminal? Instala Strata y ejecuta **`strata launch`**.
<details open>
<summary><b>Comandos de instalación</b></summary>
**Linux / Mac**
```sh
curl -fsSL https://github.com/Niko1221/Strata/releases/latest/download/install.sh -o install.sh && sh install.sh
```
Revisa tu tarjeta gráfica, tu RAM y tu disco, y elige el modelo que cabe. Luego lo instala, lo arranca y te explica
cómo conectar tus aplicaciones. Las herramientas de IA también pueden instalar, arrancar y detener Strata con su
[servidor MCP](docs/MCP_SERVER.md).
**Windows PowerShell**
### O hazlo tú mismo
[Descarga Strata](https://github.com/Niko1221/Strata/archive/refs/heads/main.zip) y descomprímelo (o usa `git clone`).
**Windows:** haz doble clic en **`START-HERE.bat`**. **Linux:** ejecuta **`./setup.sh`** en la carpeta de Strata.
**Mac (Apple Silicon, M1 a M5, macOS 15 o posterior):** instala una vez las Command Line Tools de Apple (`xcode-select --install` en Terminal; no hace falta Xcode). Luego abre Terminal en la carpeta de Strata y ejecuta **`./setup.sh`**. Compila el motor de Mac en tu Mac (unos 7 minutos), hace las mismas preguntas, descarga el modelo y lo arranca. La próxima vez, `./setup.sh` arranca enseguida. Qué tamaño cabe en tu Mac y qué velocidad tiene: [docs/MAC.md](docs/MAC.md).
Los pasos son los mismos para NVIDIA y AMD. El instalador detecta tu tarjeta y prepara el motor adecuado para ella.
Te hace unas pocas preguntas:
- qué modelo y qué tamaño,
- cuánto contexto (cuánto texto tiene presente el modelo),
- si debe entender imágenes.
Pulsa Enter cada vez para usar la respuesta recomendada. Después descarga el modelo (unos 70 GB) y lo arranca. Si la
descarga se detiene, vuelve a ejecutarlo: sigue donde lo dejó. Tu navegador abre la aplicación de Strata en
`http://127.0.0.1:8080`.
> **Mientras el modelo arranca, tu PC puede ir lento o dejar de responder durante 1-3 minutos** (más la primera vez).
> Strata carga 35-55 GB en tu RAM y reserva una parte para la tarjeta gráfica. Es normal. Espera y no cierres la
> ventana. La ventana muestra lo que está haciendo Strata.
**La próxima vez**, vuelve a ejecutar `START-HERE.bat` (o `./setup.sh`). Arranca enseguida y no descarga nada dos
veces. Cierra su ventana para detener el modelo. `UPDATE.bat` (`./update.sh`) actualiza Strata sin arrancarlo.
Actualizaciones, Docker, varias tarjetas, dónde se guardan los archivos y todas las opciones: [docs/INSTALL.md](docs/INSTALL.md).
## ¿Qué modelo elijo?
El instalador te recomienda uno según tu RAM. El mismo modelo viene en varios tamaños, más o menos comprimidos. Los
tamaños pequeños son más rápidos. Los grandes son algo más inteligentes.
| Tu RAM | Elige | Por qué |
| --- | --- | --- |
| **32 GB** | **Coder** | cabe en 32 GB y está hecho para programar (con una tarjeta de 24 GB también funcionan Q2_0 e IQ2_XS) |
| **48 GB** | **IQ2_XS** (o Q2_0, el más rápido) | los tamaños más grandes no caben |
| **64 GB** | **IQ2_XS** (recomendado), o IQ3_XXS / IQ3_S | caben todos los tamaños; IQ3_S es el mejor y el más lento |
| **96 GB o más** | **IQ3_S**, o el UD-IQ4_XS de Unsloth (~4 bits) | espacio para los tamaños más grandes con todo lo demás abierto |
- **[Coder](docs/MODELS.md#coder):** una versión para programar a la que se le quitó la mitad de los expertos. Alcanza
el 91% de la puntuación SWE-bench Verified del modelo completo (medido por sus autores) y cabe en 32 GB de RAM. Es
más flojo fuera del código, también con chino y otros textos CJK (#438). Para eso, elige Q2_0, IQ2_XS o IQ3_S, que
conservan todos los expertos.
- **[Swift 1.5](docs/MODELS.md#swift-15):** un ajuste fino que piensa mucho menos tiempo antes de responder. Recibes
la respuesta antes, con más o menos la misma calidad.
- **[Unsloth UD-IQ4_XS](docs/MODELS.md#unsloth-ud-iq4_xs):** la versión de ~4 bits de Unsloth, entre IQ3_S y
UD-Q4_K_XL en calidad. Una descarga de 94 GB. Con menos de ~80 GB de RAM, Strata lee una parte desde el SSD
mientras responde, así que ahí es más lento (un SSD NVMe ayuda).
- **[Unsloth UD-Q4_K_XL, UD-Q5_K_XL, UD-Q6_K_XL y Q8_0](docs/MODELS.md#unsloths-files-ud-iq4_xs-to-q8_0):** de 4 a 8 bits, más cerca del modelo completo
(93-95 % el mismo token siguiente que Q8_0). Rápidos cuando todos los expertos caben en la RAM (~88 GB para
UD-Q4_K_XL hasta ~136 GB para Q8_0); con menos RAM, Strata lee una parte desde el SSD mientras responde, lo que es
mucho más lento (7-8.5 tokens/s para UD-Q4_K_XL en un PC con 64 GB).
- **[OrcaRouter's Uncensored IQ3_XXS](docs/MODELS.md#orcarouter-uncensored-iq3_xxs):** se instala a mano. No está en
el menú del instalador.
Tamaños, descargas y qué cabe dónde: [docs/MODELS.md](docs/MODELS.md). Para añadir otro modelo más adelante, ejecuta
`SETUP.bat` (Linux: `./setup.sh --setup`).
## Cómo usarlo
<p align="center"><img src="docs/media/runpagoda.png" width="900" alt="La pestaña Monitor de la aplicación de Strata junto a un agente de programación"><br>
<sub>El <b>Monitor</b> de la aplicación de Strata (izquierda) mientras un agente de programación escribe el jardín de la pagoda del vídeo (derecha)</sub></p>
- **En el navegador:** abre `http://127.0.0.1:8080`. Tiene **Chat**, un **Monitor** en vivo del modelo y de tu
GPU/CPU/RAM, y **About** con los ajustes y las direcciones.
- **Tus aplicaciones y agentes de programación:** añade un proveedor "compatible con OpenAI" con la URL base
**`http://127.0.0.1:8080/v1`**. Sirve cualquier clave de API y cualquier nombre de modelo.
- Aplicaciones que usan la API de Anthropic: `http://127.0.0.1:8080/v1/messages` (Claude Code:
`ANTHROPIC_BASE_URL=http://127.0.0.1:8080`).
- Codex CLI y otras aplicaciones que usan la API Responses de OpenAI: `/v1/responses`
- **Razonamiento:** elige **off, low, medium o high** en el menú del chat o en el "reasoning effort" de tu aplicación.
Off es lo más rápido. High es lo mejor para preguntas difíciles.
- **Imágenes:** responde que sí a "Images?" en la instalación. Luego haz clic en **Picture** en el chat, o adjunta
imágenes en tu aplicación. Las tarjetas AMD leen imágenes en Linux a través del procesador; en Windows todavía no.
- **Desde tu móvil u otro PC:** `START-HERE.bat --setup --host 0.0.0.0 --api-key <secret>`. Pon siempre una clave.
- **Una petición cada vez:** por defecto Strata responde una petición y las demás esperan. Para responder varias a la
vez, pon `"parallel": 2` ([BATCHING.md](docs/BATCHING.md)). En una tarjeta de 12 GB, cada respuesta va más lenta.
- **Prompts largos:** Strata lee entero el primer mensaje de un chat, más o menos 1 minuto por cada 30,000 tokens. Los
mensajes siguientes empiezan en segundos.
Más: [dónde se guardan tus chats](docs/INSTALL.md#where-things-are-stored), [la API](docs/DETAILS.md#using-it).
### Opcional: más velocidad con una sola tarjeta cuando todo el modelo cabe en la RAM
Dos ajustes opcionales (de RubenTorney, #1904 y #1905). Ayudan si usas una tarjeta gráfica y tu RAM guarda todos los expertos (el modo arena de Strata, por ejemplo Q2_0 o IQ3_XXS en un PC con 64 GB). Abre tu configuración `strata-<model>.json` en la carpeta de datos de Strata ([dónde](docs/INSTALL.md#where-things-are-stored)) y añade:
- **Generación más rápida:** `"--adapt-async", "1"` en la lista `"args"`. La caché de expertos se actualiza entonces en un hilo auxiliar en vez de pausar la respuesta. Medido en una RTX 5070 (Windows, Q2_0): +4-5 % de generación, +7-10 % en código y documentos largos. Las respuestas pueden diferir un poco de las por defecto (son iguales de una ejecución a otra).
- **Lectura del prompt más rápida en Windows:** `"STRATA_PIN_BY_NEED": "1"` en el bloque `"env"`. Windows solo puede bloquear cerca de la mitad de la RAM para la tarjeta; esto bloquea primero los expertos que la tarjeta necesita de verdad. Medido en la RTX 5070: +4,4 % en la lectura del prompt.
Reinicia Strata (`START-HERE.bat` o `./setup.sh`) después del cambio. Para deshacerlo, quita las dos entradas.
En Windows, abre una terminal nueva después. Luego:
## ¿Algo salió mal?
```sh
strata launch
```
- **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.
Sigue las indicaciones para descargar e iniciar un modelo. [Guía del Terminal](docs/TERMINAL_LAUNCH.md).
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).
</details>
## ¿Cómo funciona?
<p align="center">
<img src="docs/media/terminal-monitor.png" width="900" alt="Aplicación de Terminal de Strata con la actividad del modelo, la velocidad, la memoria y las peticiones recientes">
<br><sub>Observa tu modelo y tu hardware mientras se ejecuta.</sub>
</p>
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.
<a id="build-from-source"></a>
<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>
[Compilar desde el código fuente](docs/SOURCE_INSTALL.md) · [Docker](docs/DOCKER.md) · [Guía de instalación completa](docs/INSTALL.md)
- **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.
<a id="what-you-need"></a>
<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>
## Hardware
- **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.
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)).
**Claude Code · Codex CLI · OpenCode · oh-my-pi · pi.dev · Hermes Agent · OpenClaw**, y más, mediante las API compatibles con OpenAI y Anthropic.
## Apoya a Strata
Ejecuta **`strata connect`** para ver las instrucciones de tu herramienta. [Guía de conexión](docs/TERMINAL_LAUNCH.md#connect-your-apps-strata-connect).
Strata es libre y de código abierto. Si te resulta útil, puedes apoyar su desarrollo:
<a id="guides"></a>
<a id="how-fast-is-it"></a>
<a id="how-does-it-work"></a>
<a id="something-went-wrong"></a>
<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>
## Ayuda
[Todas las guías](docs) · [Solución de problemas](docs/TROUBLESHOOTING.md) · [Informar de un error](https://github.com/Niko1221/Strata/issues/new/choose) ·
**Ayuda a mantener la IA local abierta a todos.** Un café ayuda a financiar el desarrollo y las pruebas en más hardware.
<a href="https://buymeacoffee.com/strataengine"><img src="https://cdn.buymeacoffee.com/buttons/v2/default-yellow.png" alt="Buy Me a Coffee: apoya Strata" height="48"></a>
[Licencia MIT](LICENSE) · [Créditos y licencias de los modelos](docs/HOW_IT_WORKS.md#credits) · [Privacidad](docs/SAFETY.md)
<a id="option-c-let-your-ai-set-it-up"></a>
<sub>Para herramientas de IA y lectores automáticos: <a href="AGENTS.md">referencia técnica</a> · <a href="docs/AI_SETUP.md">instrucciones de instalación</a>.</sub>
<p align="center"><a href="https://github.com/Niko1221/Strata/releases/download/v0.1.10/Pagoda.mp4"><img src="docs/media/pagoda-preview.webp" width="720" alt="Un jardin de pagode en voxels écrit par le modèle de Strata, qui tourne dans le navigateur"></a><br>
<sub>Un jardin de pagode en voxels, généré en une seule requête sur une RTX 5070 avec Strata (IQ3_S, contexte de 128K) ·
<a href="https://buymeacoffee.com/strataengine"><img src="https://img.shields.io/badge/Buy_me_a_coffee-FFDD00?logo=buymeacoffee&logoColor=000" alt="Soutenir Strata sur Buy Me a Coffee"></a>
</p>
Strata fait tourner **[Qwen3.8-Flash-Next](https://huggingface.co/Qwen/Qwen3.8-Flash-Next)** sur un PC normal. C'est
un grand modèle d'IA, très capable, qui demande d'habitude un serveur. Il discute, écrit du code, lit des images et
fonctionne avec vos applications et vos agents de code. Rien ne quitte votre PC.
**Gratuit, open source, et à vous de l'exécuter.** Utilisez Strata sur votre bureau ou avec vos outils de programmation préférés. Aucun compte nécessaire.
Nous l'avons mesurée sur deux PC de jeu ordinaires. Un token correspond à environ ¾ d'un mot.
## Modèles
- **É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).
Nous recommandons **d'abord ISTA-DASLab**, puis **Unsloth** et d'autres fournisseurs, lorsqu'un modèle compatible tient en mémoire.
<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>
| Taille | Écrit les réponses | Lit votre prompt |
| --- | ---: | ---: |
| **Q2_0** | 94 tokens/s | 2,650 tokens/s |
| **IQ2_XS** | 79 tokens/s | 2,090 tokens/s |
| **IQ3_XXS** | 62 tokens/s | 1,750 tokens/s |
| **IQ3_S** | 53 tokens/s | 1,620 tokens/s |
| **Coder** | 55 tokens/s | 2,180 tokens/s |
</td><td>
| Taille | Écrit les réponses | Lit votre prompt |
| --- | ---: | ---: |
| **Q2_0** | 60 tokens/s | 1,160 tokens/s |
| **IQ2_XS** | 52 tokens/s | 1,110 tokens/s |
| **Coder** | 44 tokens/s | 1,420 tokens/s |
</td></tr>
</table>
NVIDIA : Q2_0 avec le moteur 0.1.36, les autres lignes avec 0.1.26 (réponses de 4K, prompts de 32K). Les tableaux
complets sont dans [DETAILS.md](docs/DETAILS.md#speed-measured). Une carte avec plus de VRAM va plus vite : une
RTX 3090 (24 Go) devrait écrire environ 100-140 tokens par seconde. Longues discussions et autres cartes :
[vitesse de chaque modèle](docs/MODELS.md#how-fast-is-each-size), [résultats de la communauté](docs/COMMUNITY_BENCHMARKS.md).
<p align="center"><a href="https://buymeacoffee.com/strataengine"><img src="https://cdn.buymeacoffee.com/buttons/v2/default-yellow.png" alt="Buy Me A Coffee" height="50"></a><br>
<sub>Strata est gratuit. S'il tourne bien sur votre PC, un café aide à poursuivre le travail.</sub></p>
## Ce qu'il vous faut
| | |
| Fournisseur | Choix |
| --- | --- |
| **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. |
| **Gyro · agentionai** | [Gyro-S, Gyro-M](https://huggingface.co/agentionai/Qwen3.8-Flash-Next-Gyro-GGUF). Utilise moins de mémoire, mais plus lent. |
Dans l'**application de bureau** et dans le **Terminal**. Ce qui ne tient pas en mémoire est lu depuis votre SSD : il faut donc beaucoup d'espace disque libre.
Fonctionne sur les NVIDIA RTX 30, 40 et 50 (Windows et Linux) et sur l'AMD Radeon AI PRO R9700 (Linux). L'installation télécharge les fichiers et le moteur DeepSeek, puis démarre le modèle.
Mesuré avec le jeu 2.00 bpw sur Strata 0.2.5. « Sortie » est la vitesse de la réponse, « Prompt » la vitesse à laquelle Strata lit votre texte.
| Matériel | Sortie | Prompt |
| --- | ---: | ---: |
| RTX 5070 12 GB, 64 GB de RAM | 4 à 6 tokens/s | environ 110 tokens/s (4K) |
| RTX 3060 12 GB, 46 GB de RAM | 9 à 11 tokens/s | environ 155 tokens/s (4K) |
| 1x Radeon AI PRO R9700, 251 GB de RAM | 21 à 25 tokens/s | 320 à 360 tokens/s (8K) |
| 4x Radeon AI PRO R9700 | 74 à 77 tokens/s | 400 à 450 tokens/s (8K) |
[Mémoire, disque et détails](docs/DEEPSEEK41.md) · [Notes de version](https://github.com/Niko1221/Strata/releases)
</details>
<a id="the-other-sizes"></a>
Vous ne savez pas ce qui tient ? [Choisir un modèle pour votre ordinateur](docs/MODELS.md#pick-by-memory).
## Installation
### Laissez votre IA l'installer
<a id="option-a-the-strata-app"></a>
Vous utilisez un assistant de code IA (Claude Code, Cursor, Codex, GitHub Copilot, ...) ? Collez-lui ceci :
### Application de bureau
```text
Set up Strata on this PC for me: https://github.com/Niko1221/Strata - follow docs/AI_SETUP.md in that repository.
**[Télécharger Strata pour Windows, Linux ou Mac →](https://github.com/Niko1221/Strata/releases/latest)**
Installez l'application, choisissez un modèle et commencez à discuter. Il y a un seul installateur par système. Au premier lancement, l'application télécharge le moteur de votre carte graphique. [Aide pour l'installateur](docs/INSTALL.md#the-app).
curl -fsSL https://github.com/Niko1221/Strata/releases/latest/download/install.sh -o install.sh && sh install.sh
```
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).
**Windows PowerShell**
### Ou faites-le vous-même
[Téléchargez Strata](https://github.com/Niko1221/Strata/archive/refs/heads/main.zip) et décompressez-le (ou faites un `git clone`).
**Windows :** double-cliquez sur **`START-HERE.bat`**. **Linux :** lancez **`./setup.sh`** dans le dossier Strata.
**Mac (Apple Silicon, M1 à M5, macOS 15 ou plus récent) :** installez une fois les Command Line Tools d'Apple (`xcode-select --install` dans le Terminal ; Xcode lui-même n'est pas nécessaire). Ouvrez ensuite le Terminal dans le dossier Strata et lancez **`./setup.sh`**. Il 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` démarre tout de suite. Quelle taille convient à votre Mac et sa vitesse : [docs/MAC.md](docs/MAC.md).
Les étapes sont les mêmes pour NVIDIA et AMD. L'installateur détecte votre carte et installe le bon moteur pour elle.
Il vous pose quelques questions :
- quel modèle et quelle taille,
- combien de contexte (la quantité de texte que le modèle garde en tête),
- s'il doit lire les images.
Appuyez sur Entrée à chaque fois pour garder la réponse recommandée. Ensuite il télécharge le modèle (environ 70 Go)
et le démarre. Si le téléchargement s'arrête, relancez-le : il reprend là où il s'était arrêté. Votre navigateur
ouvre l'application Strata à l'adresse `http://127.0.0.1:8080`.
> **Pendant le démarrage du modèle, votre PC peut ralentir ou ne plus répondre pendant 1-3 minutes** (plus longtemps la première fois).
> Strata charge 35-55 Go dans votre RAM et en réserve une partie pour la carte graphique. C'est normal. Attendez, et
> ne fermez pas la fenêtre. La fenêtre montre ce que fait Strata.
**La fois suivante**, relancez `START-HERE.bat` (ou `./setup.sh`). Il démarre tout de suite et ne télécharge rien
deux fois. Fermez sa fenêtre pour arrêter le modèle. `UPDATE.bat` (`./update.sh`) met Strata à jour sans le démarrer.
Mises à jour, Docker, plusieurs cartes, emplacement des fichiers et toutes les options : [docs/INSTALL.md](docs/INSTALL.md).
## Quel modèle choisir ?
L'installateur en recommande un selon votre RAM. Le même modèle existe en plusieurs tailles, plus ou moins
compressées. Les petites tailles sont plus rapides. Les grandes sont un peu plus intelligentes.
| Votre RAM | Prenez | Pourquoi |
| --- | --- | --- |
| **32 Go** | **Coder** | il tient dans 32 Go et il est fait pour le code (avec une carte de 24 Go, Q2_0 et IQ2_XS tournent aussi) |
| **48 Go** | **IQ2_XS** (ou Q2_0, le plus rapide) | les plus grandes tailles ne tiennent pas |
| **64 Go** | **IQ2_XS** (recommandé), ou IQ3_XXS / IQ3_S | toutes les tailles tiennent ; IQ3_S est le meilleur et le plus lent |
| **96 Go ou plus** | **IQ3_S**, ou l'UD-IQ4_XS d'Unsloth (~4 bits) | de la place pour les plus grandes tailles, avec tout le reste ouvert |
- **[Coder](docs/MODELS.md#coder) :** une version pour le code, avec la moitié des experts en moins. Elle atteint 91 %
du score SWE-bench Verified du modèle complet (mesuré par ses auteurs) et tient dans 32 Go de RAM. Elle est moins
bonne en dehors du code, y compris pour le chinois et les autres textes CJK (#438). Pour ces cas, prenez Q2_0,
IQ2_XS ou IQ3_S, qui gardent tous les experts.
- **[Swift 1.5](docs/MODELS.md#swift-15) :** un fine-tune qui réfléchit bien moins longtemps avant de répondre. Vous
avez la réponse plus tôt, avec à peu près la même qualité.
- **[Unsloth UD-IQ4_XS](docs/MODELS.md#unsloth-ud-iq4_xs)** : la version ~4 bits d'Unsloth, entre IQ3_S et
UD-Q4_K_XL en qualité. Un téléchargement de 94 Go. Avec moins de ~80 Go de RAM, Strata en lit une partie depuis
le SSD pendant qu'il répond, il y est donc plus lent (un SSD NVMe aide).
- **[Unsloth UD-Q4_K_XL, UD-Q5_K_XL, UD-Q6_K_XL et Q8_0](docs/MODELS.md#unsloths-files-ud-iq4_xs-to-q8_0) :** de 4 à 8 bits, plus proches du modèle
complet (93-95 % le même token suivant que Q8_0). Rapides quand tous les experts tiennent dans la RAM (~88 Go pour
UD-Q4_K_XL jusqu'à ~136 Go pour Q8_0) ; avec moins de RAM, Strata en lit une partie depuis le SSD pendant qu'il
répond, ce qui est bien plus lent (7-8.5 tokens/s pour UD-Q4_K_XL sur un PC avec 64 Go).
- **[OrcaRouter's Uncensored IQ3_XXS](docs/MODELS.md#orcarouter-uncensored-iq3_xxs) :** vous l'installez à la main.
Il n'est pas dans le menu de l'installateur.
Tailles, téléchargements et ce qui tient où : [docs/MODELS.md](docs/MODELS.md). Pour ajouter un autre modèle plus
- **Réflexion :** choisissez **off, low, medium ou high** dans le menu du chat ou dans le « reasoning effort » de
votre application. Off est le plus rapide. High est le meilleur pour les questions difficiles.
- **Images :** répondez oui à « Images? » pendant l'installation. Ensuite cliquez sur **Picture** dans le chat, ou
joignez des images dans votre application. Les cartes AMD lisent les images sous Linux via le processeur ; sous
Windows, pas encore.
- **Depuis votre téléphone ou un autre PC :** `START-HERE.bat --setup --host 0.0.0.0 --api-key <secret>`. Définissez
toujours une clé.
- **Une requête à la fois :** par défaut, Strata répond à une requête et les autres attendent. Pour répondre à
plusieurs en même temps, mettez `"parallel": 2` ([BATCHING.md](docs/BATCHING.md)). Sur une carte de 12 Go, chaque
réponse devient alors plus lente.
- **Longs prompts :** Strata lit en entier le premier message d'une discussion, environ 1 minute pour 30,000 tokens.
Les messages suivants démarrent en quelques secondes.
Plus d'infos : [où sont stockées vos discussions](docs/INSTALL.md#where-things-are-stored), [l'API](docs/DETAILS.md#using-it).
### Optionnel : plus de vitesse sur une seule carte quand tout le modèle tient en RAM
Deux réglages optionnels (de RubenTorney, #1904 et #1905). Ils aident si vous utilisez une seule carte graphique et que votre RAM contient tous les experts (le mode arena de Strata, par exemple Q2_0 ou IQ3_XXS sur un PC de 64 Go). Ouvrez votre configuration `strata-<model>.json` dans le dossier de données de Strata ([où](docs/INSTALL.md#where-things-are-stored)) et ajoutez :
- **Génération plus rapide :** `"--adapt-async", "1"` dans la liste `"args"`. Le cache d'experts est alors mis à jour dans un thread auxiliaire au lieu de mettre la réponse en pause. Mesuré sur une RTX 5070 (Windows, Q2_0) : +4-5 % de génération, +7-10 % sur le code et les longs documents. Les réponses peuvent légèrement différer de celles par défaut (elles restent identiques d'une exécution à l'autre).
- **Lecture du prompt plus rapide sous Windows :** `"STRATA_PIN_BY_NEED": "1"` dans le bloc `"env"`. Windows ne peut verrouiller qu'environ la moitié de la RAM pour la carte ; ce réglage verrouille d'abord les experts dont la carte a vraiment besoin. Mesuré sur la RTX 5070 : +4,4 % en lecture du prompt.
Redémarrez Strata (`START-HERE.bat` ou `./setup.sh`) après la modification. Pour annuler, retirez les deux entrées.
Sous Windows, ouvrez ensuite un nouveau terminal. Puis :
## Un problème ?
```sh
strata launch
```
- **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.
Suivez les invites pour télécharger et démarrer un modèle. [Guide du Terminal](docs/TERMINAL_LAUNCH.md).
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).
</details>
## Comment ça marche ?
<p align="center">
<img src="docs/media/terminal-monitor.png" width="900" alt="Application Terminal de Strata affichant l'activité du modèle, la vitesse, la mémoire et les requêtes récentes">
<br><sub>Suivez votre modèle et votre matériel pendant qu'il tourne.</sub>
</p>
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.
<a id="build-from-source"></a>
<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>
[Compiler depuis les sources](docs/SOURCE_INSTALL.md) · [Docker](docs/DOCKER.md) · [Guide d'installation complet](docs/INSTALL.md)
- **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.
<a id="what-you-need"></a>
<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>
## Matériel
- **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.
| **Apple Silicon · M1 ou plus récent** | [Installation sur Mac](docs/MAC.md) |
| **AMD Ryzen AI Max · Strix Halo** | [Installation Strix Halo](docs/STRIX_HALO.md) |
L'explication plus longue : [docs/HOW_IT_WORKS.md](docs/HOW_IT_WORKS.md). Chaque partie et ses chiffres :
[les détails](docs/DETAILS.md#how-it-works) et l'[article](docs/paper/Strata-Paper.pdf).
<a id="using-it"></a>
## Crédits et licence
## Outils
Le modèle est [Qwen3.8-Flash-Next](https://huggingface.co/Qwen/Qwen3.8-Flash-Next), de l'équipe Qwen. Il a été
compressé par [ISTA-DASLab](https://huggingface.co/ISTA-DASLab/Qwen3.8-Flash-Next-GSQ-RCO-GGUF), UkisAI (Swift 1.5)
et Unsloth. Strata utilise des parties de [llama.cpp / ggml](https://github.com/ggml-org/llama.cpp). Tous les
crédits : [docs/HOW_IT_WORKS.md](docs/HOW_IT_WORKS.md#credits). Strata est open source sous [licence MIT](LICENSE).
Quelques parties et chaque modèle ont leur propre licence ([lesquelles](docs/HOW_IT_WORKS.md#license)).
**Claude Code · Codex CLI · OpenCode · oh-my-pi · pi.dev · Hermes Agent · OpenClaw**, et d'autres via les API compatibles OpenAI et Anthropic.
Lancez **`strata connect`** pour voir la configuration de votre outil. [Guide de connexion](docs/TERMINAL_LAUNCH.md#connect-your-apps-strata-connect).
<a id="guides"></a>
<a id="how-fast-is-it"></a>
<a id="how-does-it-work"></a>
<a id="something-went-wrong"></a>
## Aide
[Tous les guides](docs) · [Dépannage](docs/TROUBLESHOOTING.md) · [Signaler un bogue](https://github.com/Niko1221/Strata/issues/new/choose) ·
[Benchmarks](docs/COMMUNITY_BENCHMARKS.md) · [Comment ça marche](docs/HOW_IT_WORKS.md) · [Contribuer](CONTRIBUTING.md)
## Soutenir Strata
Strata est gratuit et open source. S'il vous est utile, vous pouvez soutenir son développement :
**Aidez à garder l'IA locale ouverte à tous.** Un café aide à financer le développement et les tests sur davantage de matériel.
<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 : soutenir Strata" height="48"></a>
[Licence MIT](LICENSE) · [Crédits et licences des modèles](docs/HOW_IT_WORKS.md#credits) · [Confidentialité](docs/SAFETY.md)
<a id="option-c-let-your-ai-set-it-up"></a>
<sub>Pour les outils d'IA et les lecteurs automatiques : <a href="AGENTS.md">référence technique</a> · <a href="docs/AI_SETUP.md">instructions d'installation</a>.</sub>
<a href="https://buymeacoffee.com/strataengine"><img src="https://img.shields.io/badge/Buy_me_a_coffee-FFDD00?logo=buymeacoffee&logoColor=000" alt="Buy Me a Coffee で Strata を支援する"></a>
</p>
Strata を使うと、**[Qwen3.8-Flash-Next](https://huggingface.co/Qwen/Qwen3.8-Flash-Next)** を一般的な PC で動かせます。
通常はサーバーを必要とする大規模で高性能な AI モデルで、チャットやコードの生成、画像の読み取りができます。
<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>
**ローカル AI をみんなに開かれたものに。** コーヒー1杯が、開発と、より多くのハードウェアでのテストの支えになります。
<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: Strata を支援する" height="48"></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>
<a href="https://buymeacoffee.com/strataengine"><img src="https://img.shields.io/badge/Buy_me_a_coffee-FFDD00?logo=buymeacoffee&logoColor=000" alt="Support Strata on Buy Me a Coffee"></a>
</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
<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>
## What you need
| | |
| Provider | Choices |
| --- | --- |
| **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. |
<img src="docs/media/app-chat.png" width="900" alt="Strata desktop app with a chat and live code preview side by side">
<br><sub>Chat, files and a live preview in one window.</sub>
</p>
<a id="option-b-the-terminal-one-line"></a>
### Terminal app
Prefer the terminal? Install Strata, then run **`strata launch`**.
<details open>
<summary><b>Install commands</b></summary>
**Linux / Mac**
```sh
curl -fsSL https://github.com/Niko1221/Strata/releases/latest/download/install.sh -o install.sh && sh install.sh
```
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).
**Windows PowerShell**
### Or do it yourself
[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, M1 to M5, macOS 15 or newer):** install Apple's Command Line Tools once
(`xcode-select --install` in Terminal; Xcode itself is not needed). Then 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 and how fast it is:
[docs/MAC.md](docs/MAC.md).
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:
- which model and which size,
- how much context (how much text the model keeps in mind),
- whether it should read pictures.
Press Enter each time for the recommended answer. Then it downloads the model (about 70 GB) and starts it. If the
download stops, run it again: it continues where it left off. Your browser opens the Strata app at
`http://127.0.0.1:8080`.
> **While the model starts, your PC can be slow or stop responding for 1-3 minutes** (longest the first time).
> Strata loads 35-55 GB into your RAM and locks part of it for the graphics card. This is normal. Wait, and don't
> close the window. The window shows what Strata is doing.
**Next time**, run `START-HERE.bat` (or `./setup.sh`) again. It starts right away and downloads nothing twice. Close
its window to stop the model. `UPDATE.bat` (`./update.sh`) updates Strata without starting it. Updating, Docker,
several cards, where the files go and every option: [docs/INSTALL.md](docs/INSTALL.md).
**Is it fast on your PC?** Run `BENCH.bat` (`./bench.sh`): about 5-10 minutes, it measures chat, 4K/16K prompts and an agent-style session with your own settings and writes a report to share ([how](docs/COMMUNITY_BENCHMARKS.md#run-the-benchmark)).
## Which model should I pick?
The installer recommends one for your RAM. The same model comes in several sizes, compressed more or less. Smaller
sizes are faster. Larger sizes are a bit smarter.
| Your RAM | Take | Why |
| --- | --- | --- |
| **32 GB** | **Coder** | it fits 32 GB, and it is made for code (with a 24 GB card, Q2_0 and IQ2_XS run too) |
| **48 GB** | **IQ2_XS** (or Q2_0, the fastest) | the larger sizes do not fit |
| **64 GB** | **IQ2_XS** (recommended), or IQ3_XXS / IQ3_S | every size fits; IQ3_S is the best and the slowest |
| **96 GB or more** | **IQ3_S**, or Unsloth's UD-IQ4_XS (~4-bit) | room for the largest sizes with everything else open |
- **[Coder](docs/MODELS.md#coder):** a coding version with half of the experts removed. 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, UD-Q5_K_XL, UD-Q6_K_XL and Q8_0](docs/MODELS.md#unsloths-files-ud-iq4_xs-to-q8_0):**
4- to 8-bit, closer to the full model (93-95% the same next token as Q8_0). Fast where every expert fits in RAM
(~88 GB for UD-Q4_K_XL up to ~136 GB for Q8_0); with less RAM Strata reads part of them from the SSD while it
answers, which is much slower (7-8.5 tokens/s for UD-Q4_K_XL on a 64 GB PC).
- **[OrcaRouter's Uncensored IQ3_XXS](docs/MODELS.md#orcarouter-uncensored-iq3_xxs):** you set it up by hand. It is
not in the installer's menu.
Sizes, downloads and what fits where: [docs/MODELS.md](docs/MODELS.md). To add another model later, run
`SETUP.bat` (Linux: `./setup.sh --setup`).
## Using it
<p align="center"><img src="docs/media/runpagoda.png" width="900" alt="The Strata app's Monitor tab next to a coding agent"><br>
<sub>The Strata app's <b>Monitor</b> (left) while a coding agent writes the pagoda garden from the video (right)</sub></p>
- **In the browser:** open `http://127.0.0.1:8080`. It has **Chat**, a live **Monitor** of the model and your
GPU/CPU/RAM, and **About** with the settings and addresses.
- **Your apps and coding agents:** add an "OpenAI-compatible" provider with the base URL
**`http://127.0.0.1:8080/v1`**. Any API key and any model name work.
- Apps that use Anthropic's API: `http://127.0.0.1:8080/v1/messages` (Claude Code:
`ANTHROPIC_BASE_URL=http://127.0.0.1:8080`).
- Codex CLI and other apps that use the OpenAI Responses API: `/v1/responses`
Restart Strata (`START-HERE.bat` or `./setup.sh`) after the change. To undo, remove the two entries.
On Windows, open a new terminal afterwards. Then:
## Something went wrong?
```sh
strata launch
```
- **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.
Follow the prompts to download and start a model. [Terminal guide](docs/TERMINAL_LAUNCH.md).
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).
</details>
## How does it work?
<p align="center">
<img src="docs/media/terminal-monitor.png" width="900" alt="Strata terminal app showing model activity, speed, memory and recent requests">
<br><sub>Watch your model and hardware while it runs.</sub>
</p>
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.
<a id="build-from-source"></a>
<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>
[Build from source](docs/SOURCE_INSTALL.md) · [Docker](docs/DOCKER.md) · [Full install guide](docs/INSTALL.md)
- **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 id="what-you-need"></a>
<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>
## Hardware
- **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.
| **AMD Ryzen AI Max · Strix Halo** | [Strix Halo setup](docs/STRIX_HALO.md) |
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).
<a id="using-it"></a>
## Credits and license
## Tools
The model is [Qwen3.8-Flash-Next](https://huggingface.co/Qwen/Qwen3.8-Flash-Next) by the Qwen team. It was
compressed by [ISTA-DASLab](https://huggingface.co/ISTA-DASLab/Qwen3.8-Flash-Next-GSQ-RCO-GGUF), UkisAI (Swift 1.5)
and Unsloth. Strata uses parts of [llama.cpp / ggml](https://github.com/ggml-org/llama.cpp). All credits:
[docs/HOW_IT_WORKS.md](docs/HOW_IT_WORKS.md#credits). Strata is open source under the [MIT License](LICENSE). A few
parts and every model have their own licenses ([which ones](docs/HOW_IT_WORKS.md#license)).
**Claude Code · Codex CLI · OpenCode · oh-my-pi · pi.dev · Hermes Agent · OpenClaw**, and more through the OpenAI- and Anthropic-compatible APIs.
Run **`strata connect`** to see setup instructions for your tool. [Connection guide](docs/TERMINAL_LAUNCH.md#connect-your-apps-strata-connect).
<a id="guides"></a>
<a id="how-fast-is-it"></a>
<a id="how-does-it-work"></a>
<a id="something-went-wrong"></a>
## Help
[All guides](docs) · [Troubleshooting](docs/TROUBLESHOOTING.md) · [Report a bug](https://github.com/Niko1221/Strata/issues/new/choose) ·
[Benchmarks](docs/COMMUNITY_BENCHMARKS.md) · [How it works](docs/HOW_IT_WORKS.md) · [Contribute](CONTRIBUTING.md)
## Support Strata
Strata is free and open source. If it is useful to you, you can support its development:
**Help keep local AI open to everyone.** A coffee helps fund development and testing on more hardware.
<a href="https://buymeacoffee.com/strataengine"><img src="https://cdn.buymeacoffee.com/buttons/v2/default-yellow.png" alt="Buy Me a Coffee: support Strata" height="48"></a>
[MIT license](LICENSE) · [Credits and model licenses](docs/HOW_IT_WORKS.md#credits) · [Privacy](docs/SAFETY.md)
<a id="option-c-let-your-ai-set-it-up"></a>
<sub>For AI tools and automated readers: <a href="AGENTS.md">technical reference</a> · <a href="docs/AI_SETUP.md">setup instructions</a>.</sub>
<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://img.shields.io/badge/Buy_me_a_coffee-FFDD00?logo=buymeacoffee&logoColor=000" alt="Wesprzyj Stratę na Buy Me a Coffee"></a>
**Bezpłatna, z otwartym kodem źródłowym i do uruchomienia u siebie.** Używaj Straty na swoim komputerze albo razem z ulubionymi narzędziami do programowania. Konto nie jest potrzebne.
## Modele
Najpierw polecamy **ISTA-DASLab**, potem **Unsloth** i innych dostawców — o ile zgodny model mieści się w pamięci.
| **Gyro · agentionai** | [Gyro-S, Gyro-M](https://huggingface.co/agentionai/Qwen3.8-Flash-Next-Gyro-GGUF). Zużywa mniej pamięci, ale działa wolniej. |
W Windows otwórz potem nowe okno terminala. Następnie:
```sh
strata launch
```
Postępuj zgodnie z podpowiedziami, aby pobrać i uruchomić model. [Przewodnik po Terminalu](docs/TERMINAL_LAUNCH.md).
</details>
<p align="center">
<img src="docs/media/terminal-monitor.png" width="900" alt="Aplikacja Terminal Straty pokazująca aktywność modelu, szybkość, pamięć i ostatnie zapytania">
<br><sub>Obserwuj model i sprzęt w trakcie pracy.</sub>
</p>
<a id="build-from-source"></a>
[Budowanie ze źródeł](docs/SOURCE_INSTALL.md) · [Docker](docs/DOCKER.md) · [Pełny przewodnik instalacji](docs/INSTALL.md)
| **AMD Ryzen AI Max · Strix Halo** | [Konfiguracja Strix Halo](docs/STRIX_HALO.md) |
<a id="using-it"></a>
## Narzędzia
**Claude Code · Codex CLI · OpenCode · oh-my-pi · pi.dev · Hermes Agent · OpenClaw** i inne — przez API zgodne z OpenAI i Anthropic.
Uruchom **`strata connect`**, aby zobaczyć instrukcję konfiguracji dla swojego narzędzia. [Przewodnik po połączeniach](docs/TERMINAL_LAUNCH.md#connect-your-apps-strata-connect).
[Testy wydajności](docs/COMMUNITY_BENCHMARKS.md) · [Jak to działa](docs/HOW_IT_WORKS.md) · [Współtworzenie](CONTRIBUTING.md)
## Wesprzyj Stratę
**Pomóż zachować lokalną sztuczną inteligencję otwartą dla wszystkich.** Kawa pomaga finansować rozwój i testy na większej liczbie sprzętu.
<a href="https://buymeacoffee.com/strataengine"><img src="https://cdn.buymeacoffee.com/buttons/v2/default-yellow.png" alt="Buy Me a Coffee: wesprzyj Stratę" height="48"></a>
[Licencja MIT](LICENSE) · [Podziękowania i licencje modeli](docs/HOW_IT_WORKS.md#credits) · [Prywatność](docs/SAFETY.md)
<a id="option-c-let-your-ai-set-it-up"></a>
<sub>Dla narzędzi AI i automatycznych czytników: <a href="AGENTS.md">dokumentacja techniczna</a> · <a href="docs/AI_SETUP.md">instrukcja instalacji</a>.</sub>
<p align="center"><a href="https://github.com/Niko1221/Strata/releases/download/v0.1.10/Pagoda.mp4"><img src="docs/media/pagoda-preview.webp" width="720" alt="Um jardim com pagode em voxels que o modelo do Strata escreveu, rodando no navegador"></a><br>
<sub>Um jardim com pagode em voxels, feito com um único prompt numa RTX 5070 com o Strata (IQ3_S, contexto de 128K) ·
<a href="https://buymeacoffee.com/strataengine"><img src="https://img.shields.io/badge/Buy_me_a_coffee-FFDD00?logo=buymeacoffee&logoColor=000" alt="Apoie o Strata no Buy Me a Coffee"></a>
</p>
O Strata roda o **[Qwen3.8-Flash-Next](https://huggingface.co/Qwen/Qwen3.8-Flash-Next)** num PC comum. É um modelo
de IA grande e inteligente que normalmente precisa de um servidor. Ele conversa, escreve código, entende imagens e
funciona com seus apps e agentes de programação. Nada sai do seu PC.
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
| | |
| Provedor | Opções |
| --- | --- |
| **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. |
No **aplicativo de desktop** e no **Terminal**. O que não cabe na memória é lido do seu SSD, por isso é preciso muito espaço livre em disco.
| Conjunto (IST-DAS) | Download | Espaço livre necessário |
| --- | ---: | ---: |
| 1.75 bpw | 239 GB | cerca de 245 GB |
| 2.00 bpw | 257 GB | cerca de 265 GB |
| 2.25 bpw | 274 GB | cerca de 280 GB |
Roda em NVIDIA RTX 30, 40 e 50 (Windows e Linux) e em AMD Radeon AI PRO R9700 (Linux). A instalação baixa os arquivos e o motor do DeepSeek e depois inicia o modelo.
Medido com o conjunto de 2.00 bpw no Strata 0.2.5. “Saída” é a velocidade da resposta; “Prompt” é a rapidez com que o Strata lê o seu texto.
| Hardware | Saída | Prompt |
| --- | ---: | ---: |
| RTX 5070 12 GB, 64 GB de RAM | 4 a 6 tokens/s | cerca de 110 tokens/s (4K) |
| RTX 3060 12 GB, 46 GB de RAM | 9 a 11 tokens/s | cerca de 155 tokens/s (4K) |
| 1x Radeon AI PRO R9700, 251 GB de RAM | 21 a 25 tokens/s | 320 a 360 tokens/s (8K) |
| 4x Radeon AI PRO R9700 | 74 a 77 tokens/s | 400 a 450 tokens/s (8K) |
[Memória, disco e detalhes](docs/DEEPSEEK41.md) · [Notas da versão](https://github.com/Niko1221/Strata/releases)
</details>
<a id="the-other-sizes"></a>
Não sabe o que cabe? [Escolha um modelo para o seu computador](docs/MODELS.md#pick-by-memory).
## Instalação
### Deixe sua IA instalar
<a id="option-a-the-strata-app"></a>
Você usa um assistente de programação com IA (Claude Code, Cursor, Codex, GitHub Copilot, ...)? Cole isto nele:
### Aplicativo de desktop
```text
Set up Strata on this PC for me: https://github.com/Niko1221/Strata - follow docs/AI_SETUP.md in that repository.
**[Baixar o Strata para Windows, Linux ou Mac →](https://github.com/Niko1221/Strata/releases/latest)**
Instale o aplicativo, escolha um modelo e comece a conversar. Há um instalador por sistema. Na primeira vez que abre, o aplicativo baixa o motor da sua placa de vídeo. [Ajuda com o instalador](docs/INSTALL.md#the-app).
<img src="docs/media/app-chat.png" width="900" alt="Aplicativo de desktop do Strata com um chat e uma prévia do código ao vivo lado a lado">
<br><sub>Chat, arquivos e prévia ao vivo em uma só janela.</sub>
</p>
<a id="option-b-the-terminal-one-line"></a>
### Aplicativo de Terminal
Prefere o terminal? Instale o Strata e execute **`strata launch`**.
<details open>
<summary><b>Comandos de instalação</b></summary>
**Linux / Mac**
```sh
curl -fsSL https://github.com/Niko1221/Strata/releases/latest/download/install.sh -o install.sh && sh install.sh
```
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.
**Windows PowerShell**
### Ou faça você mesmo
[Baixe o Strata](https://github.com/Niko1221/Strata/archive/refs/heads/main.zip) e descompacte (ou use `git clone`).
**Windows:** clique duas vezes em **`START-HERE.bat`**. **Linux:** rode **`./setup.sh`** na pasta do Strata.
**Mac (Apple Silicon, M1 a M5, macOS 15 ou mais recente):** instale uma vez as Command Line Tools da Apple (`xcode-select --install` no Terminal; o Xcode em si não é necessário). Depois abra o Terminal na pasta do Strata e rode **`./setup.sh`**. Ele compila o motor do Mac no seu Mac (uns 7 minutos), faz as mesmas perguntas, baixa o modelo e o inicia. Da próxima vez, `./setup.sh` inicia na hora. Qual tamanho cabe no seu Mac e qual a velocidade: [docs/MAC.md](docs/MAC.md).
Os passos são os mesmos para NVIDIA e AMD. O instalador encontra sua placa e configura o engine certo para ela.
Ele faz algumas perguntas:
- qual modelo e qual tamanho,
- quanto contexto (quanto texto o modelo guarda na memória),
- se ele deve entender imagens.
Aperte Enter em cada uma para usar a resposta recomendada. Depois ele baixa o modelo (cerca de 70 GB) e o inicia.
Se o download parar, rode de novo: ele continua de onde parou. Seu navegador abre o app do Strata em
`http://127.0.0.1:8080`.
> **Enquanto o modelo inicia, seu PC pode ficar lento ou parar de responder por 1-3 minutos** (mais na primeira vez).
> O Strata carrega 35-55 GB na sua RAM e reserva parte dela para a placa de vídeo. Isso é normal. Espere e não
> feche a janela. A janela mostra o que o Strata está fazendo.
**Da próxima vez**, rode `START-HERE.bat` (ou `./setup.sh`) de novo. Ele inicia na hora e não baixa nada duas
vezes. Feche a janela para parar o modelo. `UPDATE.bat` (`./update.sh`) atualiza o Strata sem iniciá-lo.
Atualização, Docker, várias placas, onde ficam os arquivos e todas as opções: [docs/INSTALL.md](docs/INSTALL.md).
## Qual modelo devo escolher?
O instalador recomenda um de acordo com sua RAM. O mesmo modelo vem em vários tamanhos, mais ou menos comprimidos.
Os tamanhos menores são mais rápidos. Os maiores são um pouco mais inteligentes.
| Sua RAM | Escolha | Por quê |
| --- | --- | --- |
| **32 GB** | **Coder** | cabe em 32 GB e é feito para código (com uma placa de 24 GB, Q2_0 e IQ2_XS também rodam) |
| **48 GB** | **IQ2_XS** (ou Q2_0, o mais rápido) | os tamanhos maiores não cabem |
| **64 GB** | **IQ2_XS** (recomendado), ou IQ3_XXS / IQ3_S | todos os tamanhos cabem; IQ3_S é o melhor e o mais lento |
| **96 GB ou mais** | **IQ3_S**, ou o UD-IQ4_XS da Unsloth (~4-bit) | espaço para os maiores tamanhos com todo o resto aberto |
- **[Coder](docs/MODELS.md#coder):** uma versão para programação com metade dos experts removida. Ela alcança 91%
da pontuação do modelo completo no SWE-bench Verified (medido pelos autores) e cabe em 32 GB de RAM. É mais fraca
fora do código, inclusive em chinês e outros textos CJK (#438). Para isso, escolha Q2_0, IQ2_XS ou IQ3_S, que
mantêm todos os experts.
- **[Swift 1.5](docs/MODELS.md#swift-15):** um fine-tune que pensa por bem menos tempo antes de responder. Você
recebe a resposta mais cedo, com quase a mesma qualidade.
- **[Unsloth UD-IQ4_XS](docs/MODELS.md#unsloth-ud-iq4_xs):** a versão de ~4 bits da Unsloth, entre IQ3_S e
UD-Q4_K_XL em qualidade. Um download de 94 GB. Com menos de ~80 GB de RAM, o Strata lê uma parte dele do SSD
enquanto responde, então ali ele fica mais lento (um SSD NVMe ajuda).
- **[Unsloth UD-Q4_K_XL, UD-Q5_K_XL, UD-Q6_K_XL e Q8_0](docs/MODELS.md#unsloths-files-ud-iq4_xs-to-q8_0):** de 4 a 8 bits, mais próximos do modelo
completo (93-95% o mesmo próximo token que o Q8_0). Rápidos quando todos os especialistas cabem na RAM (~88 GB para
o UD-Q4_K_XL até ~136 GB para o Q8_0); com menos RAM, o Strata lê uma parte do SSD enquanto responde, o que é bem
mais lento (7-8.5 tokens/s para o UD-Q4_K_XL num PC com 64 GB).
- **[OrcaRouter's Uncensored IQ3_XXS](docs/MODELS.md#orcarouter-uncensored-iq3_xxs):** você configura à mão. Ele
não está no menu do instalador.
Tamanhos, downloads e o que cabe onde: [docs/MODELS.md](docs/MODELS.md). Para adicionar outro modelo depois, rode
`SETUP.bat` (Linux: `./setup.sh --setup`).
## Como usar
<p align="center"><img src="docs/media/runpagoda.png" width="900" alt="A aba Monitor do app Strata ao lado de um agente de programação"><br>
<sub>O <b>Monitor</b> do app Strata (à esquerda) enquanto um agente de programação escreve o jardim com pagode do vídeo (à direita)</sub></p>
- **No navegador:** abra `http://127.0.0.1:8080`. Lá tem o **Chat**, um **Monitor** ao vivo do modelo e da sua
GPU/CPU/RAM, e o **About** com as configurações e os endereços.
- **Seus apps e agentes de programação:** adicione um provedor "OpenAI-compatible" com a URL base
**`http://127.0.0.1:8080/v1`**. Qualquer chave de API e qualquer nome de modelo funcionam.
- Apps que usam a API da Anthropic: `http://127.0.0.1:8080/v1/messages` (Claude Code:
`ANTHROPIC_BASE_URL=http://127.0.0.1:8080`).
- Codex CLI e outros apps que usam a OpenAI Responses API: `/v1/responses`
- **Raciocínio:** escolha **off, low, medium ou high** no menu do chat ou no "reasoning effort" do seu app. Off é o
mais rápido. High é o melhor para perguntas difíceis.
- **Imagens:** responda sim para "Images?" na instalação. Depois clique em **Picture** no chat, ou anexe imagens no
seu app. Placas AMD entendem imagens no Linux usando o processador; no Windows, ainda não.
- **Do seu celular ou de outro PC:** `START-HERE.bat --setup --host 0.0.0.0 --api-key <secret>`. Sempre defina uma chave.
- **Uma requisição por vez:** por padrão, o Strata responde uma requisição e as outras esperam. Para responder
várias ao mesmo tempo, defina `"parallel": 2` ([BATCHING.md](docs/BATCHING.md)). Numa placa de 12 GB, isso deixa
cada resposta mais lenta.
- **Prompts longos:** o Strata lê a primeira mensagem de uma conversa inteira, cerca de 1 minuto a cada 30,000
tokens. As mensagens seguintes começam em segundos.
Mais: [onde suas conversas ficam salvas](docs/INSTALL.md#where-things-are-stored), [a API](docs/DETAILS.md#using-it).
### Opcional: mais velocidade com uma placa quando o modelo inteiro cabe na RAM
Duas configurações opcionais (de RubenTorney, #1904 e #1905). Elas ajudam quando você usa uma placa de vídeo e sua RAM guarda todos os experts (o modo arena do Strata, por exemplo Q2_0 ou IQ3_XXS num PC com 64 GB). Abra sua configuração `strata-<model>.json` na pasta de dados do Strata ([onde](docs/INSTALL.md#where-things-are-stored)) e adicione:
- **Geração mais rápida:** `"--adapt-async", "1"` na 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-5% de geração, +7-10% em código e documentos longos. As respostas podem diferir um pouco do padrão (ficam iguais de uma execução para outra).
- **Leitura do prompt mais rápida no Windows:** `"STRATA_PIN_BY_NEED": "1"` no bloco `"env"`. O Windows só consegue travar cerca de metade da RAM para a placa; isto trava primeiro os experts de que a placa realmente precisa. Medido na RTX 5070: +4,4% na leitura do prompt.
Reinicie o Strata (`START-HERE.bat` ou `./setup.sh`) depois da mudança. Para desfazer, remova as duas entradas.
No Windows, abra um novo terminal depois. Em seguida:
## Algo deu errado?
```sh
strata launch
```
- **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.
Siga as instruções para baixar e iniciar um modelo. [Guia do Terminal](docs/TERMINAL_LAUNCH.md).
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).
</details>
## Como funciona?
<p align="center">
<img src="docs/media/terminal-monitor.png" width="900" alt="Aplicativo de Terminal do Strata mostrando a atividade do modelo, a velocidade, a memória e as solicitações recentes">
<br><sub>Acompanhe seu modelo e seu hardware enquanto ele roda.</sub>
</p>
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.
<a id="build-from-source"></a>
<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>
[Compilar a partir do código-fonte](docs/SOURCE_INSTALL.md) · [Docker](docs/DOCKER.md) · [Guia de instalação completo](docs/INSTALL.md)
- **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.
<a id="what-you-need"></a>
<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>
## Hardware
- **Adivinhare 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.
O Strata é gratuito e de código aberto. Se ele é útil para você, você pode apoiar o desenvolvimento:
**Ajude a manter a IA local aberta para todos.** Um café ajuda a financiar o desenvolvimento e os testes em mais hardware.
<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: apoie o Strata" height="48"></a>
[Licença MIT](LICENSE) · [Créditos e licenças dos modelos](docs/HOW_IT_WORKS.md#credits) · [Privacidade](docs/SAFETY.md)
<a id="option-c-let-your-ai-set-it-up"></a>
<sub>Para ferramentas de IA e leitores automáticos: <a href="AGENTS.md">referência técnica</a> · <a href="docs/AI_SETUP.md">instruções de instalação</a>.</sub>
<a href="https://buymeacoffee.com/strataengine"><img src="https://img.shields.io/badge/Buy_me_a_coffee-FFDD00?logo=buymeacoffee&logoColor=000" alt="在 Buy Me a Coffee 上支持 Strata"></a>
<a href="https://buymeacoffee.com/strataengine"><img src="https://cdn.buymeacoffee.com/buttons/v2/default-yellow.png" alt="Buy Me a Coffee:支持 Strata" height="48"></a>
@@ -23,8 +23,9 @@ this PC only. The details and every setting are in [docs/DETAILS.md](docs/DETAIL
- **Where it listens.** `127.0.0.1` by default. `--host 0.0.0.0` (or `"host"` in `strata-<model>.json`) opens it to
your network, and the server then warns when no API key is set.
- **API key.** `"api_key"` in the run config (or `STRATA_API_KEY`) is required on `/v1/*` and on every endpoint
that shows the model's state, requests or answers (`/status`, `/metrics`, `/settings`, `/mcp`, `/props`, `/slots`,
`/api/requests`, `/config`) and on every `POST`. It is compared in constant time. Set one before you open the
that shows the model's state, requests or answers (`/status`, `/metrics`, `/metrics/prometheus`, `/settings`,
`/mcp`, `/props`, `/slots`, `/api/requests`, `/config`) and on every `POST`. It is compared in constant time.
Set one before you open the
server to your network or put a tunnel in front of it.
- **Host check (DNS rebinding, 0.1.38).** Without an API key the server answers only requests whose `Host` is a name
it knows (`localhost`, an IP address, the address it listens on and, when it listens beyond this PC, this PC's
@@ -43,4 +44,53 @@ this PC only. The details and every setting are in [docs/DETAILS.md](docs/DETAIL
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 -o install.sh && sh install.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 2169 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.