I/O path only: the same bytes are read, only HOW the experts outside the RAM copy are read (unbuffered or through the file cache) is chosen differently.
file_tier_unbuffered took its `avail` from cgroup_available_bytes, which credits the group's whole `file` counter as reclaimable. On a RAM-budget
config that credit includes pages the engine holds itself: P2003 (RTX 3060, 46 GB, 36 GiB group), UD-IQ4_XS-v31, 22 GiB RAM copy: the re-check read 18.7 GiB
available when the group had 11 GiB left (limit 36, current 24.8). With held = 0.52 >= 0.5 the timed drive probe was skipped and the file tier switched to the file
cache (direct reads 7.9 GB/s vs 2.5 GB/s cached): prompts 2.5-3x slower (pp2k 85 vs 273 tok/s, pp8k 195 vs 580), decode 10-14% slower. The one start that read
12.0 GiB kept the fast mode, which is why the first start of a session was sometimes fast.
Now the smaller of that figure and the conversation cache's sampler (only the clean inactive file cache is credited, the group's own limit, no operator
cap) decides. The --memory-limit-mib cap is not applied: it bounds the engine's own allocations, not the page cache (counting it sent IQ3_S unbuffered and cut its decode
2-3x). A flip now prints once: "strata: expert reads now go through the file cache (X GiB of RAM free for them, Y GiB limit)"; the technical line is behind STRATA_TRACE.
STRATA_UNBUFFERED_LOAD still forces either mode.
Measured on P2003 (build wip11, n >= 3 starts per arm, interleaved fresh engines, I/O-quiet before each start, memory watch on, floor 14336 MiB):
v31 unforced: main flipped in 5 of 6 starts, this change 0 of 9 (6 + 3): pp2k 85-94 -> 263-273 tok/s, pp8k 187-198 -> 580-590, code_py decode +14 to +20%;
v31 DFlash2 vs MTP in the kept state (3 rounds): DFlash2 +2 to +14% per kind (json_extract -3.5%);
IQ3_S: the file cache stays (9 of 9 starts), decode level with main (code_py 17.0 vs 15.6 tok/s, json_extract 14.2 vs 13.4, page cache evicted before every start);
low RAM (6-7 GiB ballast, budget 16 GiB): no regression, MemAvailable minimum 14911 MiB;
memory minimum with the unbuffered state 15.6-15.8 GB (floor 14.3 GB), no arm stopped.
Not P1.8: main and the P1.8 build flip alike.
(cherry picked from commit be3831e9d944fb44ac36d176f4764e30df0f9f6b)
native_w11_kernel takes a Q5_0 down block (d, qh, qs[16]; 22 bytes): the 5-bit codes are rebuilt with the qh bits and kept as the signed int8 q - 16 (no minimum
term), the scale is d. d_covered_on() says yes for T_Q5_0 only on gfx12 with the switch; the layer otherwise leaves the fused path for the gather + FP16
dequantize + hipBLASLt route as before. hip_prefill_fused_iq gets the pairs IQ3_XXS / Q5_0 and IQ4_XS / Q5_0 (reference-checked: the build has no Q5_0 MMQ).
R9700 (p620, v31, DFlash2, same executable with and without the switch, 3 rounds x 2 reps): prompt 2K +44%, 8K +40%, 32K +13%; decode unchanged.
First-token KL against the FP16 route (self-variation 0): 2K mean 0.016, max 0.068, top-1 24/24. Off by default in this commit.
(cherry picked from commit 97c3033f566d27ea1d741d7b893ac13e0f4e8526)
"strata: RDNA4 (gfx12): prompt chunks of 30,000+ tokens use the chunked GDN scan (STRATA_GDN_CHUNKED=0 turns it off)" is printed once at start with the
other gfx12 default lines (owner's wording, 11-06): only on gfx12, only while STRATA_GDN_CHUNKED is unset and STRATA_GFX12_DEFAULTS is not 0.
The gate follows the gfx12 defaults' one off switch, so the line is true whenever it is printed.
(cherry picked from commit b2d1680a864195383697559edb9b6cf8180b9a19)
STRATA_GDN_CHUNKED unset on a gfx12 card (RDNA4): the FP16 matrix-core chunked recurrence runs for prompt chunks of 30,000 tokens or more
(--prefill auto takes chunks up to 32,768 there, so a 32K prompt is one chunk of about 31,996 rows). Shorter chunks keep the kernels they
ran, so every prompt up to 20K is bit for bit what it was. STRATA_GDN_CHUNKED=0 turns it off, =1 is the old opt-in (every chunk of 64
tokens or more), STRATA_GDN_CHUNKED_MIN=N moves the line. Owner OK 11-06.
R9700 (p620, IQ3_S, DFlash2, interleaved pairs against the same tree without the change, 5 rounds x 2 reps): prompt 32K +6.1% (5/5),
64K +6.7% (5/5). First-token KL against the unchanged build (self-variation 0.00000): all 24 prompts of 2K, 8K, 20K and the 8K agent turn
identical; 32K 0.00005, 32K+ 0.00004, 64K 0.00086; top-1 equal on all 30 prompts. No log line yet (wording is with the coordinator).
(cherry picked from commit 7b579367aaa56978232cb60ef405987a199d736f)
In peer mode the adaptive overlap was refused, so every decode window first waited for the previous round's copies. With the overlap the peer
tier issues its copies in layer order, signals each layer's landing the way the primary does (a stream-ordered D2H copy of the layer number into
a pinned int, or host functions), its residency table takes the swaps at the window's start (flip_pending), and a layer waits for a tier's copies
only when that tier swapped in it (AdaptGate: peer_landed, peer_has, per-tier last). The wait before a window clears the peer's marks with the
primary's. Without the variable nothing changes.
Ported from #2720 (bulhenvaster), rebased by hand onto the current gate (the watchdog label stays). Not ported: STRATA_PEER_LAUNCH_IN_POOL
(the author measured no gain on his build), the new decode timing line and the documentation paragraph.
(cherry picked from commit e78b64a7348d685dfd11799fdb395661f2120436)
With --peer-device the prompt path never gave the CPU pool a share (cpu_maybe required !m.pp), so a short read at long context streamed every
expert neither card holds over the primary's link while the CPU sat idle. With the switch the share runs beside the peer: it takes only experts
neither card computes, its rows go between the primary's and the peer's block (the peer's stays the tail its combine reads from back_at), and the
staged chunks go up to cpu_share_max()'s 3072 tokens. Without the switch nothing changes.
Ported from #2721 (bulhenvaster), rebased by hand onto the current prompt path (the default-armed share's limit and the routed-stream gate).
(cherry picked from commit c8b6c43c1f2653636d15fe14abf53112357570aa)
Decode reads missed expert blobs from host RAM three ways; the PCIe share's
blobs cross into VRAM anyway and were dropped after the window. With the
switch set, the share takes each layer's most-routed misses and a fetched
expert routed enough more than the layer's least-routed resident expert that
the window does not route is copied straight into that expert's slot
(fetch_blobs gets an optional dst2 target per blob). =2 leaves the tier's own
swaps out of a layer where an admission ran. STRATA_ADAPT_FETCH_VERIFY=1 reads
every admitted slot back. Off, with a log line saying why, where it cannot run.
Unset changes nothing.
Ported from PR 2964 (sergqwer) onto f-0216-core; the strata generate verify
loop was merged by hand (main's speculative-q lines kept) and the DETAILS.md
note became a row of the opt-in switch table.
l620 (RTX PRO 5000, IQ3_S, 3,896-slot cache, --pcie-frac 0.25): decode =2 +10.2%,
=1 +5.0% (4 rounds); token forcing over 14,167 tokens: mismatch 2.73% vs 2.53% /
2.53% for two unset arms (+0.19 pt, 90% CI -0.02..+0.42), PASS.
(cherry picked from commit b2bfaef906acee4f86bdf9f052aee6a4d295fb4e)
Prompt-path block scores in single-pass TF32 with a per-block error bound,
3xTF32 rescoring only near each query's top-k cut-off, so qsa_block_topk
returns the same cells in the same order, ties included. Off unless the
variable is set; says once where it cannot run (HIP, Metal, SYCL, layer-major
path). Adds qsa_two_pass_test, qsa_two_pass_status_test and the GPU parity
test qsa_two_pass_parity.
Ported from PR 4119 (bitaccesscomau); the log line of the per-prompt counters
is shortened without dropping a field.
l620 (sm_120, IQ3_S), greedy ids identical off vs on in every pair:
prompt 128K +1.65% (n=12), 256K +4.10% (n=8), 32K -0.14% (noise).
(cherry picked from commit bbf7c55f1351d913233250fe7869642f7c2b98a1)
drafting.CLASS_DEFAULTS: one removable entry per set, read from the signals the confidence already reads (a CUDA card whose engine slots are under the last-but-one CONF_STEPS step).
IQ3_S: on P2003 the file cache makes the MTP faster than DFlash2 by 12-44% (RTX2 t4b). UD-IQ4_XS-v31: DFlash2 beats the MTP by about 6.5% (t8, 3 starts; DF-VAL forced-fast tuned 0.8 x1.037).
The v31 entry DEPENDS ON RTX2's FILE-TIER FIX (unbuffered reads) BEING IN THE SAME RELEASE; without it remove that entry.
Anything chosen by name wins. Resident CUDA, Strix, gfx1201, Arc and Metal entries and the conf tables are unchanged. The sm_86 support line reads the set: UD-IQ4_XS-v31 gets the existing
'faster on some kinds of text and slower on others' line, every other model keeps 'slower'.
(cherry picked from commit 42412d42ff835d6a66a78fbf6d119911908eda1b)
One more SET_DEFAULTS line for the measured cell (DF-VAL, setup's config, budget 38, 3 rounds: tuned 0.35 against stock 0.35 x1.046, 3 of 3 rounds, every kind 3 of 3).
(cherry picked from commit c2b801157e71a503da859b66baff12fff28cd299)
The SYCL 0.2.11 engine reads only bf16 selector codebooks, so Arc gets its own tuned file (qwen3.8-flash-next-dflash2.strata1.q4_0-bf16sel.pack, 522,458,112 bytes, own SHA-256 pin) through
SetDefault.pack. Only IQ3_XXS, the set measured on the A750 (tuned 0.6 x1.054, 3 of 3 rounds); other Arc sets keep the stock pack. SYCL never gets the selq4 file, CUDA / HIP / Metal never the
bf16sel one. NOTICE names the second file and no longer overclaims about the text.
(cherry picked from commit 6111c583b70e3c88a3d54039e13d76b19f15742d)
11 of 50 engines on 0.2.11 never got past the prompt on this card class. Only Auto changes: DFlash2 chosen by name, a saved choice and the person's own options still win; gfx1151, other AMD, CUDA, Metal and SYCL and the confidence tables are unchanged. The support line under the Drafting dropdown says it in plain words (app catalog entry in all nine languages).
(cherry picked from commit 8b4398743aa67368d72277310bbe8093bbe4923c)
SET_DEFAULTS entries carry a card condition (tuned_applies: the CONF_STEPS first-step slots or the Spark's shared memory for CUDA, gfx1151 for HIP); no entry names a conf, so
conf_for / CONF_STEPS / BACKEND_CONF run as before and only the pack changes. Enabled only for the cells measured at the shipped conf (CUDA-resident IQ3_S and UD-IQ4_XS-v31, Strix IQ3_S, UD-Q4_K_XL, UD-IQ4_XS).
12 GB and 24 GB cards, several cards, gfx1201, other AMD and SYCL never fetch or use it. Fetch keeps tuned, then stock, then MTP; silent offline; feed backends are asked per start's backend.
(cherry picked from commit f096dcd750b5928ace6f9eca1e2936b9e35578d4)
EXPERT_WINDOW_SLOTS (2 * MAXT * TOPK) is the slots a size pool must hold for one window's picks to have a place; ExpertCache's check, the residency
planner's floor and the arena's keep all read it. expert_residency_test now also covers the GSQ-RCO IQ3_S set's real pools (7, two of them a single
layer), a sweep of RAM targets against a copy of the old global ranking as the negative control, a refusal beyond what the floors allow, and that
the card's sliced 16-byte-padded strides partition the layers like the whole-expert bytes do. Class E (placement only).
Mixed-quant models can fail to load in the split engine after cold-expert offload: the global residency ranking can leave one size pool below the
2 * MAXT * TOPK slots Up::expert_cache() needs even while other pools can still give up experts. Group experts by both the gate/up and the
down byte strides, keep the cold-first ranking but skip eviction from a pool that has reached the window floor, count only the extra evictions
that really happen, and say so before the model is uploaded when the request cannot be placed.
Ported from #5158 (midhatn) unchanged; the tests and the engine check follow in the next commit.
With --trim-stage-weights every later stage's HC_Q8 loop over the GGUF copied the rows of all 48 layers and read its own. The loop now skips a
tensor outside the stage's layer range, the filter the upload loop above it and the bf16-pack path below it already use. The Q8_0 rows copied
and the bytes not copied are recorded in the run manifest (STRATA_MANIFEST). STRATA_HC_Q8_ALL_STAGES=1 copies every layer as before.
No value changes: a stage never reads another stage's layers; without a trim the range is unset and nothing is skipped. Class E.
Co-authored-by: havok2-htwo <[email protected]>
SET_DEFAULTS is the one per-(backend, set) table (Metal only; IQ3_S stays on the MTP). Setup, strata launch and strata drafting fetch fetch the tuned pack through the signed feed
(pinned size and SHA-256, resumable, silent offline, the standard drafter and then the MTP as fallbacks). Apache-2.0 licence and the modified-by-Strata notice ship in
data/dflash2-tuned and are copied beside the file. The Metal 'not tested' line is replaced for these sets.
Ported from #4871 (cctan): src/prefill/moe_iq4_w4a8.cu, tried first by mmq::Context::run; unset, nothing changes.
Changes on top of the PR: the "would MMQ split this launch along K" test is now mmq::launch_splits_k, which reads the
device's real MMQ configs (tile width, stream_k flag, tile efficiency) instead of an Ampere table, so the kernel declines
exactly the launches whose sums MMQ adds in another order, on any architecture; sm_120 joins sm_80..sm_88 (bit-for-bit
against MMQ on an RTX PRO 5000 and an RTX 3060); the kernel counts what it ran and why it declined and prints that at exit;
prefill_iq4_w4a8_test takes --tokens N (tails), --wide (scale extremes) and --bench (MMQ against the kernel per product set).
Class E: 0 differing output words in every case (tokens 97 / 2049 / 4096 / 8191 / 15360 / 32768, uniform and log-normal routing,
group buffers and pointer table). Kernel time against MMQ on the down products (IQ4_NL): +9..+70% on sm_86, +13..+50% on sm_120.
cpu_vnni_ok() = AVX-512 F + DQ + BW + VL + VNNI + OS state, cpu_vbmi_ok() = F + VL + VBMI; the Q2_0 row dot and the q8 quantizer take the 512-bit form
on VNNI without VBMI (the one VBMI op, vpmultishiftqb, has a bit-for-bit substitute unpack), and the i-quant / Q5_0 rows of iq_avx512.cpp, whose object holds
no VBMI instruction, gate on cpu_vnni_ok() instead of cpu_avx512_ok(). Adjustments over the PR: DQ is asked for (the file is compiled with it), the i-quant rows
gate on VNNI (the rows use vpdpwssd/vpdpbusd), and STRATA_NO_VBMI=1 runs the substitute unpack on a CPU that has VBMI (a test knob).
IQ4_XS gate/up: ggml-cpu's dot order (the integer lanes of its AVX2 dot, f16 scale times y.d, eight float lanes, its hsum) for every token count;
before, an AVX-512 CPU ran ggml's one-token dot per token. Q5_0 down: ggml's dot order, weights decoded once per block pair for all tokens.
IQ4_NL down: iq4nl256_down_rows' order, 512-bit. Each is checked once against the code it replaces; STRATA_NO_IQ4XS512=1 / STRATA_NO_QD512=1 switch
them off. The IQ4_XS nibble-plane decode follows the layout of PR 5243.
Co-authored-by: bsorensen110 <[email protected]>
Ported from #4972 (Phlimosx), extended: the residency is checked again at every request's start, so a
smaller cache (VRAM resize) or an eviction turns io prefetch, its decode gate and the look-ahead back on,
and a regrown cache turns them off again. Only what the defaults switched on is touched: an explicit
STRATA_IO_PREFETCH=1 keeps io prefetch and the look-ahead, STRATA_LOOKAHEAD=1 keeps the look-ahead,
STRATA_FILE_TIER_IDLE=0 turns the policy off. set_io_prefetch clears the gate flag with the prefetch.
Class E: the answers are identical (45 of 45 decode answers on l620, --mmap-experts, 24576 of 24576 resident).
A diagnostic for testers and for the performance work, off by default. With STRATA_MANIFEST unset every call returns at once
and nothing is printed or written. With it set, the engine writes <file> (whole file, replaced by rename) when it is ready and
again after a request if something new was recorded; the launcher (strata_control/split_engine.py) writes the all-cards split
decision to <file>.launch.json. Sorted keys, no timestamps, process ids or addresses, so two matched runs give the same text.
Content:
- engine: version, engine kind (single GPU or layer split), stages, KV format, drafter, spec window, pipeline windows, PLE I/O.
- budgets: context, KV resident cells, expert slots and cache MiB (all tiers and primary), VRAM reserve, prefill chunk cap and
stages, pool workers, memory limit, resident budget, batch slots, conversation cache.
- paths: one entry per decision with selected/declined and the reason in plain words: prompt experts per format class (MMQ,
FP16 WMMA tiles, native fused kernels, FP16 GEMM, with the gate/up and down formats and the layer count), MMQ direct and group
sort, HC_Q8, the output head (VRAM or pinned host, repacked or not), route tail skip, DMA batch mode.
- tensors: dense and head formats as held (count, bytes), the packed and HC_Q8 copies made at load.
- launch: split-engine mode, engine chosen, reasons, card architecture and count, routed-expert types, and the AMD auto rule
condition by condition (architecture, card count, expert types), so four gfx1201 with the IQ3_S set is recognised and two or
three cards are not promoted by the four-card rule.
The only text it prints is "strata: STRATA_MANIFEST: cannot write <file>" on stderr, once, when the variable is set and the file
cannot be written.
Tests: tests/core/run_manifest_test.cpp (off is free and writes nothing, matched runs in any recording order give the same text,
replace-on-rerecord, escaping, dirty tracking, unwritable target), tests/split/test_split_engine.py::LaunchManifest.
Class E: the queued jobs (page offsets, read lengths, Use order inside a job) and the dedup_rows count are the same as
before; the bytes delivered are the same.
PleReader::issue() built a std::unordered_map by page and a std::vector<Job> on the heap on every call, then sorted the
jobs by offset. A decode step asks for 1..16 rows (the prefetch for PLE_N_HEADS = 16), so those allocations and the
hashing were most of the work. A batch of up to 64 rows (PR #4910) now collects its misses into a fixed stack array,
sorts them by (page offset, request order), and merges equal pages in one pass straight into the queue. Nothing is shared
between calls, so concurrent issuers (they hold the reader's mutex when threaded) have no scratch to race on.
Batches above 64 rows (a prompt chunk: thousands of rows) keep the previous hash path. The PR also sorted every miss in
that case with a reusable vector; measured, that is slower there (see below), so it was not taken. A batch of more than
2^32 rows is refused like a failed reader, as the 32-bit order/ticket counters cannot hold it (the old code could not
queue it either).
Measured with tests/ngram/ple_issue_bench.cpp (this thread's heap allocations and thread CPU time inside issue(), threaded
reader, 3000 calls per cell, 2 rounds that agree within 3%; Ryzen 5 5600X, g++ -O2, synthetic 300k-row table, rows are a
4000-row window / uniform / skewed):
rows allocations/issue before -> after CPU us/issue before -> after
1 5.1 -> 2.1 1.69 -> 1.54
4 13.4 -> 5.4 2.00 -> 1.82
16 40.3 -> 17.4 3.44 -> 2.53
64 125.2 -> 51.8 8.07 -> 5.01
above 64: unchanged (the PR's sort-everything variant took 159 vs 66 us at 3072 rows and 2.9 vs 0.55 ms at 49152).
The decode impact is not measured here (see the round's results file).
Tests: ple_reader_test --selftest now also checks, for batch sizes 0, 1, 2, 15, 16, 63, 64, 65, 66, 127..129, 300, 1000,
4096 (random, clustered, one row n times, descending, out-of-range mixed in, page-straddling rows twice), that the bytes
are right, requests grew by the rows asked for and dedup_rows by (rows read - distinct pages). With the existing
selftest and the multi-client stress test (rows across pages, injected delays and read errors, late writers, failed
reader): pass plain, under ASan+UBSan, and the stress test under TSan.
Co-authored-by: martifaj <[email protected]>
Correctness (class E): the same windows are served in the same order, only a little earlier; the tokens do not change.
With --pipeline-windows 2 on a two-stage layer split the drafter sits on the first stage's card (mtp_first).
read_windows_pl called mtp.prefill(...) right after a stage-1 window finished, while stage 0 still had a window in
flight on that same card. MtpDrafter::prefill blocks the host until its stream is idle (on HIP it synchronizes once
per group whatever its `sync` argument says: prefill_per_group_sync), a stage-0 window only advances when this same
thread serves its doorbells, so the host sat in hipStreamSynchronize and the window it waited for could never
finish: the first request of a fresh engine read its prompt, produced 0 tokens, and the watchdog ended the engine
60 s later (gfx1201 + gfx1030, issue #5147). Neither 20 s guard in Verifier::service / done can fire, because the
loop never runs again.
serve_stage0_to_end() serves stage 0's windows to their end before that prefill, when the drafter shares stage 0's
card. The HIP per-group synchronization (which exists because the unsynchronized E-4 queue hangs on gfx1201 /
ROCm 7.2.4) is left as it is: the caller is fixed, not the mitigation. Pipeline windows 0 and 1 take no new path
(mtp_first is false there). The overlap lost around each drafter prefill has not been measured.
Not testable on the boxes of this round: it needs a mixed pair of HIP cards (gfx1201 + gfx1030) and a two-stage split;
p620 has one R9700. Compiles on CUDA (z440, sm_86).
Co-authored-by: cloud4t0r <[email protected]>
Correctness (class E, not a speedup): the product values do not change; the rows past T*K are never routed to.
llama.cpp's mul_mat_q_stream_k_fixup loads ids_dst[col_low + jt*J + j] for a whole J-column tile before it compares
the tile with the expert's rows (the main kernel skips an empty tile first; the fixup only skips its writes), and its
tiles run to ceil(max_rows / J) * J for every expert of the launch. A table of exactly T*K entries is read past its
end by up to roundup(max_rows, J), and the main kernel reads up to J - 1 past its last row. Strata's `ids_identity`
was an exact-sized allocation: when it ended a 2 MiB page the over-read faulted on the next (Xid 31 FAULT_PDE,
"prefill copy_i32: an illegal memory access" on a long prompt on a CUDA layer split; issue #1468 and others).
PR #5108 added T + 256 rows by hand. Here the margin is derived: J is bounded by ggml's mul_mat_q_switch_J
(J = 8 .. 128 in steps of 8), the tile loops of moe_mmq.cu now use that one constant (kMmqTileColsMax /
kMmqTileColsStep), and the table is T*K + roundup(T, J) + J rows (an expert takes at most one row per token), with
checked 64-bit arithmetic: a size that does not fit fails the carve instead of wrapping to a small table. The same
size is used by carve, bytes_needed_impl and the three places that write the identity (helper rebind, normal prompt,
layer-major), so the estimate and the carve stay identical. The peer prefill's row table, an exact cudaMalloc used by
the same MMQ launches, gets the same margin.
Tests: tests/prefill/mmq_ids_margin_test.cpp enumerates every index the fixup and the main kernel can read for 2000
random layouts (late experts, empty tiles, every J), checks the exact-sized table of before is overrun by the late
expert layout, and the overflow refusals. Builds on CUDA sm_86 (z440).
Co-authored-by: quantforge-br <[email protected]>
Correctness (class E): kept even without a speedup.
Flag B (PR #5096): the graph waits for "flag B >= layer + 1" and flag B is one counter that only rises. A non-empty
plan raises it from a host function on the copy stream, after its uploads. An empty plan (n <= 0) raised it on the
spot, even when the copy stream still held an earlier layer's, or the other token group's, uploads: the counter
jumped past a layer whose bytes had not landed. Verifier::raise_flag_b_ordered() now raises it through the copy
stream whenever copies were queued in this window, and directly only when none were. The same ordering is used at
the four places that publish an empty plan when the pool did not (run, run_slot_rows, batch_serve, service), which
had the same hazard. A failed host-function launch used to be ignored (flag B never rose and the window hung); it
now stops the engine like a failed upload does, after releasing the GPU waits.
Adaptive-copy bookkeeping (PR #4844): a round planned on the pipelined loop whose loop drained before step 3 is
marked resident on the serial rule, which used the lo/hi/bi of an earlier round, so only that range of swaps was
marked and the others were lost (the slot held the new expert, the table still named the old one). The pipelined plan
now names the whole round. The range arithmetic moved into AdaptiveCopyRange (adaptive_range.hpp) so a host test can
walk the rounds.
Tests: tests/core/flag_order_test.cu (delayed host-function copy standing in for a slow DMA, a consumer kernel that
reads staging once flag B allows it, a negative control that shows the old direct raise releases it early, flag only
rises, nothing queued, graph replay twice, every device; passes on both A4000s of z440), tests/program/
adaptive_range_test.cpp (serial batches for 0, 1, many swaps and several caps, the stale-range loss, empty round).
Co-authored-by: AlanJager <[email protected]>
Co-authored-by: uvlad23 <[email protected]>
Reporting only (class E): no engine speedup, no change to what the engine computes.
serve/server.py
- A prompt read that gives way (BYIELD) and resumes keeps the request's external prefix boundary, and the prompt
time and tokens of every leg are summed exactly once (prefill_leg_stats; PR #5318).
- The admission's DONE times one token and BDONE counts that token but times only the slot: the slot time now
includes the admission time, and the output token is not counted twice. A figure the engine never reported
(a close at the admission token, in the slot, a carry back to the solo path) stays unknown (None) instead of
turning into 0 or into the API's output count (PR #5307).
- The two patches overlap in the request finalization; ended_on_yield is taken before the unknown-figures step so
the identity check still sees the yield's own record.
PfTimer::mark(): a mark in the phase already running is not recorded (PR #4006). The totals are the same, a
fraction of the event records; under WDDM every record is a submission of its own and the launch gaps they made
were charged to "dequant". The rule is a pure function in prefill/timer_marks.hpp with a host test.
Tests: serve/test_yield_accounting.py (new: two yields, solo and admission yields, cancel after one and two
yields), serve/test_parallel.py BatchLegAccounting (EOS and length finishes, zero slot time, close at the
admission token and in the slot, slot to solo carry), serve/test_request_accounting.py (unknown stays unknown,
explicit zero), tests/prefill/timer_marks_test.cpp (same totals with 18 of 23 records skipped).
Co-authored-by: YusenZhang0601 <[email protected]>
Co-authored-by: shoucandanghehe <[email protected]>
Co-authored-by: Vop3p <[email protected]>
vec_dot_q5_0_q8_1 used the q8_1 block's ds.y (the sum of the original activations). With the SwiGLU outputs (not zero-mean) the whole-expert error with Q5_0 down was 2.1e-2 .. 3.1e-2 against 1.2e-2 for every other down type. Now 1.2e-2, and the GPU agrees with the CPU path. The Q5_0 parity limit is tightened to 1.8e-2.
native_expert_parity: --bench decode micro-number, +0/-0 is no fp16 difference.
Registers every gate/up x down pair of the new sets (CUDA and HIP, SYCL), a dense type test
(native_dense_types_parity), Q5_0 down in native_grouped_parity, new-set types in the MMQ screen,
and a --synthetic mode in the SYCL native_expert_parity.
- native_expert_parity: ctest entries for every gate/up x down pair the new sets use
(IQ1_M, IQ2_XXS, IQ2_XS, IQ2_S, IQ3_XXS, IQ4_XS gate/up; Q2_0, IQ4_NL, Q5_0, Q8_0 down)
at hidden 2560 / FF 640.
- native_dense_parity: dense Q2_0..Q6_K, IQ4_NL, IQ4_XS decode (MMVQ, 1-8 columns) and the
prompt dequantizers (f32, f16, bf16) against ggml.
- prefill_mmq_v31_test: the new expert formats through MMQ (Q5_0 only in a
STRATA_ORCA_Q4KS_MMQ build), plus a --bench of Q5_0 MMQ against dequantize + cuBLAS.
The checker treated every ET_DYN without PT_INTERP as a static executable, so the RUNPATH $ORIGIN that Pillow, numpy and sharp libraries carry was
reported as 'static PIE with a damaged dynamic section' (35 files in 0.2.11, 18 Pillow libraries in the published 0.2.10 Control; false positives).
A static PIE is now an ET_DYN with no interpreter, a non-zero entry point and DF_1_PIE in DT_FLAGS_1 and no DT_SONAME; a library (no entry point, or a
SONAME and no PIE flag) is never checked. A candidate whose dynamic section cannot be read is reported as damaged, because the overwrite that crashes
rg also erases the flags, so the test for 'executable' uses the entry point, which the damage does not touch. Tests (tools/test_appimage_elf.py, 16, run
on p1006): shared libraries with and without a SONAME, entry point or RUNPATH pass; a static PIE passes; a static PIE with a 0x5A .dynamic or an added
RUNPATH fails; a real gcc shared library with RUNPATH $ORIGIN passes; a real gcc static PIE passes and its overwritten copy fails. Build tool only.
The sidebar now shows a file's current content, so a CRLF file reaches the Markdown renderer. Every line then ends in a carriage return, which '.'
does not match: a list line passed the list-start test but not the item pattern, the list loop took no line, and the outer loop never advanced - an
endless loop pushing nodes (150 MB to 4.4 GB in 20 s on a 60 byte file; QAGUI final pass, the agent's Add-Content writes CRLF). 0.2.10 never showed
the changed text so it never met it; the bug is older and any CRLF Markdown list opened in the preview would hit it.
Line ends (CRLF, CR, LF) are read as one in the renderer and in the preview slicer, and the list loop always moves on (a line the item pattern rejects is
shown as text). Test: tests/markdown-crlf.test.mjs renders in a child process with a 200 MB cap and a 20 s limit; on 68bead108 all 4 cases abort out of
memory, with the fix all pass. Edge 156, seeded live turns (stream, finish with the card, file rewritten with CRLF, cards opened 6 times): heap stays
near 19 MB with the fix. No visible text.
The Select popup (every dropdown in Settings, Models and the tune dialogs) was capped at 320 px whatever the window and opened upward through a
transform, so in a short window or at a large Interface size it scrolled although the window had room, or started above the top of the window
(Edge 156 matrix: Select popups 4 of 72 and 3 of 72 cases right on 548fdf061, 69 of 72 and 61 of 72 after; the rest are harness not-open rows).
It now uses lib/overlay-place.ts like the menus, measures its real height once it is on the screen, and writes maxHeight. The Mode menu and the
sidebar More menu already passed on 548fdf061 (77 of 77, 84 of 84; 43 of 77 and 49 of 84 on v0.2.10). No visible text.
The preview kept a file's first content: openPreview dropped the freshly read item when its id was already a tab, the panel's loading effect had no
content version, and the effect that re-adds a message's artifacts put the stored first copy back. All three change together, through pure rules in
lib/preview-merge.ts: opening a changed file replaces its tab in place; a message's artifact only adds a missing tab or replaces an older, different
copy (each copy carries the time it was read or made) and never overwrites a newer one; both return the same array when nothing changed, so no update
and no loop follows. The panel reloads when (id, content version) changes: a hash of the text, or the host's mtime and size for pictures and PDFs
(/api/files/preview now sends mtime). The reading view is reset by the file, not by its content. Tests: stale case, 2000 alternations without a
change, one load per real change. Real Edge 156 with the real panel: the old rules keep Version One, the new ones show Version Two through 300
sync/render alternations with 0 Markdown renders. No visible text.
A reload does not stop an agent turn, but the page that came back showed "Network error. Try sending the message again."
with no Stop button, and a new message was refused with "This chat is still working". The host now answers GET /api/chat/active
(the chats with a turn running) and POST /api/chat/attach: a running turn sends one catch-up (the start, the text and thinking
so far as a resync frame, every tool card as it is now with the approvals waiting for a yes, the open questions) and then its
live frames; a turn that ended while the page was away is shown whole once, named by the request id of the cut-off reply.
At start the page asks for the running turns, takes each over in the reply it already has (running, Stop button, the
failure it wrote is cleared), and asks once about cut-off replies of the other recent chats; nothing to take over leaves the
reply as it was. Existing states and texts only.
The things that used to run at bootOnce from the chats in memory (follow time-skip runs, end loops the last window left running,
mark tool cards of vanished turns) now run once the chats have been read from the host (resumeSessions), because the chats are
no longer in the page at that moment. The save retry waits a little longer each time (up to a minute) and never keeps a closing
page alive. Also: tests for a second window's changes (applyChatChange) in the parallel-chats test.
Conflicts in strata_cli/bench_facts.py (the redactor), three hunks: kept every side. The patterns stay QATUI's (the key and the names are matched as whole tokens, a hyphen next to the key does not hide it, the names are not touched inside a label the function wrote), each entry carries SECURE's third field (is a login or host name) for redact_document, and the host names come from socket.gethostname() and sysinfo.node() (LIFE), both marked as names.
On a PC whose login is an ordinary word ("user", "test", "admin") the whole-document pass turned status reasons and field names into <user>, and the report failed its own validation. Paths, addresses and the key are still replaced everywhere; the name rules apply to hardware, model and configuration text, error messages, engine options and request checks.
main() and mac_control_setup() read fam['hf'] to build the pin lookup URL; the NVFP4 families download a file list and have no such key, so every NVFP4 install raised KeyError.
The helper takes None and then keeps the old mark. Found by the INT3 merge run (tools/test_setup_nvfp4.py, tools/test_setup_dl_fix.py).
The app host saves each chat as its own file (data/chats/<id>.json, an index, a version and a revision in
every file), written to a temporary file, flushed and renamed over the chat, so a crash or a full disk in
the middle keeps the older version whole. The folder and files are owner-only. The page talks to the host
through /api/chats (the page must name itself with x-strata-client; other sites are refused), sends only the
chats that changed, never cuts anything, and keeps a chat the host cannot write whole in memory, retries it,
and says so with the notice and the "Copy unsaved chats" button in all nine languages. A save from an older
copy (a second window) keeps the version it displaces in chats/conflicts. The browser's two older layouts
(the 0.2.10 list and the per-chat entries) are handed to the host once at start and left untouched as a
fallback for one release; a last-moment copy covers a page that is closed before the host confirmed a save.
Other windows are told by an event and take over what changed.
Conflict in serve/server.py main(): LIFECYCLE wraps _main in the shared activity lease (#4463), OWN wraps it in the decoupled console (#4479 / #2481). Kept both: the lease is taken first, and _main runs inside decoupled_console() unless STRATA_BLOCKING_CONSOLE=1; the lease is closed in the same finally.
Conflict in strata_cli/bench_facts.py (the redaction helper): kept both sides' intent. The host name comes from sysinfo.node() (LIFE) and the key is redacted with usable_key(key), word=True, exact=True (SECURE).
DRAIN_TIMEOUT_S (300 s) and QUARANTINE_S (600 s) apply only after a request has left and the server has sent STOP / BSTOP:
_drain_control (the finally of generate_batched), _quarantine_control and the BDONE wait of _release_slot_when_done. They
never cover a generation a client is still reading. But they were counted from the start of the drain, so an engine that
was told to stop and kept producing lines (an older engine runs on to max_new; a long prompt read ends at its next chunk)
could be ended after 900 s, with every other request in it. Now every line the engine writes starts the limit again: only
an engine that says nothing at all for 300 s + 600 s after the stop is ended.
Tests (fake engine, limits scaled to 0.3 s + 0.4 s): an engine that keeps talking for ~1.8 s after the stop is not ended and
the request in another slot is untouched (fails with the previous commit's absolute limits); a silent one is ended; the
drain-timeout leak test now really closes the stream mid-read (the old version was ended by a BYIELD before the drain);
long replies, far over the limits, complete. Also: a client that is idle while the engine thinks is not cut by the 60 s
write limit (0.3 s between tokens, 0.1 s limit, the whole reply arrives).
On a batch engine the control lines carry one exchange at a time and every request takes `ctl` before it sends or reads
there. VRAM and SAVE / RESTORE read the same lines for their answer without it (POST /v1/vram took it itself, the image
encoder's cache resize, the load-time reserve and the session handler did not): run beside a request they took its
T / PP / DONE lines, and the request lost its reply (or session_file ended the engine as out of step).
StrataEngine.vram / session_file now run under _exclusive_control(): ctl on a batch engine (a thread that already holds it,
as Service.vram does, is marked and does not wait for itself), nothing to take for one request at a time. Waiting longer
than CONTROL_WAIT_S raises the existing "a request is still running; try again when it has finished". No output changes.
Test: the fake engine answers VRAM / SAVE / RESTORE; a request streams while the call is made. On the base the request
ends with 212 of 421 tokens; with the fix it is complete and the call is answered.
Each client puts its own random canary in its prompt and asks the model to repeat it; some leave before the first byte,
during the prompt read or after a few chunks. Every text a client receives (content and reasoning) is searched for the
canaries issued in the run: a canary that is not the client's own is a cross-delivery (exit 1). Used for the real-engine
A/B of the round 0213 ownership fixes.
print() in the server blocks when the console stops taking output (a Windows console in selection mode, a paused
terminal, a launcher that stopped reading the pipe), in whatever thread called it - including those that own the
engine's turn, a cancel or an admission. While serve main() runs, stdout and stderr (when they are the process's own)
are replaced by serve/nonblocking_out.NonBlockingOut: write() queues and returns, one daemon thread writes to the real
stream, the oldest text is dropped (and counted) when the queue is over 1 MiB / 4000 chunks. Restored and drained at the
end of main(). Protocol output (engine pipe, HTTP) is not touched. STRATA_BLOCKING_CONSOLE=1 turns it off.
No visible text is added; a drop is counted, not announced.
Tests: a plain print to an unread pipe blocks (control); the wrapper never blocks, keeps the newest lines in order, and
drains. Approach credit: the proposal in #4479 (one process-owned output consumer).
The thread that writes the SSE stream to a client also holds the engine's turn (the one-request path). A client that
stops reading without closing (a hung process, a dropped network) filled the socket buffers and the write blocked until
the OS gave the connection up: the engine idle, every other request waiting. A write that makes no progress for
STREAM_WRITE_TIMEOUT_S (60 s; STRATA_STREAM_WRITE_TIMEOUT_S, 0 = off) now ends as a disconnect: the engine is
stopped and the turn given back. No text changes.
Test: a client that never reads a 400k-token stream; on the base the turn is still held after 20 s, with the fix it is
given back at once. Part 2 of #4271 (arrival order of the one-request path: the turn is a plain Lock) is not done here.
_take_control read its turn, gave up the lock and then waited: a notification sent in between was lost and the waiter
slept up to 0.5 s although it could go. It now reads the turn (and the short-first arbitration) again under the lock
before waiting, and a waiter that leaves (cancelled, closed) notifies the others.
Approach credit: PR #4738. Tests are new (a hooked Condition forces the schedule; on the base the waiter waits).
- GpuBusy before any segment and with zero tokens: the history row says "error" with no prompt tokens and takes no part
in the totals, latencies or last timings (#3040); a continuation after native work keeps the old accounting;
- the engine's and the image encoder's stdout are decoded with errors="replace": on Windows a byte such as 0x8e ended
the reader thread (UnicodeDecodeError) and the server lost the engine's output. Protocol lines stay ASCII.
Not taken from PR #5044: the q36 engine OOM messages (engine C++, and the wording goes to the coordinator first).
Approach credit: PR #5044 (Python parts). Tests: test_parallel (refused admission accounting), test_serve_robustness
(a 0x8e byte before READY and in a reply).
StrataEngine.__init__ cleaned up a start that ended before READY through self.proc; restart()'s close() on another
thread had set it to None (AttributeError: NoneType has no attribute stdin, a recoverable engine abort became a 400).
The cleanup now uses the process object of THIS start, captured once right after it was spawned.
Approach credit: PR #4792 (same idea; the test is new: the closing thread is forced inside proc.wait()).
Polls come from several request and monitor threads. A second poll moved `pos` between the first one's size check and
its read: lines were read twice (parks counted double) and a read length could go negative (ValueError). Found by
HWSTALL while triaging #3443.
serve/test_serve_robustness.py: the overlap is forced with a hooked getsize; on the base every parked line is counted
twice (8 != 4).
A --batch request read its slot's lines from `self.slot_q[slot]`, the control lines from `self.lines`, and sent its
BSTOP / BYIELD / STOP with the slot number, all looked up when used. After the engine was restarted the same names meant
the NEW process: a request of the old one read (and could stop) the new one's requests. And a request that left before
the engine had ended its exchange (a drain that timed out after 300 s) gave the slot and the control lines back, so the
engine's late T / PP / DONE / BADM / BT frames reached the next request as its own.
- the request notes the queues of the engine process it was admitted on and reads only those; a send meant for that
process (BSTOP, BYIELD, STOP) is not sent to a later one (EngineDied instead); slot state is only given back on the
same process;
- an exchange that is not drained in DRAIN_TIMEOUT_S is quarantined: the control lines and the admission's slot stay with
it, in the background, until the engine gives DONE / BADM (then a continued admission is BSTOPped and freed at its
BDONE); if the engine never does within QUARANTINE_S it is ended, as a silent engine is (#481). A slot's BDONE wait that
times out ends the engine too, instead of freeing a slot whose lines may still come;
- the pump drops BT / BDONE frames for a slot no request has claimed (counted in stale_frames);
- _ctl_mode, _ctl_result, _yielded, _last_done, _ctl_erred are per request thread on a batch engine;
- serve/test_stream_ownership.py (BatchOwnership): interleaved clients with canaries, a cancel in the middle of the prompt
read followed by reuse, a drain that times out (DEAFSTOP), the engine ending during a drain and restarting. On the base
the restart case fails: requests of the old process stay attached to the new process's slots.
The other cases of the register (mid-prefill cancel, drain timeout, slot not handed on) pass on the base with the fake
engine; they are kept as regression tests. The real-engine check follows.
ReplicaEngine.generate released the per-replica run lock in its finally without closing the child generator. The
child's own STOP and drain-to-DONE ran later, when the wrapper frame was collected, after the next request had sent
its GEN: the two read each other's tokens and DONE (an empty reply, or another request's text), and a cleanup error
was lost as an ignored generator exception. The unbatched engine's shared `last` was also read by Service after the
lock was free, so a reply could carry the next request's figures.
- close the child before the lock is given back; a close that fails ends the replica (heal() starts it again) and the
error still reaches Service (EngineSilent handling is unchanged);
- copy the new DONE figures into a per-caller object while the replica is owned; with no new DONE the caller's earlier
object stays, so an old result is never recorded as new;
- serve/test_stream_ownership.py: canary replies from a fake engine, a hooked run lock that forces the legal schedule
(the successor takes the replica right after release), 30 forced hand-offs and 60 unforced back-to-back requests.
On the base: empty reply for the successor, the predecessor's canary in its text, 30 of 30 forced hand-offs crossed,
figures of the other request.
Approach credit: PR #2886 (scheme A: explicit child close, caller-local DONE snapshot). Written in this tree.
linuxdeploy rewrites the RPATH of every ELF in the AppDir; for a static PIE (the bundled rg) that leaves .dynamic overwritten and the program does not
start (shown on a real gcc static PIE in tools/test_appimage_elf.py). repack-appimage.sh already put the staged bytes back, but --check covered only
engine-bundle and node, nothing ran the rg, and nothing stopped a pack with a damaged one. appimage_elf.py scans for a damaged .dynamic (garbage tags,
RUNPATH on a static PIE, filled relocation table, filler runs) and compares to the staged files; --check now covers control, harness, node and engine-bundle,
scans, and runs the bundled rg; the pack path runs the same check before it squashes. Whether the released AppImages were repacked is outside this repo.
shortTitle() cuts at a grapheme (Intl.Segmenter) or a code point, so a title never ends in half an emoji, a lone surrogate or a ZWJ. lib/store.ts still
cuts with prompt.slice(0, 47) in three places (not edited here: the GUIDATA owner's file); they should call shortTitle. Tests cover ordinary emoji,
ZWJ families, flags, skin tones, keycaps, combining marks, Arabic, Hebrew, CJK, and a JSON save and restore.
Interface size scales the page with CSS zoom on the root. In WebView2 (Edge 156) getBoundingClientRect and innerWidth are screen pixels, but left/top/
width/max-height written on an element are zoomed pixels, so a menu placed from its anchor landed (scale - 1) too far right and down: at 125% on a
1080p window the More menu was entirely off the screen (14 of 18 size and scale cases broken, measured in Edge 156). lib/overlay-place.ts places in
screen pixels (below, else above, else the roomier side shortened, kept inside the window) and divides by a scale measured from the page at the end.
Used by FloatingMenu (More, chat actions, preview file actions), Popover, the Select popup and the two tooltips. No layout or text change at 100%.
'N settings could not be tried: not enough free memory' is the result message (and the Terminal's tuning line) only when some candidates were skipped for memory
in a result that otherwise keeps the current settings; the structured outcome is unchanged and values.not_tried carries the count. Owner decision 11-04.
The window process (only it) is pointed at a fontconfig file that includes the system configuration, rejects *COLRv1* font files inside
<selectfont> and prefers the outline Noto Emoji. Written once under the data folder, only when a COLRv1 font is installed; FONTCONFIG_FILE already
set, STRATA_FONT_CONFIG=off or =<file> leave it alone. fontguard.rs is plain std with unit tests that run on every platform. The three variables
join the crash report. Also: the desktoplog crash-file test asserted the literal 0.2.0 and failed on any other version; it now follows the
crate version. Linux-only wiring (run_window_process) is not compiled on this PC; no WebKitGTK here to show the crash or its absence.
limit.output, CLAUDE_CODE_MAX_OUTPUT_TOKENS and maxTokens are max(4096, context/8): 16384 at 128K, so OpenCode's compaction summary has room (the tested 32K server keeps
4096). The input limit and compaction point stay as tested. Rewriting a config keeps an output cap other than the 4096 older versions wrote. #4528: the documented
provider block is still accepted by OpenCode's schema (deprecated in favour of 'providers'); no change. Idea as in PR #4230, written here.
The engine's flag schema picks which options are valueless: on DeepSeek --mmap-experts on|off|auto keeps its word through derive_config, the one argument
precedence (arg_layers) and a second launch. Approach as in PR #5028, written here.
The pre-pass that gave a hand-copied full-length shard the 'whole' mark also marked a shard left by a run that stopped between the rename and the hash check,
so download() skipped it and the pinned hash was never looked at. A file the pin table names is left unmarked now, and download() hashes and repairs it;
--gguf-dir files and files without a pin keep the mark. Both setup flows call one helper. Approach as in PR #4318.
WKWebView sends compositionend before the committing keydown, with isComposing false and keyCode 229; WebView2 sends the keydown first. The
composer asks a small guard (lib/ime-enter.ts): a key that says it is composing or has keyCode 229 belongs to the IME, and the first Enter in the
few milliseconds after a compositionend is the commit; the flag is used up by the next key and expires, so the next Enter sends exactly once.
The guard runs before the command menu too. Replay tests for WebView2, WKWebView, WebKitGTK, Latin input, Shift+Enter, a cancelled conversion
and shortcuts. No visible text.
Windows: the shell subscribes to WebView2 ProcessFailed; macOS: to the web content process termination. The page's own renderer dying is
written to the run log (kind, reason, exit code, the address it showed) and the page is loaded again after a short wait: the same screen at
/?recovery=1 first, home at /?recovery=2 second, and nothing after two recoveries in two minutes (the window is left as it is). The decisions
are in src/recovery.rs (unit tested). The page reads ?recovery=n before it renders (lib/recovery.ts), removes it from the address, and starts
with the sidebar previews closed until the person opens one or another chat. The chats are untouched. No visible text.
#5056 the worker is started without STRATA_API_KEY, so it answers to the key the tuner made for it. #5122 a candidate that adds no memory needs what the base run
started with (min of the old peak-RSS rule and base free memory plus the candidate's delta). #5078 a 5xx from the worker adds the engine log's last error lines;
an allocation failure is an OOM, other aborts fail that candidate alone.
pick_slot also recognises an earlier message boundary of a slot's held stream (the last question replaced), so the request no longer overwrites another chat's idle slot. A boundary every held conversation shares (one client's system prompt) names no owner; busy slots are never picked. Tests: branch affinity, busy owner, shared root, interleaved branches (retained prefix 96000 -> 115200 of 115200 tokens).
Two chats in one workspace folder: the end-of-turn sweep of chat A listed the files chat B had just written as
chat A's deliverables, and the other way round. A registry shared by the turns of the host (FolderClaims) now
records which chat's file tool wrote which file; the sweep skips files another chat claimed, and for turns that
overlapped in the same folder it also leaves out files nobody claimed (a script's output cannot be told apart),
rather than give them to the wrong chat. A turn that ran alone behaves as before. Chat records, tool cards and
approvals were already keyed by chat id and session id; a store-level test with two interleaved live chats and
the same call id in both pins that down. The per-folder bucket, the shared model cache and the approval-policy
notice described in the issue are Harness and engine behaviour, not changed here.
Every SYCL start died with TypeError: unexpected keyword 'announce' because install_two_card_spec wrapped engine_args(cfg) while serve/server.py calls it with exe
and, for the memory budget check, announce=False. The quiet call prints no line. Tests run the real base function under the wrapper.
Approach from PR #5075. --hf failed on every repository with AttributeError: 'Header' object has no attribute '_size'; a cut or huge-length header is now a ValueError that names the file.
Approach from PR #5040: _close_verdict holds while </tool_call> is still arriving, and the streaming scan of a body whose end is known runs as final. #4701 (final call) and #4702 (longer code fence) are already fixed on the base; the corpus gets the four 4701 specimens (PR #4762 and #5040 fixtures) and runs them whole, in pieces and cut at every byte, with streamed arguments on and off.
_drm_vendor_backend skips a make whose every card reports a small own memory (the 512 MiB carve-out of an APU) while another make has a card;
intel_arc_cards reads each entry on its own, takes card10 and up, and counts an unlisted Battlemage/Alchemist id as an Arc of the family's smallest memory.
The launcher's merge of the Arc beside other makes was already in place. Backend half as in PR #5184, written here.
The sidebar parsed and rebuilt the whole file on every render of the chat. It now renders through PreviewMarkdown: memoised by the text, cut into slices at block boundaries (each memoised, keyed by place and content), two slices at first and two more per waiting step (one timer, cancelled by a newer text), a block above 40000 characters and a file above 200000 shown as plain text. No visible text, no layout change.
Explicit launch values, then persisted overrides (user_args and hand edits to the launch config), then calibrated values (recorded by
engine calibrate --apply-result with the model and context they were measured for), then Auto. derive_config builds the argv once through
strata_control/arg_layers.py; explicit values go to user_args so the DFlash2 policy keeps them, and are not remembered when left out next time.
Failed stops are counted apart from stopped ones; the empty-list line is for zero targets only. No wording changes: the existing lines are reused.
Approach as in PR #4568.
running_launches binds prof before the try so a failed load uses this row's defaults; kept_key skips list rows that carry only an error
and keeps looking for a valid key. Approach as in PR #4562 and #4563; tests use real temporary profiles in both orders.
Approach from PR #4204. sd-cli at the pinned stable-diffusion.cpp commit reads --prompt-file as raw bytes (checked in examples/common/common.cpp at 2988060). The job folder is the 0700 mkdtemp one; the file is 0600, created exclusively and removed with the job's other files. The stand-in sd-cli reads the file; a canary test checks the recorded argument list.
Approach from PRs #4500 (one redact_object call before validation) and #4599 (none is no key, whole word, own case). tools/strata_bench.py gets the same none rule and key matching.
The 8-character floor of #4599 is not taken: a short key is still removed where it stands alone.
requirements.txt and the four hash-locked terminal locks (windows, linux x86_64, linux aarch64, macOS arm64) move to 13.0.0; Python 3.10 stays on
12.3.0. Image.open() in the vision path, the EXIF probe and the DeepSeek picture reader now only opens JPEG, PNG, GIF, BMP, WebP, TIFF and AVIF, so a
JPEG 2000 file never reaches OpenJPEG on any Pillow version. Approach from PR #4220 (version split by Python range).
The saved chat list cut long fields, dropped the oldest messages of a long chat and left old chats out of the
list when it passed 3.5M characters, and every restart trimmed the chats again (12 messages of 140,000
characters came back as 6). Now each chat has its own storage entry holding the chat exactly as it is, the index
only keeps the order, and a chat the browser refuses keeps its last good copy, stays whole in memory, is tried
again at every save and is reported (state.sessionSave, unsavedSessionsJson). The old single-entry list is moved
over chat by chat and a chat leaves it only after its own entry was written. Approach from PR #5192 (never remove
chats) kept; the field clipping it still did is gone.
- gpu_roles.describe(card_numbers=True) prints "GPU <n+1> (--gpu <n>)"; the two test_launch.py assertions are back on the old digits plus the hint. The Terminal has no translation catalog for this line.
- split_engine: the refusal sentence ends "has 7 GiB free)" when the free memory was read (the size, as before, when it was not).
- the desktop app asked `drafting plan` / `drafting fetch` before it unloads the running model, so its own engine counted as busy memory on a model switch and the plan could pick DFlash2 and fetch the 1.2 GB drafter. New flag --engines-unloading (hidden from --help), sent by native-control.mjs for a load that unloads the running instances (not for a model added beside them); same effect as Runner.card_probe. Logic only, no text.
- tests: python (drafting plan with and without the flag), node split-app.test.mjs (flag on the argv, a 2-card model switch, a model added beside).
- discovery.prepared_beside(gguf, data_dirs): after the models-folder walk it also looks in --data-dir and the remembered data folder for mtp/rt, the pack and the picture reader (setup writes them there); plan, download_plan and the own-files preview pass them. A model folder outside the data folder no longer asks for the 5.2 GB layer a second time.
- split_engine: Card.free_mib from the same nvidia-smi call (memory.free; AMD: the render node's vram_used), fit = min(size, free) - 700 MiB; busy cards get the layer split with DFlash2 and no draft-layer part. Runner.card_probe leaves free memory out while a server of this launch (same profile or port) is about to be stopped. Free cards decide as before.
- plan: the cards line prints the numbers --gpu takes and leaves out cards CUDA_VISIBLE_DEVICES (AMD: ROCR/HIP_VISIBLE_DEVICES) hides; the split probe respects the same variables when no card is named. Digits only.
- tests/terminal/test_fix3_0212.py (41 tests); launch_support.Home clears the visibility variables.
Logic only, no text changed: bench_tips takes the GPU and leaves the tip out for those configs; older cards, AMD, a layer split and no GPU information keep it. The explicit auto:32768 warning is unchanged. test_setup_bench_tips covers it (10 passed on l620).
The gfx12 rule in generate.cpp now serves both: nv_chunk.hpp (STRATA_NV_CHUNK=0 or STRATA_DEFAULTS=026 keep 8192, =16 keeps 16384, =1 skips the RAM guard), the fit scan still picks a smaller chunk when the cache cannot lend the buffers, the #2090 RAM guard steps 32768 -> 16384 -> 8192, and a layer split takes 8192 back (STRATA_SPLIT_CHUNK16K=1 keeps the ceiling). An explicit --prefill N, auto:N or STRATA_PREFILL_AUTO_MAX is never touched. Volta / Turing / Pascal keep 8192.
A6000 UD-Q4_K_XL, 13.1K of 24.6K experts in VRAM, MMQ_KQ on: prompt 20K +43.4%, 32K +22.5%, 64K +21.3%, 8K 0.0%; cards that hold every expert: flat.
nv_defaults_test covers the ceiling, the switches, the card rule and the step-down.
An integrated GPU cannot report how much room is left (the pool reads 0 MiB free on a start that works). With a chunk above the 16384 default that the person or the environment names (STRATA_PREFILL_AUTO_MAX=32768, --prefill auto:32768) and a UD-Q4_K_XL cache that fills the pool, the driver could not create a stream (PAL failed to submit CMD) and the first launch after the prompt buffers died with HIP 719. Once the prompt path's own buffers are in place the engine now tries one stream, fill and wait; if that fails it prints one line naming the chunk, the cache and the 16384 alternative and ends with exit code 2, before any prompt is read (HIP builds, integrated GPU, own buffers, chunk above 16384 only; the 16384 default, CUDA and Metal are untouched; STRATA_PF_OWN_PROBE_OFF=1 skips it). The failed context cannot be reused, so the engine cannot step down in the same process.
The #1630 warning about a named chunk of 8192 or more no longer prints on gfx1151 (it said a bare --prefill auto stops at 6144, stale since 0.2.8); it is printed once the device is known. STRATA_TRACE marks around the prompt path and the buffer size of the chunk.
setup: the bench tip for --prefill auto:32768 is not said on an APU (apu_hides_chunk_tip). The tuner's prefill_32k arm needs a discrete GPU. The app's saferStart does not retry a start that ended with the chunk refusal (the same chunk would fail again, and a memory setting is not the cause). Tests for all three.
- drafting.blocked_by(named=True): only --pipeline-windows still blocks; Metal keeps the old rule (no per-request drafters there)
- apply/launcher/plan pass the choice; the all-cards split engine runs --draft dflash2 --draft-model PACK only for DFlash2 by name AND --split-engine on
- the split engine's own DFlash2 start line counts as "DFlash2 on"; a split run config with --draft dflash2 is left as it is
- app Drafting hint and Terminal --drafting help say it (owner-approved wording, English only)
- tests: terminal drafting/single/split-parallel, split engine, serve parallel, node parallel-default
kNvKqDefaultOn is true; STRATA_MMQ_KQ=0, STRATA_NV_KQ_DEFAULT=0 and STRATA_DEFAULTS=026 turn it off, a STRATA_MMQ_KQ the user sets is kept, Volta / Turing / Pascal keep the FP16 prompt path. A pack without K-quant experts never reads the variable.
Start check (RTX PRO 6000, UD-Q4_K_XL, no STRATA variable set): the start prints the K-quant MMQ line and loads libstrata_kq; 4K prompt GPU timeline 1,047 ms (dequant 7.3%) against 1,500 ms (dequant 25.1%) with STRATA_MMQ_KQ=0, prompt 2,854 vs 2,233 tok/s (one request each).
Owner 11-03 ("3 Yes"). Since 0.1.44 setup wrote STRATA_ROUTE_TAIL_SKIP=0 into the run config of every Unsloth file whose experts are all in RAM. On Linux gfx1200 / gfx1201 it no longer
does: the engine's own default (rank 7, applied only while the variable is unset, so =0 still turns it off) runs. UD-Q4_K_XL on one R9700: decode +6.2% (2 rounds x 8 classes, interleaved) and a
median +15.6% over the 61 suite tasks; the suite scores 56/61 against 55, 57 and 56 for the default arms; start line "STRATA_ROUTE_TAIL_SKIP=7 is ON (default)". Teacher-forced decode-window
KL against no skip 0.0773 (noise floor 0.00000, top-1 91.1%; agent segment 0.20) on a static, low-hit-rate cache, an upper bound for real use. Windows and other cards keep the 0.
unsloth_default_env takes the card's arch (default "": the old behaviour); test_tail_skip_stays_on_for_linux_gfx12 pins the rule.
gemm_dequant_side_parity failed with the cache because its synthetic scratch (10.5 M elements) is smaller than four of its weights, so uncached they take the sliced path and cached one full-N GEMM. The engine's scratch is 32 M elements and every dense projection fits it. gemm_w16_parity runs the engine's scratch, shapes and types, cache off against on, bitwise. STRATA_PF_W16_CACHE_MIB=0 turns it off.
UD-Q4_K_XL's Q4_K gate/up and Q5_1 down experts read the prompt through the FP16 dequant path plus cuBLAS on CUDA (STRATA_PREFILL_TIMING, 8K prompt on an A6000: dequant 21%, gemm gate/up 24%, gemm down 13% of the GPU timeline).
With STRATA_MMQ_KQ=1 (libstrata_kq): RTX PRO 6000 (sm_120) prompt 4K +37.7%, 8K +25.8%, 20K +34.3%, 32K +32.6% (n 4 per cell, 2 rounds, all won); A6000 (sm_86) 4K +1.7%, 8K +9.0%, 20K +8.1%, 32K +11.2%. First-token KL against the FP16 path over 24 prompts of 2K: 0.0243 (PRO 6000) and 0.0351 (A6000), top-1 23/24 and 22/24.
nv_kq_default_env / apply_nv_kq_default: compute capability 8.0 and up (lowest card decides), off switches STRATA_MMQ_KQ=0, STRATA_NV_KQ_DEFAULT=0, STRATA_DEFAULTS=026; kNvKqDefaultOn stays false, so nothing changes by default.
Conflict in src/split/src/prefill.cu (pf_ring and the ring wiring): kept both sides. The HIP all-to-all small-chunk path stays, guarded by !own_out (the own-rows output is the NVIDIA hyper-connection split's), and the ring runs with nvdual's own_out argument; in the next-card wiring the HIP a2a box/flag copies and the hyper-connection split's input pointers are both kept.
Owner 11-03 ("do all 5"), two prompt defaults for Linux gfx12 (R9700), both changing rounding slightly and both with their off switch.
STRATA_MMQ_KQ=1 joins the gfx12 value-changing defaults (gfx12_extra.hpp, applied by apply_arch_defaults, so a raw engine gets it too; Linux only like the
other approved gfx12 prompt defaults; STRATA_MMQ_KQ=0, STRATA_GFX12_DEFAULTS=0 or STRATA_DEFAULTS=026 turn it off, a value the user sets wins). The experts of
Unsloth's UD-Q4/Q5/Q6_K_XL files (Q4_K, Q5_K, Q5_1, Q6_K) then run their prompt products on llama.cpp's MMQ kernels instead of being dequantized to FP16 and
multiplied by hipBLASLt (UD-Q4_K_XL ran 25K dequant launches and ~50K small Tensile GEMMs per 8K prompt). R9700, same job, interleaved: prompt 4K +10.3%,
8K +14.3%, 32K +16.8%; first-token KL against the FP16 path 0.031 over 24 prompts of ~2K (agent turns 0.065), top-1 23/24. A pack without K-quant experts never
reads the variable. The fused variant (STRATA_PF_FUSED_KQ) stays a flag.
The gfx12 ceiling of a bare --prefill auto goes from 16384 to 32768 (gfx12_chunk.hpp, pure functions with tests). A 20K prompt is one chunk instead of
16384 + a 3.6K tail, and every chunk of 1,024 tokens or more streams all the experts the card does not hold. R9700, IQ3_S, 3 rounds interleaved: 20K +12.1%,
64K +2.6%, 32K +1.3%, 8K +0.9%; UD-Q4_K_XL 20K +26%; first-token KL against 16384 at most 5e-5. The lent slots are taken only for what a prompt needs and are
refilled after it; the RAM guard (#2090) now steps 32768 -> 16384 -> 8192 until the extra loan fits. STRATA_GFX12_CHUNK=16 keeps 16384, =0 keeps 8192.
gfx12_extra_test covers both (gfx only, Linux, off switches, user value wins, the ceiling and its step-down); docs/DETAILS.md lists them.
The Linux gfx12 matrix-core prompt path hard-set the streamed-expert ring to 96 slots (measured before the fused IQ3_S kernels). A layer's streamed
experts want a deeper ring at small and middle chunks. R9700 (gfx1201), IQ3_S, 3 rounds x 2 reps interleaved, prompt tok/s against ring 96: ring 256
4K +12.5%, 8K +2.4%, 2K +1.4%, 32K -0.4%; ring 192 4K +11.8%, 8K +4.4%, 32K -1.3%. New default build against the old one: 2K +2.3%, 4K +16.1%,
8K +2.7% (3/3 rounds each), 20K and 32K +0.1%, decode unchanged. UD-Q4_K_XL (MMQ): ring 192 4K +6.9%, 8K +9.5%, 20K +4.0%.
The ring is staging depth only: first-token full-vocabulary logits are bit-identical (md5) for ring 96 / 192 / 384 on 6 prompts of 8K to 64K; greedy
128-token hashes with static residency are identical on UD-Q4 (12/12) and, on IQ3_S, the few divergent requests occur in ring-96 engines as often
as in ring-192 engines (the known HIP timing divergence of an engine's first requests).
Chunks of 12,288 tokens and more keep 96. The ring is capped at ~800 MiB of blobs. STRATA_PREFILL_RING=N and STRATA_GFX12_RING=0 restore the old value.
Only the Linux gfx12 branch (matrix-core prompt attention active) changes; gfx1151 and Windows keep their rule.
Host: draftingPlan/draftingFetch no longer pass --no-split (only when told noSplit, after the draft layer for the split could not be downloaded); the start asks Control
(splitEngine) to turn the run config into the split engine's before the profile is saved, the second try after a failed start is never split, the load waits at least 10 minutes
for a split start, and the Details line says what runs (or that the server started the layer split). A failed MTP layer fetch ends in one plain note and the 0.2.10 layer split
plan. Screen: the split word counts with two or more cards; the download note and the Drafting hint say Auto is the split with the MTP draft layer (text only, two new strings in nine languages).
Tests: tests/split-app.test.mjs (fake Control, 13 tests); drafting.test.mjs and ui-r3.test.mjs updated.
`strata split-engine apply --config IN --out OUT` turns the run config the app built into the split engine's (same steps as strata launch: rule, strata-split-N beside the
engine, draft layer on disk, lanes 1..8 from "parallel"); nothing is written on a no. Shared helpers in split_engine.py (exe_finder, draft_layer_present, apply_run_config,
split_plan_line), used by the launcher too. `strata drafting plan` asks the rule with the choice in hand (a saved DFlash2 under an Auto on the screen is Auto), says split for
an explicit MTP as the Terminal does, and uses the Terminal's plan line; expected() refuses a conversation memory other than int8 as the start does. --no-split stays.
Tests: AppPlanEqualsTerminalPlan, AppStartsTheSplit replace AppDoesNotSplit; docs/SPLIT_ENGINE.md describes the app.
Strata's gdn_tc.cuh scan (mma.m16n8k16 f16 hi/lo split, state in registers, 2048-token super-blocks, prep kernel per value head) ported
into the split engine's prompt GDN (gdn_tc_prompt.cuh, part 4, CUDA sm_80+; HIP keeps the sub-chunk kernels). FNX_PFX_GDN_TC=1 runs it in
place of k_gdn_ch_prep / k_gdn_ch_rec when the card has the scratch for it. split_gdn_tc_test checks outputs and end state against the
sub-chunk kernels on random inputs (one card slice, C 33..32768) and times both. FNX_GDN_TIME=1 prints the prompt GDN's GPU time per card.
App: `strata drafting plan/fetch` gain --no-split, and every drafting call the host makes (control-runtime.mjs draftingPlan, draftingFetch) carries it, so Auto in the app on
2 NVIDIA / 4 R9700 cards is the layer split with DFlash2 alone, no draft layer is planned or fetched for the split, and the Models screen label and fit say DFlash2 again
(capabilities.dflash2Split stays false). The Terminal's plan is unchanged (split + MTP). The gguf_dir fix of installMtpLayer and the explicit-MTP path stay.
Lanes: split_config cuts the lanes to 1..8 (the engine refuses "--batch must be 1..8"): the AMD four-card default PARALLEL 32, a layer config's own --batch (IQ3_S on 4 R9700
gave 32 and fell back to the layer split) and a profile parallel above 8; serve/server.py parallel_args' split branch cuts the same.
Tests: tests/terminal/test_split_parallel.py (AppDoesNotSplit, LaneClamp), apps/host/tests/drafting.test.mjs.
Found live on the AppImage (srv118): after an app install from the person's own GGUF folder, a plain Auto load on two cards (the plan says split + MTP, the layer is missing) ran installMtpLayer, which asked the installer for the model by name without gguf_dir; the installer started downloading the 111 GB model into the app's data folder (stopped after 3.5 GB). installMtpLayer now passes gguf_dir = the folder of the run config's --native. Test: apps/host/tests/drafting.test.mjs.
The Qwen family's picture file is also looked for in the folder above --gguf-dir and in that folder's models/ (pictures.places() already did);
Qwen3.6 links another size's copy of its picture file instead of fetching it again; on a Mac, when the installed DeepSeek engine reads no pictures,
the download total printed before the engine step is corrected (nothing is fetched before that point); the early Coder builds' temporary-repository
picture file (972,207,968 B, the same tensors) is accepted as the shared one (DSV41_ALT_FILES). test_dsv41_mac_vision's step(5) search starts at
the engine line (it found an earlier family's step 5).
App (apps/host): a plain Auto load (no choice carried, none saved but Auto) fetches the draft layer when `strata drafting plan` says mtp_needed and split,
the same as the Terminal; a Load that carries a choice is as before. probeDrafting reads the plan's split word (capabilities.dflash2Split); the install's drafter
(resolveDrafter) takes it too, so a new install on 2 cards leaves DFlash2 out and gets the MTP layer. drafting.ts / native-settings.mjs
resolveDrafter(choice, family, served, split): Auto is the MTP where the split takes the start, a DFlash2 the person chose stays; NativeModels.tsx passes it, so the
label and the fit use the MTP. `strata drafting plan` gains "split".
Launcher: when the server starts the normal layer split because the all-cards split engine would not come up (metrics engine.split_fallback), the final summary
replaces "Using the all-cards split engine on N cards" with "The all-cards split engine could not start (<the engine's own reason>); running the normal layer split."
Tests: apps/host/tests/drafting.test.mjs; tests/terminal/test_split_parallel.py (PlanTellsTheApp, SummaryTellsWhatRuns).
The split runs the model's draft layer (--mtp) and exits at start without it ("sfnx: --native and --mtp are required"). The plan and the fetch for Auto
resolved to DFlash2 (drafter 1.2 GB, no draft layer), so on 2 x sm_86/sm_120 or 4 x gfx1201 the split was chosen at start, failed, and the server fell back
to the layer split after "Using the all-cards split engine on 2 cards".
(B) strata_control/split_engine.expected(): the rule apply_to_config uses, asked before anything is on disk (model assumed when its file is not there yet).
Runner.split_expected / resolved_drafting and drafting_inputs: Auto where the split will take the start is the MTP, so download_plan and the fetch include the
draft layer (5.2 GB) and not DFlash2's drafter, setup gets --mtp on without --drafter dflash2, and the plan says "Drafting: MTP (the all-cards split engine uses
the MTP draft layer, 5.2 GB)". The app's `strata drafting plan/fetch` (drafting_cmd._inputs) follows the same rule. A person's own DFlash2 / off, --split-engine
off, one card, other cards, three cards keep what they had.
(A) Runner.apply_split_engine: when the --mtp folder is missing or empty at start the split is not picked (Auto: silently, the layer split with DFlash2 starts as
the plan said; "on": one line saying to add --drafting mtp). _auto_gates() is the Auto-only part of the rule, shared by apply_to_config and expected().
Tests: tests/terminal/test_split_parallel.py (Terminal and app plan paths, AMD 4 x gfx1201 and 2, one card, sm_89, own DFlash2, off, missing draft layer).
The split runs the model's draft layer (--mtp) and exits at start without it ("sfnx: --native and --mtp are required"). The plan and the fetch for Auto
resolved to DFlash2 (drafter 1.2 GB, no draft layer), so on 2 x sm_86/sm_120 or 4 x gfx1201 the split was chosen at start, failed, and the server fell back
to the layer split after "Using the all-cards split engine on 2 cards".
(B) strata_control/split_engine.expected(): the rule apply_to_config uses, asked before anything is on disk (model assumed when its file is not there yet).
Runner.split_expected / resolved_drafting and drafting_inputs: Auto where the split will take the start is the MTP, so download_plan and the fetch include the
draft layer (5.2 GB) and not DFlash2's drafter, setup gets --mtp on without --drafter dflash2, and the plan says "Drafting: MTP (the all-cards split engine uses
the MTP draft layer, 5.2 GB)". The app's `strata drafting plan/fetch` (drafting_cmd._inputs) follows the same rule. A person's own DFlash2 / off, --split-engine
off, one card, other cards, three cards keep what they had.
(A) Runner.apply_split_engine: when the --mtp folder is missing or empty at start the split is not picked (Auto: silently, the layer split with DFlash2 starts as
the plan said; "on": one line saying to add --drafting mtp). _auto_gates() is the Auto-only part of the rule, shared by apply_to_config and expected().
Tests: tests/terminal/test_split_parallel.py (Terminal and app plan paths, AMD 4 x gfx1201 and 2, one card, sm_89, own DFlash2, off, missing draft layer).
The split's run config always carries --batch N. ensure_profile still wrote the launch profile's parallel=<card fit> (4 on 32 GB or more) beside
it, and strata serve --profile (cli._effective) refuses a parallel override next to --batch: every 2-card NVIDIA start with the split on Auto
stopped with "parallel override conflicts with --batch/--slots in run config args" before the engine ran. Same for the AMD four-card default.
- runner.ensure_profile: no parallel override when the run config already names its slots (run_config_owns_slots)
- runner.apply_split_engine: the person's --parallel N is the split's lane count (cfg["parallel"] for the rule; taken out again on a refusal)
- cli._effective: a parallel override on the split's run config (split_engine_note) is not a conflict (the app's Concurrent requests); other configs keep the check
- server.parallel_args: "parallel" on the split's run config sets its --batch
- tests/terminal/test_split_parallel.py: Terminal path, app path, AMD four-card, lanes from the flag, the check kept for other configs
fit_start treats a discrete AMD card (backend hip; an APU gives no card memory and is left alone) like CUDA: the KV window walk, the working-cache threshold and
single requests priced from a measured base. The HIP base is 3900 MiB, measured on an R9700 (gfx1201) held by a memory hog: the engine's free VRAM before the expert cache is
the squeeze minus 5045 MiB from 7000 to 5200 MiB (CUDA: minus 6324), so a HIP start holds about 1.28 GiB less. DFlash2 alone at 128K with setup's window of 32768:
7942 MiB 787 slots 864 MiB free 48-52 tok/s; 7000 439 slots; 6400 273 slots (loan keeps 16); 5800 head on the host (31 tok/s); 5200 and 4600 refused with the plain line.
An 8 GB AMD card therefore starts as set up; a 6 GB one walks the window.
The draft-layer drop, the 16-slot prompt-path loan, the own-buffer fallback and the head-to-host move run only on a discrete GPU (strata::spill_allowed:
not Metal, not an integrated GPU such as Strix Halo), with a CHECK for the gate in arch_defaults_test. qsa_kv_resident_min is 4096 (20480 on a Metal build;
STRATA_KV_RESIDENT_MIN overrides): windows of 4096 and 8192 gave the same tokens as 20480 with the cache fixed. fit_start walks the window 20480 / 8192 / 4096 only as
far as the card needs. When every step leaves the cache no slot, the output head (Q6_K, 0.41 GiB) moves to mapped pinned host memory (about 3x slower decode).
Measured on the 8 GB mimic at 128K: DFlash2 alone 391 MiB free, 63-70 tok/s, 32K prompt 19.8 s; MTP asked: layer dropped, 555 MiB free, 61-71 tok/s, 32K prompt 13.4 s;
2 requests 277 MiB free, 46 tok/s per user; 4 requests 171 MiB free, 15 tok/s per user. 12 GB cells unchanged.
A card whose expert cache stays below the 128 slots a prompt-path loan keeps behind used to be squeezed (reserve lowered to 100 MiB free, then the first long
prompt aborted in an mmq allocation) or gave the prompt path 600 MiB of its own buffers and stopped. The loan now leaves 16 slots, the auto lend cap is lifted
for such a card, and every 128-slot floor in the lend, request-time lend and VRAM-shrink paths follows it (g_lend_keep). When the draft layer, the head and a
working cache with room left do not fit in the free VRAM before the layer loads, the start runs without the layer and says so (STRATA_MTP_KEEP=1 keeps it).
An out-of-memory in the layer or head load with --batch names --batch and --max-context. Measured on an 8 GB mimic at 128K with --kv-resident 20480: DFlash2
alone 391 MiB free, 69-73 tok/s, a 32K-token prompt in 19.8 s; MTP asked: layer dropped, 557 MiB free, 62-70 tok/s, 32K prompt in 13.4 s. 12 GB unchanged.
A short section next to the Linux setup: what Strata's own downloader does, the measured speeds (anonymous, 16-24 MB/s whatever the method), and the manual route
with `hf download` and `--gguf-dir`, which setup already supports (checks the SHA-256, never deletes a file in your own folder). No code change.
The context ladder, the fewer-requests step and the one-request-at-a-time fallback of the earlier fit are gone. A start short of VRAM keeps its
context and its number of requests and is moved off the GPU by the backend's knobs (CUDA: the KV window down to the engine's 20480; Arc: the prompt
chunk, then the reserve, then the window), with one plain note. CUDA single requests are priced from the measured base per drafter (DFlash2 alone,
DFlash2 with the MTP layer, MTP, none). Measured on the 8 GB mimic: DFlash2 alone starts at 128K with --kv-resident 20480 and not with 32768.
BASE_SLOTTED_MIB gains (cuda, flash-next) 4990 from the 12 GB and 8 GB CUDA cells; a CUDA start with request slots needs a working cache (263 slots) not the one-slot refusal limit; the one-request fallback keeps the largest context a single request runs (16K on 8 GB); plan() tries fewer requests (down to 2) before one at a time; the draft-layer and head loads name --batch/--max-context when they run out of memory.
drafting.DRAFTER_MIB gains dflash2_selq4 (715); drafting.vram_gb(data_dir, engine) gives the true figure (an engine program that carries 'selector codebooks: Q4_0 rows' and a Q4_0 pack that setup / Load converts), strata drafting plan reports vram_gb and selq4 and its text says the same number, Control passes capabilities.dflash2Selq4 to the page and the fit (apps/host/src/lib/native-fit.ts DFLASH2_SELQ4_VRAM_GB, apps/host/server/native-parallel.mjs) uses it; apps/host/tests/drafting.test.mjs reads DRAFTER_MIB from drafting.py and fails when a copy differs.
Marker per backend: CUDA and HIP builds carry the text (src/core/dflash2.cpp); the SYCL drafter (sycl/src/core/dflash2.cpp, bf16 select kernel) does not read Q4_0 rows, its built binary (P1006 rc1sycl) has no such text so it stays on the bf16 pack, and its loader now refuses a Q4_0 codebook with a plain message instead of tiling it as a matrix. Metal: DF2-METAL's f-0291-metal-selq4. The idea of the Q4_0 selector is nazeshinjite's (nazeshinjite/Qwen3.8-Flash-Next-DFlash2-GGUF).
IQ3_S on 2 RTX A6000, 32K and 62.9K prompts, four pairs against the same build without it: 3.3 to 5.6% faster. Only the memory path changes: the merge reads
device memory instead of the mapped host buffer, so the logits are unchanged (checked on the 53-prompt set).
The selector's two codebooks as Q4_0 rows take 174 MiB (about 88 expert slots) less graphics memory than bf16, with the same drafts: the committed text was byte-identical to the bf16 pack's in 40 of 40 requests (greedy and sampled, everything resident), draft time and acceptance equal, and ahead 1.7-2.2% where the cache is short (RESULTS-DF2MEM.md section 15).
The idea and the bytes are nazeshinjite's: the community GGUF nazeshinjite/Qwen3.8-Flash-Next-DFlash2-GGUF (Apache-2.0) stores the two tables as Q4_0, and tools/dflash2_pack.py --q4_0-selector / --convert-selector writes exactly those bytes (selector_predecessor sha256 55f70049...ffd28, selector_successor 3b596669...a72d2 over the tensor bytes, equal to the GGUF's).
drafting_fetch.convert_selector (called by setup, strata drafting fetch and Load, never by a start or a serve; no network; about 120 MB of memory) checks the Q4_0 pack against PACK_SHA256, converts it in-process and checks the result against CONVERTED_BYTES / CONVERTED_SHA256 (339694592 bytes, 4a42b982...3ecd); anything else is deleted and the bf16-selector pack keeps running. drafting.pack_path returns the converted pack only when it verifies (a marker beside it keeps the hash check off the start path) and the engine program carries the text 'selector codebooks: Q4_0 rows'; a missing, short or corrupt file, an engine without the text or an engine that is not known gets the Q4_0 pack. Includes f-0291-df2-comm (Q4_0 rows in the select kernel, STRATA_DF2_HEAD=native) merged into rc1.
DF2-METAL found that SUPPORTED_FAMILIES and the model-name pattern left Unsloth out, so UD-Q4_K_XL and UD-IQ4_XS (the same Qwen3.8-Flash-Next weights, and the case the
DFlash2 numbers on Strix, R9700 and M5 Max were measured on) started the MTP everywhere.
- strata_control/drafting.py: "unsloth" is a supported family; the name pattern also matches qwen3.8-flash-next-unsloth-<model> (what setup.py and metal/setup_mac.py write:
f"{family name}-{model.lower()}"). Swift (UkisAI's fine-tune), Coder (ISTA-DASLab's coding version) and NVFP4 stay on the MTP. The stale "Unsloth" in the "another model"
comment is gone.
- The app's two copies of the rule follow (apps/host/server/native-settings.mjs, src/lib/drafting.ts): the Drafting dropdown and the memory estimate treat Unsloth like Qwen.
- Tests: Unsloth names on cuda, hip, metal and sycl resolve to DFlash2 alone with the vocabulary; Swift, Coder and NVFP4 names stay MTP with their arguments untouched; the names
are read from setup.py and metal/setup_mac.py; the terminal flow gets the Drafting step for Unsloth; tests/drafting.test.mjs.
On a Strix Halo the prompt path keeps its own buffers at the 16,384-token chunk (prefill_own_keep_chunk) and the cache sizing already reserved them. The
step-down before the prompt path init read cudaMemGetInfo after the pinned RAM complement (which the unified pool counts twice): 860 MiB "free" in the MTP
engine (no chunk fits, the chunk stays, prompts read at 955 tok/s) but 1.7 GiB in the DFlash2-alone engine (no MTP layer, a slightly larger cache), where
1,536 fit: chunk cut to 1,536, prompts at 303 tok/s (UD-Q4_K_XL, 30K prompt 98.9 s against 31.4 s; 8K 28.3 s against 10.8 s). A real allocation failure still
steps the chunk down (the init loop). Only reached when prefill_own_keep_chunk is set: Windows HIP gfx1151 with a bare --prefill auto.
The Monitor tab (serve/web/app.js), the API monitor page (monitor.js, monitor.html) and the Terminal Overview (strata_cli/overview.py) show the token-weighted
average of the run instead of the last request's speed when the server is idle; live values are unchanged. Ported by hand onto rc1 from the community patch
by keinebenutzer: https://github.com/keinebenutzer/STRATA-average-patch (MIT, Copyright (c) 2026 keinebenutzer), made against v0.1.55 c62d5e3a. Read in full
(no network, telemetry, eval or markup in what it adds); none of its files was run.
Tests: serve/test_speed_average.py (the page code run in node, display-only guard), tests/terminal/test_overview.py (edge cases, fallback).
split_config writes "drafting": "mtp" (what the split engine runs), so serve/server.py's drafter step, which resolves Auto to DFlash2 where the
engine has it, cannot add DFlash2 flags to the split engine's arguments; the fallback keeps the person's own drafting word (or none: Auto), so the
layer split that starts instead gets DFlash2 as usual. The rule refuses (quietly in auto) for an explicit dflash2 or off, and for Auto on a card
where the split has not been measured to decode faster than the layer split with DFlash2. Tests: all four cases, the card rule, the server's
drafter step on a split config.
1 The version dropdown no longer shows the "Recommended" tag (the order and the Downloaded tag stay).
2 Gyro-S with Auto sent --mtp-draft-vocab FILE and no --mtp; engine 0.1.55.2 only takes --mtp DIR for the vocabulary and ended with exit code 2.
(a) strata_control/drafting.py reads the engine program (never starts it) for the start message that says what it takes: the vocabulary file alone, --mtp DIR
(the MTP layer still not loaded), or no --draft-fallback at all (then no DFlash2 flag is sent). serve/server.py hands the program to the rule.
(b) An Auto start that picked DFlash2 and ended before it was ready is started once more with the MTP, in one quiet line ("Drafting: MTP (DFlash2 didn't start)",
nine languages); a DFlash2 the person named fails with the engine's reason.
(c) tests: tests/terminal/test_drafting.py (new class), serve/test_drafting_retry.py (a fake engine process), version-picker test.
strata launch --split-engine auto|on|off (settings field, saved in the preset and written to the launch config, so an off survives updates);
STRATA_SPLIT_ENGINE=0/1 for testers. auto (the default) uses the split engine where the rule says yes and says nothing where it says no; on says
why it cannot run; off never. The rule also leaves the layer split for choices the split engine does not honour (DFlash2, custom engine arguments,
replicas, a conversation memory format other than int8). The engine is looked up per card count and architecture (strata-split-N-smCC, then
strata-split-N); sm_120 joins sm_86 as a tested architecture. Tests: modes, auto/on/off, silent refusals, the flag, a saved off.
serve/server.py: when the split engine fails to start, the one line now says why in the engine's words (its 'sfnx: failed:' line) and the
split engine's log is kept beside the normal engine's (it wrote the same file). src/split/src/session.cpp: FNX_DUMP_FIRST_LOGITS=PATH writes
each request's first-token full-vocabulary logits to PATH.<n>, for comparing the prompt path with another engine's.
dflash2_gemm_bench q4 on an R9700 (gfx1201, p620, 8 columns, cold weights, bitwise vs variant 1 in every cell): gate+up 56 -> 44.5 us, down 39 -> 23.4, qkv 26.8 -> 21.4, o 23.6 -> 14.9, fc 61.5 -> 35.2, kv 15.2 -> 10.0, akp/mkp 12.1 -> 6.3, selector 11.1 -> 5.2.
Serve A/B (128K, 8 classes, 3 rounds x 2 reps): drafter 2.5-3.3 -> 2.2-2.8 ms a round, +1.0% tok/s over the old variant (n 6 per class). CLI greedy ids identical across old / new. Other cards keep 17. gfx1151 and gfx1100 compile.
Before this, a start with such a file (UD-Q6_K_XL) read 55 GB of embeddings and spent more than 25 minutes loading before it gave up, so the
server's fallback to the normal layer split came very late. The check runs when the file is opened, before anything is read or placed.
EXPERT_TYPES gains IQ2_S, IQ3_XXS, IQ3_S, IQ4_XS, IQ4_NL and Q2_0; a set with such experts may have embeddings, shared experts and an output
head in any type the loader converts to Q8_0; the memory estimate counts the Q2_0 to Q4_0 repack twice; the per-layer embedding table type is
checked. Tests for the supported sets (the IQ3_S set, UD-IQ4_XS), the plain refusal lines and the scan.
On 2x RTX A6000 (IQ3_S, MTP, three residency levels) a 32K prompt reads 37-55% sooner chunk-major and an 8K prompt is level (DF2-MGPU, boston); the default had been on for a layer split since 0.2.7 from a 2x A4000 measurement with streamed experts only. Owner's yes 2026-11-02. STRATA_PREFILL_LAYER_MAJOR set by hand still wins both ways; a single card, HIP and the small-card rule (which sets the variable itself) are unchanged.
Setup writes absolute paths into the run config (cwd and --expert-profile into the Control bundle that ran it, the picture reader and library folders into
an engine package, the DeepSeek package). An update keeps the new Control/engine and the one before, so the second update after setup removed the folder
the config names: the model showed as not installed and a start failed ("Qwen breaks each time Strata is updated").
strata_control/cfg_paths.py is the one resolver: a string that names a file in components/control/<ver>, components/engine/<ver>-<kind> or
engine-dsv41/<ver>-<kind> whose version folder is gone is mapped to the same file in the active Control / the newest engine of its own kind. Paths the
person typed, packs, models, output paths (--expert-profile-save, log) and folders that still exist are left alone.
- profiles.read_run_config applies it in memory, so the doctor, the model list, the launch plan and the host see the current folders.
- runtime.start writes it once into every saved run config, one line in the host log, keeps a caller's expected source digest valid, and stops with the
file named when it exists in no current version.
Tests: tests/terminal/test_cfg_paths.py (real update_control twice, real runtime.start for the Terminal and the app, DeepSeek); 9 of 11 fail without the wiring.
strata_control/drafting.py: "sycl" joins DFLASH2_BACKENDS, so Auto picks DFlash2 (Q4_0 pack, no MTP layer, --draft-fallback none) on an Intel engine.
BACKEND_CONF = {"sycl": 0.6} replaces the CUDA slot steps for Intel (A750, 298 expert slots, serve sweeps: 0.6 was the best of 0.35 / 0.6 / 0.8); a --draft-conf
of the person's own still wins. An Arc A-series card (A3xx / A5xx / A7xx, told by its Windows adapter name or its Linux PCI id) counts as tested; Arc B-series,
Arc Pro and integrated graphics keep "has not been tested on your graphics card yet".
sycl engine: --mtp-draft-vocab is accepted in place of --mtp for the draft vocabulary (the app passes no --mtp with DFlash2); STRATA_DF2_TIMING also prints the
request-end context update and each prompt context update, STRATA_DF2_NO_PROMPT_CTX (measurement only) skips the prompt's context update.
dflash2_bench: a ctx mode that times the prompt's context update.
Measured on the A750 (IQ3_XXS, --prompt-cache 6, 5 rounds): DFlash2 alone is 2.6% behind MTP in decode tokens per second (95% interval -4.8 to -0.4) and 1.1%
behind over time; a fresh 4K prompt reads 4.6 s slower. Tests: tests/terminal/test_drafting.py, test_drafting_hardware.py, test_issues_022.py (117 pass).
On l620 a 49 GiB cgroup limit and the 46.84 GiB arena: the engine's check (arena + blob <= limit) passed, the arena is pinned and never given back, the
engine's own memory (about 3 GiB) came on top, and the cgroup OOM killer ended the engine at the start (journal: "A process of this unit has been
killed by the OOM killer", twice). With the fallback allowed the arena now needs arena + blob + max(RAM to keep, the fallback headroom) under the
hard limit (cgroup, container, job object, --memory-limit-mib), else the RAM copy runs. Without a fallback (hand-set --ple-io ram, shared arena file,
STRATA_ARENA_RAM_FALLBACK=0) the comparison is unchanged. arena_hard_limit_need in ple_residency.hpp, unit cases for a limit just above the arena.
An engine built with CUDA 12.8.61 faults in the prompt path on an RTX PRO 6000 (compute capability 12.0, driver 580) with the batched PLE post-ops on, which is the
default: "an illegal memory access" at every prompt length from 94 tokens, first error in the native PLE postops launch (CUDA_LAUNCH_BLOCKING), and the isolated
verify_batch_parity test faults on native_ple_postops_batch_snap with 4 tokens. STRATA_PLE_BATCH=0 runs. The same source built with CUDA 13.0 (the released
cuda13 package) runs the default at 94 to 20K tokens, where the batch is ~23% faster prompts, so only the 12.8 toolchain changes: ple_batch_default(cudart_version, hip,
major, minor) is false only for CUDART_VERSION 12080..12089 on cc 12.0 (not HIP, not another card, not another toolkit), ple_batch_resolve lets STRATA_PLE_BATCH win in both
directions, and prefill.cpp says once when it turned the batch off. Known issue #220 / #224; CMake already warned for sm_120 builds with CUDA < 13.0. Unit test in
arch_defaults_test; docs/OLDER_GPUS.md.
"Leave 20% for my work" reads the version of the engine the Engine tab selected (snapshot engine version, or the folder named in the run config's exe).
An engine of RAM_KEEP_MIN_ENGINE (0.1.56) or newer gets the env STRATA_RAM_KEEP_GIB = clamp(ceil(0.2 x RAM GB), 5, 8). An older engine, or one whose
version cannot be read, gets the rule of 0.2.9 unchanged: --resident-budget-gib from the 80% rule and a STRATA_RESIDENT_HEADROOM_GIB of 20%; neither
is ever given an argument it does not know. Switching engines between loads swaps the rule; full performance removes either. Tests at 24, 32, 64 and 128 GB
for both kinds of engine, with the preference on and off.
HF discussion 30 ("the model's pack is not complete ()", 7900 XTX, files converted by ISTA's script):
- dsv41_pack_info: an engine that prints nothing is reported by how it ended (signal 11, exit code N), never as an empty string.
- The engine's file check is repeated in Python (dsv41_pack_problems); a pack that does not fit the model files (missing or short file, a file replaced
since the pack was made: pack-source.json size and time, unreadable description) is rebuilt once with one line; when only the engine's own check gave
no answer the pack is kept, one warning carries the reason and the test question decides. The failure line is one line and names the file (the
launcher shows only the first [X] line).
- Both layouts whole (what ISTA's converter leaves) use the six-shard files; without --gguf-dir six-shard files in setup's own folder are seen when all five are there.
- The 0.2.9 Linux DeepSeek package holds code for gfx1200/gfx1201 only: hip_archs_from_pci is a third probe for the card; an unnamed card gets one plain line,
a known card without code is told so (install, install_package), setup warns for its own card pick and reads BUILD.json archs beside a hand-made engine.
Tests: tools/test_setup_dsv41.py (PackCheck, CardsAndLayout, CardGate), tests/terminal/test_dsv41_engine.py (HipCards).
1 Read pictures is a plain checkbox; no hint, no download button (Load does it); a failed picture download keeps one line under the box.
2 A checkbox tick is white in light mode (dark mode unchanged).
3 The progress card shows the step, the numbers and a thin bar; Stop is a text button; no "buttons wait" sentence; reduced motion honoured.
4 The Read pictures choice is kept per version and no longer re-derived from host reports while a job runs; the reader probe keeps its last answer.
5 No separator under Read pictures.
6 Running chat: an SVG ring before the chat's name.
7 The chat's time and token count are a wrapping flex row without fixed widths.
8 A run with many files is one block: delivered-to folder, working folder, and a count that opens the list.
Also: "Leave 20% for my work" is now "Leave room for my work" (hint: keeps 5-8 GB free), nine languages (gui-<lang>.json).
lm_prepare no longer declines for the DFlash2 taps on one stage. lm_run takes a layer's completed output when the next layer starts (the hyper-connection write of its FFN half), puts each sub-chunk's rows at positions >= tap_first_ in the staging, and hands the super-chunk's rows to on_taps in position order when all its layers are done. A layer split and a multi-prompt read still decline. Untested on a GPU at this commit: the equivalence test and the A/B are running.
Measured with no MTP layer loaded against the MTP at equal VRAM (l620 mimic, IQ3_S, chat with a rewind plus four cold prompts, 3 paired rounds): best confidence 0.8 at 1.4K and 3.3K engine slots, 0.65 at 6.6K, 0.5 at 9.2K, 0.5 and 0.35 level at 15.7K; 0.35 loses to the MTP up to 6.6K slots. An explicit --expert-cache N is a budget of largest blobs, about 1.3 N engine slots on IQ3_S, now converted before the steps apply. The slot estimate no longer deducts the drafter from a baseline that already holds the MTP layer, because the DFlash2 is the only drafter.
An engine older than --ram-keep-gib (0.1.52, 0.1.54, 0.1.55, which a person can choose in the Engine tab) stops with "unknown argument";
it ignores an environment variable. The engine still takes the flag for CLI users (the flag wins over the env). Full performance removes the env;
a STRATA_RAM_KEEP_GIB, --ram-keep-gib or RAM mode of the person wins. New host test: an older engine gets no argument it does not know.
gr_down_pf_launch and gr_down_rb_launch set cudaFuncAttributeMaxDynamicSharedMemorySize inside a function-local `static const bool once` lambda. The attribute belongs to
the device: only the first card to launch got it, and on the second card of a layer split the launch of a window of 5 or more rows (51-82 KB of dynamic shared memory
against the 48 KB default) was rejected with "invalid argument", which inside the graph capture surfaced as "fused_gr_read_multi v3: operation failed due to a previous error
during capture" and an engine that answered every request with no tokens. Reproduced on 2 x RTX PRO 6000 (CUDA 12.8): the int3 head with the default mask fails at the
6-token window capture; STRATA_GR_V3=0 and one card work. set_attr_once_per_device (one flag per device, under a mutex) replaces both lambdas; no kernel, launch geometry or
arithmetic changes. Every other cudaFuncSetAttribute site of the tree was already per device (listed in phaseA-tests/blackwell/RESULTS-DF2NV.md section 9).
tools/test_func_attr_scope.py (pytest, CPU) fails if cudaFuncSetAttribute / hipFuncSetAttribute appears inside a function-local static lambda or a std::call_once.
Engine: --ram-keep-gib W (default 0; STRATA_RAM_KEEP_GIB the same). The expert arena is used only if W GiB stay available after it;
otherwise the RAM copy of the hottest experts leaves max(W, the safety headroom) free, and STRATA_RESIDENT_HEADROOM_GIB still wins when set.
The safety headroom is 8 GiB on Windows (the page trim of the copy needs the room) and 4 GiB elsewhere; with W = 0 nothing else changes.
The decision lives in ple_residency.hpp (ram_keep_plan and its parts) and is covered by ple_residency_test over free RAM x W x arena size.
The explicit modes (--resident-budget-gib, --resident-experts) leave max(W, their headroom).
Host: profile 'work' passes --ram-keep-gib W, W = clamp(ceil(0.2 x RAM GB), 5, 8), instead of the 80% budget and the 20% env headroom; the engine
decides from the RAM free at its start. A flag or env of the person wins; full performance passes nothing and takes the earlier version's settings out.
resource-ram.test.mjs covers 24, 32, 64 and 128 GB with the preference on and off. Docs in DETAILS.md.
The default path on a small expert cache streams every expert again for every 512-5,888-token chunk. On an RTX PRO 5000 held to 1.3K / 1.6K / 1.9K engine slots the unfused experts plus layer-major read a 20K / 32K prompt -44% / -50%, -41% / -42%, -14% / -12% sooner (a real 12 GB 5070: -26% at 20K); from a default chunk of 8,192 (2.3K slots) the default path wins and the change costs +1..+25%, so the rule keys on that chunk (strata::small_card_prompt_path, unit-tested at its boundaries). Output changes at rounding level (first-token KL against the FP16 prompt path 0.012-0.018 for every path including the default, top-1 47 of 48), owner's yes 2026-11-02. STRATA_PF_FUSED / STRATA_PREFILL_LAYER_MAJOR set by hand win, STRATA_PF_SMALL=0 turns the rule off; a layer split, HIP, a non-native pack and STRATA_DEFAULTS=026 keep the default.
split_moe_tile_test runs the prompt kernel's weight tiles on a GPU for one expert of each routed-expert type and tests/split/tile_check.py compares them with gguf-py. split_fnx_run is sFNX's logits tool on the same engine objects.
log_on_ defaulted to true and alloc() allocated the pinned staging; a drafter that never ran alloc() (the --parallel slots' clone_alloc) therefore ran the
context update's two D2H copies into a null pointer: an illegal memory access on its first draft. The log is now off by default, init_ring_log() (called
by alloc(), callable by a clone's own allocation on its device) allocates the staging and turns it on, and a drafter without it keeps the pre-log rule.
A request that ends on EOS or max_new skips the draft that would have made the last accepted window's context rows, so every turn left a hole of a + 1
positions (5 a turn, accumulating) that a later turn resuming after it found blank and the log could not rebuild. The serve loop now updates those rows
(one synchronous context update, ~0.4 ms) before it finishes the request: a four-turn agent loop plus a parked return now rebuilds with 0 rows missing.
The cold line prints only when 64 or more rows of the window are missing. copy_log_state_from(src) moves a lineage's log, tag shadow and epoch with a request
between two drafters (solo <-> a --parallel slot). STRATA_DF2_RING_STATS=1 prints the live log, the stash and each rebuild per request.
The draft ring has 2,064 slots (position mod 2064). A request that resumes below the highest position written (a repeated or rewound prefix, a
parked conversation brought back) found the part of its first draft's window [n - 2047, resume) overwritten by later positions or never written,
and the drafter drafted poorly until it refilled: on an RTX PRO 6000 with the prompt cache on, DFlash2 alone was +14% over MTP on cold and live
turns but +3% on restored prompts and -2% on an exact repeat (acceptance 0.63 and 0.59 against 0.79 and 0.70 with the cache off). The repeat case
printed no warning at all, because the token history was equal.
The K/V of the five layers is a function of one row per position, the quantized hidden_norm output (2560 int8 codes and 80 block scales, 2.9 KB;
the K/V is 40 KB and the taps 51 KB). Every context update now copies that row into a chunked, copy-on-write host log of the lineage (a
pinned staging copy inside the captured graph, moved to the log after the stream sync). begin_request rebuilds, with the same kernels on the
same bytes, every slot of the window below `resume` whose tag does not match (a host shadow of the tags says which): the rebuilt K/V is bitwise
the original update's (dflash2_ring_test: rewind, parked, branch, evicted; 578 rows in 4.0 ms, a whole window of 2042 rows in 12.7 ms).
A lineage replaced by another is moved, not copied, into a stash (STRATA_DF2_LOG_MIB, default 256, oldest evicted); a request whose prefix matches a
stashed lineage swaps it back in; a rewind or branch keeps the old branch as a copy-on-write stash entry. STRATA_DF2_RING_LOG=0 restores the old
behaviour. Rows never logged are reported (cold) instead of silently blank.
With DFlash2 as the drafter the MTP stays loaded as the fallback (use_mtp stays true), and three decisions tested use_mtp where they meant
"the MTP chain drew this round's candidates":
- STRATA_SPEC_COST=1 (a GB10 default): T = clamp(chain_rows, 1, S_mtp), but chain_rows is only written by the MTP chain callback, so with DFlash2
and no --draft-conf the window stayed at one row (CLI and serve; code_py 175 vs 258 tok/s on an RTX PRO 6000, serve 166-230 vs 283-397).
- STRATA_SPEC_PROB=1, sampled requests: ver.set_spec_q(mtp.spec_q()) judged DFlash2's deterministic candidates against the MTP's stale q lists
(CLI: 107 of 137 windows), and serve's gated path was not declined for DFlash2.
- the lookup choice read mtp.prob() for the same reason.
spec/window_rule.hpp holds the window rule and the q rule, both loops call it. The default path is unchanged: window_rule_test compares the new
rule with a copy of the old code over 9216 inputs (only the DFlash2 + cost rule + no --draft-conf cases differ). The CLI stats also print the
window histogram for any rule that sets the width and, when STRATA_SPEC_PROB judged windows, their count and source.
The rule asked ram_gb + KV + 1 (64.8 GB for IQ3_S, a 64 GB PC reads 63.1), which left a small card with the KV in VRAM and 720 expert slots at 128K. It is now the smaller of that and arena_gb + LOW_RAM_HEADROOM_GB + KV (62.1 GB for IQ3_S; the KV's RAM comes out of the engine's RAM copy or file cache, not on top). It never asks more than before; --kv-streaming on|off and WSL are unchanged; the size stays 32768.
Measured (phaseA-tests/gh-sweep-1031/KV-128.md): IQ3_S, 12 GB mimic on l620 under a 45 GiB cap, 756 -> 1,267 slots, 32K prompt 21-34 s -> 12-17 s, 118K prompt 76-122 s -> 45-63 s, decode flat to +10%, engine RAM unchanged; on the owner's RTX 5070 (single runs) 924 -> 1,585 slots, 32K prompt 130 -> 79 s, RAM copy 30.8 -> 28.9 GiB, no paging. Decode there is within noise.
Tests: need numbers, never-more-than-before over every model/context/KV, the rule at the edges and for WSL, setup writing --kv-resident on a 63.1 GB PC with a 12 GB card and not at 61.9 GB or at 32K. The golden baseline gets --kv-resident 32768 for the 64 GB / 32 GB card IQ3_S and recommended entries. Setup and tuning tests on boston: same 24 failures as f-0291-int3 (not touched), 7 more pass.
The fallback's RAM copy (previous commit) now uses the --resident-budget-gib form: the budget is what is available minus the headroom
(default 8 GiB, STRATA_RESIDENT_HEADROOM_GIB changes it), so the experts outside the copy are read unbuffered instead of through the mapped
view whose pages grow the working set until available RAM is near zero. On Windows the complement copy releases the mapped pages to the
standby list every 4 layers (the VirtualUnlock call that already ran once after the copy): available RAM fell to 0.9 GiB for ~10 s during the copy
on a 64 GB PC with 44.6 GiB available. No data changes.
Measured on the RTX 5070 PC (128K, 44.6 GiB available): lowest available RAM 4.05 GiB, 2,000-token answer 25.2 tok/s (9.4-10.1 without the fix).
Auto and the default pick DFlash2 with the Q4_0 pack for Qwen3.8-Flash-Next and Gyro; Intel SYCL stays MTP (one line, DFLASH2_BACKENDS). The GUI dropdown, TUI, strata launch, setup (--drafter), serve/server.py, the host's Load and the drafting command agree. A new install fetches only the selected drafter, a switch fetches the other once (never at start), the draft vocabulary comes with the DFlash2 pack. A layer split or several requests load only the MTP and say so in one line; no drafter on disk says so too. Fit, plan and slot estimates use the chosen drafter (0.89 GB DFlash2, 1.08 GB MTP).
Decode: ggml's vec_dot for IQ4_XS, IQ3_S, IQ3_XXS and IQ2_S added to the mmvq expert launchers beside IQ4_NL and Q4_0.
Prompt: weight tiles for Q4_0, IQ4_NL, IQ4_XS, IQ3_S, IQ3_XXS and IQ2_S in moe_mma_iq.cuh (ggml's dequantization rounded once to f16).
Loader: Strata's Q2_0 down experts are repacked to Q4_0 blocks (lossless, twice the bytes).
When the expert arena does not fit the available RAM (#4846), the run read every expert through the plain file source with no RAM copy:
9-10 tok/s on the owner's 5070 PC (44.6 GiB available for a 46.84 GiB arena). The same engine with a RAM copy of the hottest experts
(--resident-budget-gib 40 / --resident-experts) gave 24-31 tok/s at the same RAM. The fallback now sets the --resident-experts flags
(resident_cpu_experts, resident_pin, resident_soft, 4 GiB headroom) when a static expert profile is set; if the file cannot be opened the
flags are cleared with the rest of the fallback. STRATA_ARENA_FALLBACK_RESIDENT=0 keeps the plain file source.
Not built, not run: the patch is untested.
Adds the IQ2_S, IQ3_XXS, IQ3_S, IQ4_XS and Q2_0 types to the GGUF type table and a
loader step that, only when a file has routed experts in IQ2_S, IQ3_XXS, IQ3_S or Q2_0,
dequantizes its dense matrices, shared experts, projections and embeddings (K-quants,
IQ4_XS, IQ4_NL, BF16, F16) and writes them again as Q8_0, and the routers, injections
and SSM gates as F32. Other files are not touched. The routed IQ expert kernels are not
there yet, so the set still falls back.
Checked against gguf-py on the ISTA IQ3_S file: 762 tensors converted, the sampled ones
all within the Q8_0 bound (F32 targets exact), every source type covered.
Per-run source choice, no mid-run fallback. With the MTP loaded the DFlash2 gathered a second copy of the 106299-row draft head (212.9 MiB on Flash-Next); it now uses the MTP's when the type, the vocabulary ids and the layout match, and the expert cache is sized without the copy. The cache sizing also prices the DFlash2 head from the vocabulary file when there is no MTP.
With DFlash2 as the drafter the MTP stays loaded as the fallback (use_mtp stays true), and three decisions tested use_mtp where they meant
"the MTP chain drew this round's candidates":
- STRATA_SPEC_COST=1 (a GB10 default): T = clamp(chain_rows, 1, S_mtp), but chain_rows is only written by the MTP chain callback, so with DFlash2
and no --draft-conf the window stayed at one row (CLI and serve; code_py 175 vs 258 tok/s on an RTX PRO 6000, serve 166-230 vs 283-397).
- STRATA_SPEC_PROB=1, sampled requests: ver.set_spec_q(mtp.spec_q()) judged DFlash2's deterministic candidates against the MTP's stale q lists
(CLI: 107 of 137 windows), and serve's gated path was not declined for DFlash2.
- the lookup choice read mtp.prob() for the same reason.
spec/window_rule.hpp holds the window rule and the q rule, both loops call it. The default path is unchanged: window_rule_test compares the new
rule with a copy of the old code over 9216 inputs (only the DFlash2 + cost rule + no --draft-conf cases differ). The CLI stats also print the
window histogram for any rule that sets the width and, when STRATA_SPEC_PROB judged windows, their count and source.
Per-run source choice, no mid-run fallback. With the MTP loaded the DFlash2 gathered a second copy of the 106299-row draft head (212.9 MiB on Flash-Next); it now uses the MTP's when the type, the vocabulary ids and the layout match, and the expert cache is sized without the copy. The cache sizing also prices the DFlash2 head from the vocabulary file when there is no MTP.
With DFlash2 as the drafter the MTP stays loaded as the fallback (use_mtp stays true), and three decisions tested use_mtp where they meant
"the MTP chain drew this round's candidates":
- STRATA_SPEC_COST=1 (a GB10 default): T = clamp(chain_rows, 1, S_mtp), but chain_rows is only written by the MTP chain callback, so with DFlash2
and no --draft-conf the window stayed at one row (CLI and serve; code_py 175 vs 258 tok/s on an RTX PRO 6000, serve 166-230 vs 283-397).
- STRATA_SPEC_PROB=1, sampled requests: ver.set_spec_q(mtp.spec_q()) judged DFlash2's deterministic candidates against the MTP's stale q lists
(CLI: 107 of 137 windows), and serve's gated path was not declined for DFlash2.
- the lookup choice read mtp.prob() for the same reason.
spec/window_rule.hpp holds the window rule and the q rule, both loops call it. The default path is unchanged: window_rule_test compares the new
rule with a copy of the old code over 9216 inputs (only the DFlash2 + cost rule + no --draft-conf cases differ). The CLI stats also print the
window histogram for any rule that sets the width and, when STRATA_SPEC_PROB judged windows, their count and source.
Per-run source choice, no mid-run fallback. With the MTP loaded the DFlash2 gathered a second copy of the 106299-row draft head (212.9 MiB on Flash-Next); it now uses the MTP's when the type, the vocabulary ids and the layout match, and the expert cache is sized without the copy. The cache sizing also prices the DFlash2 head from the vocabulary file when there is no MTP.
With DFlash2 as the drafter the MTP stays loaded as the fallback (use_mtp stays true), and three decisions tested use_mtp where they meant
"the MTP chain drew this round's candidates":
- STRATA_SPEC_COST=1 (a GB10 default): T = clamp(chain_rows, 1, S_mtp), but chain_rows is only written by the MTP chain callback, so with DFlash2
and no --draft-conf the window stayed at one row (CLI and serve; code_py 175 vs 258 tok/s on an RTX PRO 6000, serve 166-230 vs 283-397).
- STRATA_SPEC_PROB=1, sampled requests: ver.set_spec_q(mtp.spec_q()) judged DFlash2's deterministic candidates against the MTP's stale q lists
(CLI: 107 of 137 windows), and serve's gated path was not declined for DFlash2.
- the lookup choice read mtp.prob() for the same reason.
spec/window_rule.hpp holds the window rule and the q rule, both loops call it. The default path is unchanged: window_rule_test compares the new
rule with a copy of the old code over 9216 inputs (only the DFlash2 + cost rule + no --draft-conf cases differ). The CLI stats also print the
window histogram for any rule that sets the width and, when STRATA_SPEC_PROB judged windows, their count and source.
- With a loaded model that runs without pictures (no internet, no room), the existing "Picture reading needs a few more files" offer says the one short line
instead of a second warning beside it; the offer is hidden while a download or load runs.
- "Download picture-reading files" ends with a toast; a model that is loaded without pictures is told to load again.
- Nine-language catalog: three more sentences. UI preview fixture: context 128K with its marker; the Chrome driver reports dialogs that are new after the click.
A model that is downloaded, in its own GGUF folder, with Read pictures on (the default) and no picture file started an install job on Load:
`--data-dir <app models folder>\Strata-data --gguf-dir <own folder> --vision yes`. That re-checked the model files (finish marks written in the
person's own folder), prepared a second pack and downloaded the draft layer into the wrong data folder, and rewrote data_dir in app-settings.json.
- strata_control/pictures.py (new): finds the pinned picture file (size and SHA-256; beside the model, one folder up, in the model's data folder) or
downloads it once into <data folder>/models, shared; the data folder is the one the model's run config names (packs/<tag>, mtp/rt). Works out the run
config's vision section for the engine in use with the same rule as setup (CPU reader under 24 GB, GPU reader from 24 GB, AMD/Intel Arc/Mac readers).
No internet is one plain sentence after a short probe; a start never downloads (acquisition guard).
- native-control.mjs: Load (kind load, vision on) fetches the files through Control, writes the vision section, shows plain-word progress with a bar and
loads. No internet, no room on the disk or a failed download: the model loads without pictures, nothing saved about pictures, one short line on the
page. A restart or a tuning run never downloads. kind pictures does the same without a load (the "Download picture-reading files" button).
A model whose only gap is a deleted picture file is no longer "missing files". installArgs: an installed model is prepared again in the folder its
own run config names, never the app's models folder, and that folder is never written to the settings; the Drafting plan looks there too.
- Models page: the Load shortcut no longer needs the picture files prepared for the families Load can fetch for; the button and the line use the new kind.
- Tests: tests/terminal/test_pictures.py, apps/host/tests/load-pictures.test.mjs, UI preview fixture scenario, Chrome driver script, nine-language catalog.
Coder: expert-pruned 256-expert Coder, M5 Max, 5 interleaved pairs, identical greedy tokens in all 40 runs per class, over --spec 2: story -2.5%
(0/5), code +7.1%, JSON +9.3%, agent turn +12.5% (5/5 each). It has no Mac setup entry yet; the name is what a config for it would pass.
fp16 decode (PR 4807): setup writes STRATA_METAL_F16=1 into the config's env for a resident IQ3_S or Q2_0 pack (STRATA_METAL_F16=0 there turns
it off; streaming and the other sets are untouched). The owner's rule was an option unless it passes the 61-task quality suite: off / on / off
again on IQ3_S 54/61 each, on Q2_0 54/61 each, the same 7 tasks failing in all arms, no family drop, no new hallucination flag, 0 pass/fail
changes between two runs of the same arm; decode +14% (IQ3_S) and +15% (Q2_0) in the suite runs, top-1 96-97% and mean KL 0.009-0.016 against fp32
(phaseA-tests/mac-q/RESULTS.md sections 20, 22, 30).
On a Strix Halo under Windows the first cudaGraphLaunch of a doorbell window sometimes never returned: the GPU had already rung layer 0
("GPU rang 1, flags served 0") while the main thread, which serves the doorbells, was still inside the launch call. HostGraphLaunch (the
CUDA instrumented-launch helper, STRATA_VERIFY_ASYNC_LAUNCH=1) is now compiled for the Windows HIP build too, and arch_defaults sets the
switch for Windows gfx1151 (launch_default_env; STRATA_GFX1151_DEFAULTS=0 or STRATA_VERIFY_ASYNC_LAUNCH=0 turns it off, a user setting is kept).
Measured on the strix box, UD-Q4_K_XL, default queues, 19,853 slots, interleaved within one job: old launch 14 hangs in 75 starts
(t2 4/20, t5 3/25, release build 7/30), helper-thread launch 0 in 75 (+ 8/8 on the final build); decode 48.2 vs 48.4 tok/s; output tokens
identical; IQ3_S 8K prompt 1,413-1,427 vs 1,419-1,423 tok/s with identical output; quality quick subset PASS, agent loops 2/2.
Also STRATA_LAUNCH_PROBE=1 (diagnostic): launch-call time, GPU doorbell state during the call, window graph node count.
(cherry picked from commit 93b9264e30054996d1cb9e89d27d211ed1156b15)
The default changes the answers and gained +0.2% to +0.6% on an Arc A750 at 16K, 32K, 64K and 128K (3 runs per arm up to 64K, one pair at 128K; first-token KL 1e-9, same top-1 and tokens): its prompts are bound by expert streaming over PCIe. Under the 5% bar, so Alchemist and other devices keep the warp kernel unless STRATA_SELECT_GEMM=1. Battlemage unchanged (+19% to +28% at 100K to 128K on 2x Arc Pro B65).
After the 20 s wait and the give-back of the PLE lock, a shortage no longer registers the arena anyway (a 60 GiB host stalled in reclaim until it was power-cycled). The run uses the --mmap-experts expert source, says so in one line with one next step, and the start never refuses. A hand-set --ple-io ram and --shared-expert-arena are kept as asked (warn only); a hard cgroup limit below the arena falls back instead of refusing; STRATA_ARENA_RAM_FALLBACK=0 is the old behaviour. If the file cannot be opened either, the arena loads the old way. Decision rule and tests in ple_residency.hpp / ple_residency_test.
The engine gives 5 experts per word on two or more RDNA4 cards that together hold at least half of the experts (4 x R9700), but strata launch --plan, the Speed screen and the app's Speed label called Full quality the default there.
dsv41_fit.rdna4_faster() applies the engine's rule with the plan's own numbers: one card the run uses (on Windows one AMD card): yes; several: yes when the plan puts at least half of the set's experts on them (3 or more R9700 for every official set; two hold 37-47%, so they keep 6). strata launch uses it with the chosen model; the model catalog carries rdna4_faster per DeepSeek size for the app (NativeSize, Speed label); --speed help and docs/MODELS.md say it. Other makes and older Radeons are unchanged.
One R9700 holds under a fifth of a 2.25 bpw set, so the CPU lane and the SSD set the speed, as on the 16 GiB cards and the multi-card RDNA4 rule that already take 5.
1 x R9700 on p620 (125 GB RAM), 2.25 bpw, frozen expert profile, 5 interleaved pairs, 5 against 6 experts: decode 15.24 against 13.08 tok/s (+16%, 5 of 5 pairs), a scripted agent loop -24% wall time (5 of 5), the long-read loop -12% (5 of 5). KL against the 6-expert run 0.030 (6 against 6: 0.008); against the original model 0.1106 against 0.1067-0.1071. quality027 quick: 18 of 19 with 5 experts and with 6, the same task flagged in both.
ds4_experts_rule.hpp: experts_per_word() takes single_rdna4 (the engine passes rdna4() with one device); DS4_DEFAULTS=026 gives the model's 6 back. The Terminal's Speed words (strata launch --plan and screens), the app's Speed label (arch added to RunCard) and docs/MODELS.md say the same. Several RDNA4 cards keep the existing rule (they must hold half of the experts); the screens still do not mirror that one.
In the single-request speculative paths the accepted rows of a verify window were committed first and the output loop cut to max_new or EOS afterwards, so a request's last window could park a snapshot with tokens the client never received. conversation_prefix() needs the whole snapshot to be a prefix of the next prompt, so the snapshot was refused and the older checkpoint was used (about 6% of a 12K-token prompt read again).
serve_stop_bound() caps the accepted rows at the rows the request can still emit and ends at the first EOS, before pl_commit_async. Merged prompt windows keep committing every row. HIP only (STRATA_USE_HIP), where it was measured.
Patch and policy test (2,736 cases against the old loop) by hanchengme, from the issue's attachment. Two changes on our side: it is on by default, STRATA_SERVE_STOP_BOUND=0 turns it off; the test is registered in CMake.
Measured on 4 x R9700, Qwen3.8-Flash-Next IQ3_S, 2 replicas x 2 cards, --batch 8 --batch-groups 2, four 11-15K-token conversations with 768-token answers, conversations opened one at a time: 4 of 16 continuations fell back to the older checkpoint without it, 0 of 16 with it (reuse 98.36% against 99.74%). Opened together as batch rows both builds miss the same continuations (slot overwritten before it is parked); not changed here.
GuidedSetup filled its family and size once at mount. When the catalog had not arrived yet it fell back to the first size in the list (Q2_0, a 66 GB download) even with an installed IQ3_S on the computer. Only the person's own choice is state now; the start is worked out from the catalog on every render (startingChoice in native-catalog.ts): a ready installed version first (the recommended one if it is among them), else the recommendation. No design change.
The owner's audit now scans every app sheet, and f-0291-ui's appended blocks broke
its rules (flat, tokens only, shadow only on surfaces that overlap content):
- .native-progress (#4802): gradient + --st-sh-pop shadow -> flat --st-accent-tint
fill, the accent border stays (it still stands out; no shadow).
- .approvaldock > .askcard/.toolcard (#4725) and .approvaldock > .askq (#4244): the
--st-sh-bar shadow dropped; the accent border that marks them stays.
- .st-code .st-btn--icon[data-copied] (#4748): the literal rgba(255,255,255,.08)
is now color-mix(--st-code-ink 8%, transparent), the same recipe liquid.css uses
for the code block edge.
- .quote-selection (#4785) floats beside selected words, so it is added to the
audit's ELEVATION_ALLOWED list instead of losing its shadow.
Merged f-gui-look-snap (682c21d64): the owner's uncommitted wt-gui-look work as one
commit on 4cf0d8cd3, 32 files +343/-216 plus the two untracked files
apps/host/src/styles/liquid.css and apps/host/scripts/browser.mjs. Line endings were
normalised from CRLF to LF when the snapshot was taken (the raw working-tree diff of
~14,800 lines is the CRLF flip; nothing of it is carried here). The approved UI list
(f-0291-ui, 14 items) is already in f-0291-int3.
Three-way merge result: no textual conflicts. Files both sides changed, and how each
resolved:
- components/NativeModels.tsx (int3: progress card, context memory + KV hint; owner:
Manage models ghost/sm + compact dialog, "Other local models" class, Expert scroll
waits for layout): disjoint hunks, both kept.
- lib/plain-errors.ts (int3: out-of-VRAM rule; owner: unwrap {"error":..} / HTTP 400
bodies before the rules run): both kept; the unwrap runs first, so a wrapped
out-of-VRAM message still reaches its rule.
- screens/Serving.tsx (int3: two addresses, CopyBtn; owner: empty-state "Connect an
app" is the primary button): disjoint, both kept.
- screens/Settings.tsx (int3: While-an-answer-is-written row, Select import; owner:
Engine panel gets the "Engine" heading): disjoint, both kept.
- styles/app.css (int3: appended blocks + the reading-comfort rule; owner: edits in
place): auto-merged, both kept.
- styles/onboarding.css (int3: language Select rules appended; owner: welcome
headline, badge ink, glow removed): disjoint, both kept.
- components/TuneRunView.tsx also auto-merged (int3 +1 line, owner: progress bar uses
transform).
M5 Max, DeepSeek 2.25 bpw, 3 interleaved pairs: with the lent room a 639-token prompt took 6.2-7.6 s against 4.1 s with DS4_BORROW=0 (same bytes
read, the compute waited for reads equally long), 8K prompts -3.9%, 32K -20% (borrow also selects the 32,768 chunk); and the engine died
("mtl: wrap 7763574784 failed", exit 4) in 8 of 9 runs of a mixed 165/600-token/8K sequence that the build with DS4_BORROW=0 finished 3 of 3.
Only Metal changes (one_gpu_or is under STRATA_METAL); CUDA and HIP keep devs.size() == 1. DS4_BORROW=1 turns it back on.
The plan said 2,947 experts for one R9700 and 13,180 for four; the engine held 2,366-2,566 and 12,147-12,192. It took 4.5 GB of dense weights and 1.15 GB of reserve per card and forgot the driver context, the prompt buffers and the dense weights every card repeats. Constants now come from the engine's startup lines (f-042-maya-rc d93547aed): 2.25 bpw one R9700 2,543, four 12,260; boston 2 x A6000 within 1%, A4000 and RTX 5070 within 5%.
strata launch --model-path on files that stay where they are printed 'tight: needs more disk space' with nothing to download: the menu row counted the whole download against Strata's folder. With own files the verdict now comes from the memory words alone.
Router: ds4_route and ds4_route_publish left an all-NaN gate row's pick at 0x7fffffff and read sc[0x7fffffff] (threadgroup memory) and
vram[0x7fffffff]; the test process dies with SIGBUS on the old kernel. ds4_route_fallback_pick takes the lowest expert not picked yet
(the CUDA/HIP guard's rule) and raises a flag in a buffer of the Metal host; router_nonfinite_take synchronises and reads it
(metal_host::router_bad_take), so ds4_main ends the request with the error it already has. The warning names the GPU on Metal; the
CUDA/HIP text is unchanged. Test: metal/tests/ds4_route_guard_parity.cpp (finite, all NaN, +-inf, half NaN, fewer than k finite).
Dense Q4_K: ds4_gguf.cu rows() sends a dense matrix of a type metal_host::dequant_rows does not know to iq::dequant_rows, whose CUDA
launch has no Metal dispatch and aborts the engine. ds4_dq_q4k (ggml's dequantize_row_q4_K, the Q5_K kernel without the fifth bit) is
added; any other dense type now fails with a plain message naming it. Test: metal/tests/ds4_dq_parity.cpp (Q4_K, Q5_K, Q6_K, Q8_0, BF16
against ref_quant, padded row stride).
Keeps the wide lookup sizes for copy-heavy text (a rewrite matches long) and leaves lists and prose, which match short, at the 8-row cap. Off by default. Tried and reverted in the next-but-one commit: it did not remove the prose cost of 16 rows and cost rewrite gain.
Port of pull request 4737 by InB4DevOps (commit c14ee15d9 on v0.2.8). The mixer's input-only QSA/GDN work runs on graph branches; scheduling only, no arithmetic change.
STRATA_SM121_PERF=0, STRATA_DEFAULTS=026 or STRATA_SIDE_MIXER=0 turn it off. Measured here on a Spark, IQ3_S, greedy: ids identical 48 of 48; alone +1.0..+2.9% per class, with mode 8 +3.6% pooled.
Not yet measured at parallel 4.
Port of pull request 4699 by InB4DevOps (commit 76c0aa601 on v0.2.8). The existing mode 8 kernels are selected for compute capability 12.1 when the sm_121 table is on;
STRATA_SM121_PERF=0, STRATA_DEFAULTS=026 and an explicit STRATA_EXP_MODE keep the old mode 0.
Measured here on a Spark, Qwen IQ3_S, serve, greedy: bit-exact (ids identical 48 of 48), with the side mixer +3.6% decode pooled over 8 prompt classes (4 rounds), mode 8 alone +0.3..+2.4%.
prompt_attn_wmma_kernel was limited by its LDS (21,648 B a block, 5 waves a SIMD), not by VGPRs (amdgpu_waves_per_eu
and amdgpu_num_vgpr change nothing here). Two exact changes, on by default:
- STRATA_PA_TRIM (default 2; 0 off, 1 first level): the score and probability buffers hold the 12 real query rows
instead of 16 (rows 12..15 of the WMMA are zero queries nothing reads; the 4th wave skips the softmax of them),
and at level 2 the staged V rows are 72 bytes instead of 80. LDS 21,648 -> 19,008 -> 17,984 B, 7 waves a SIMD.
- STRATA_PA_RPF (default on; 0 off): chunk ci + 1's selected-cell id and page-table reads are issued before chunk
ci's compute.
The arithmetic and its order do not change. hip_prompt_attn_wmma checksums are identical across all variants (32K
context 8f3a85a9cb885e7e, 8K context de4684fb3f370000), and engine first-token logits with every exact lever of this
branch on against off are byte-identical on a 4.5K and an 8K prompt (IQ3_S pack). Kernel time, gfx1151, int8 KV,
STRATA_PA_FAST: 32K context / 2,048 queries 11.05 -> 9.95 ms, 8K context / 4,096 queries 21.76 -> 18.90 ms.
The idea of raising the kernel's occupancy is the qsa3 change of halo-box/strix-llama.cpp PR 213 (MIT, The ggml authors);
the means here differ and the code is ours.
Needs the owner's yes: the existing opt-in STRATA_SELECT_WMMA=1 with STRATA_SELECT_ALT_MIN_TOKENS=28672 becomes the gfx12 default in
gfx12_extra.hpp, with its own log line. A chunk whose context is below the floor keeps the warp kernel, so a prompt that stays under about
28K tokens is bit-identical (first-token KL 0.00000 on twelve 8K-24K prompts). Off: STRATA_SELECT_WMMA=0, STRATA_GFX12_DEFAULTS=0 or
STRATA_DEFAULTS=026; a variable the user sets is kept. Chosen over PR 3073's simt2 (author Vop3p) on the data.
R9700 (p620, IQ3_S, serve, interleaved pairs, prompt tok/s against unset, 3 pairs): 32K +1.83% (3/3), 98K +5.74% (3/3); first-token KL on seven
30K-96K prompts mean 0.0018, max 0.0055, top-1 7/7; needle recall 25/25 (8K to 96K, 5 depths). Without a floor: 24K +3.34%, 32K +4.5..6.0%,
98K +6.9..7.3% but values change from 8K up (KL 0.0056); simt2 is slower below 32K. arch_defaults_test now clears the variables this
default sets before its integrated-GPU checks; gfx12_extra_test covers the default and its off switches.
warm_flow() runs cublasSgemm / cublasGemmEx on tiny scratch inputs at load. The Metal compatibility layer (metal/include/metal_compat/cublas_v2.h) only has the dispatches the real flow uses: its sgemm_tn found no host view for the scratch buffers and the call ended in strata_metal_dense_gemm_missing(), an abort (exit code -6) on every start, restarted in a loop by the server. start_warm() now returns at once under STRATA_METAL. Found on an M5 Max with the 2.25 bpw set: the v0.2.9 engine answered the four checks; this tree aborted until DS4_WARM=0 was set.
The final hyper-connection mixer of the verify window and of the draft layer as one fused_gr_read_multi instead of one gr_read per
token (PR 3264, author bsorensen110). Kept apart from the main gfx12 table in gfx12_extra.hpp so it merges with the table on
another branch without touching it; STRATA_GFX12_DEFAULTS=0, STRATA_DEFAULTS=026 and a variable the user sets decide first.
R9700 (p620, IQ3_S, serve, static residency, 8 interleaved pairs with an A/A arm): json_extract +0.82% (6/8), agent +0.63% (6/8),
code_py -0.96% (3/8, that prompt's A/A moves 3.5% a pair); greedy ids identical in 16/16 and 15/16 request sets.
On gfx1151 the compiler folded x * x into the first add of block_sum (fma of this lane's x with the other lane's
square), a different rounding than the sum of two rounded squares, so about 4% of rows came out with a scale one ulp
off. An empty asm on the square stops the fold (__fmul_rn compiles to the same fma). Checked in the assembly
(v_mul then v_add) and on the device: hip_qsa_nr_parity 44 of 44 OK, every output element bit for bit, T 1..8191;
engine first-token logits with STRATA_QSA_FUSE_NR / STRATA_QSA_FUSE_GATE on and off are byte-identical on a 4.5K and
an 8K prompt (IQ3_S pack).
The exact-column instantiations 13..16 of the wide kernels (native MMVQ, the wave variant, the packed Q8_0 and Q6_K launches,
the BF16 multi GEMV) are built only above 12, MAX_NCOLS follows the cap, and kVerifyLongT follows a wide cap (a cap of 12 keeps
--lookup-max-t at 12). Measured for the narrow/wide comparison in phaseA-tests/pr-priorities/RESULTS-NV2.md.
Mode 2 of STRATA_SELECT_SIMT was added for HIP in the previous commit as an opt-in. For the RX 6000 cards its contributor measured it
on (gfx103x) it now joins the one community switch: on by default with STRATA_AMD_COMMUNITY_TUNING on, any STRATA_SELECT_SIMT value
the user sets wins, and no other card changes. Values change in the last bits (another summation order); the contributor's numbers are
labelled as theirs in docs/AMD_HIP.md and have not been reproduced (no gfx103x card here). amd_community_test checks the default and
that the switch turns it off.
data/quant_quality/v1/set.ids.json is tokenized for Qwen (ids up to 248,069); fed to the DeepSeek engine it gave nonsense text and, for the
runs that dump logits, a late "token out of range". read_score_ids now takes the model's vocabulary and refuses the file at once, naming the
segment, the id, its position and the fix; quant_quality.py run does the same against its config's tokenizer folder (or the vocab_size that
`ids` now records in the ids file it writes).
The engine's #879 check only sees logits that come back to the host; the Metal engine picks tokens on the GPU, so a half-precision overflow (STRATA_METAL_F16) would show as wrong text, not as an event. With the variable set, each head window's logits are scanned: a line for the first five windows that held a non-finite value, a count every 500 head windows. Off by default, no cost unset.
Controls of glorysdj's pull request #4185: unset means on for a Battlemage device and off for every other device (strata::bm_default_on); a set value still decides. Measured results are in phaseA-tests/pr-priorities/RESULTS-INTEL.md.
Unset, the output is unchanged (the build with the change, unset, gives the same greedy ids as the build without it in 12 of 12 runs, gemv_parity / gr_parity / expert_parity pass). With STRATA_METAL_F16=1 the decode windows of a resident model use half-precision GEMVs (Q2_0, Q3_K-Q6_K, IQ4_XS) and the Q2_0 routed experts.
M5 Max, spec 3 + min-p 0.5, 3 interleaved pairs, decode tok/s unset -> on: Q2_0 story 105.0 -> 114.9 (+9.4%), code 125.6 -> 143.7 (+15.5%), json 130.8 -> 135.1 (+3.3%, one noisy pair), agent 116.1 -> 131.6 (+13.4%); IQ3_S (dense GEMVs only) story 93.1 -> 104.3 (+8.8%), code 103.6 -> 118.5 (+12.5%), json 122.5 -> 135.4 (+10.6%), agent 103.8 -> 117.8 (+12.3%).
It changes arithmetic: teacher-forced over 4,995 positions against the same build unset, Q2_0 top-1 97.12%, mean KL 0.0094 (p99 0.119), perplexity 4.456 against 4.442; IQ3_S top-1 96.16%, mean KL 0.0160 (p99 0.211), perplexity 4.119 against 4.135. The half range is not guarded (see the PR). Off by default.
After an app or Control update the engine is never touched, and the app said nothing. Strata Control keeps what the last update or check found newer
(update_available of 'strata components engine', Qwen engine and DeepSeek engine; read without the network); the host passes it to the app as
engine.updateAvailable and the Models screen shows one card in plain words with the existing Update engine job as its button (disabled while a job
runs or a model is loaded). A DeepSeek engine update is reported as an engine update, not as 'Strata is updated'. JavaScript only; the Control side is
on f-0291-keepcurrent.
c9e0d6688 uses it in a __global__ function; nvcc refuses a constexpr host function there (error: calling a constexpr __host__ function from a __global__ function is not allowed). HIP accepted it, CUDA did not: the f-042-maya-dq line did not build for NVIDIA.
The old kernel (the gfx11 default since 0.2.7, picked for speed) gave different dense weights on every run on a Strix Halo: layer 0 of a
200-token prompt differed by 27% between two runs of the same engine on the 2.25 pack (Q5_K) and by 26% on UD-IQ1_S, while dq_rows_kernel2
is bit-identical run to run and 1.2-1.3% from the window path (GEMV, no dequantizer). A gfx11 device now defaults to kernel2 whatever
DS4_DEFAULTS says; DS4_DQ_OLD=1 forces the old kernel there and elsewhere, but only for a type whose staging fits (Q4_K, Q5_K, Q2_K:
dq_old_ok); Q6_K (840 bytes against the 704-byte buffer) and the rest always use kernel2. 2.25 prompt speed on the Strix Halo: 4K 166.1 -> 164.9
tok/s, 8K 297.9 -> 294.2 (3 runs each, -0.7% / -1.2%). UD-IQ1_S's long prompts and agent loop work.
Port of PR 3307 by gopinath87607 (rebased onto f-0291-int-trial, three commits squashed; the mtp.cpp signatures keep the
gated-proposal DraftResult argument and the prompt-helper arming keeps the failed-allocation cudaGetLastError).
STRATA_MTP_ROOM_MIN=TOKENS (--serve with --mtp): a request that reads at least TOKENS prompt tokens points the prompt
path's ring at the drafter's 512 routed experts while it is read. The block is lent in place: never freed, never allocated
again, never moves, so no prompt path is laid out again and the decode's captured graphs stay valid; the decode's own bytes
come back (a DMA from a pinned copy) before the first window. Unset, nothing of it runs.
Follow-up to the port of PR 4039 by merbanan. The switches stay opt-in and the prompt path is unchanged when they are off;
the lines are the activation evidence the test plan asks for (chunk, rows held).
Port of PR 4040 by merbanan (rebased onto f-0291-int-trial, bitwise by the author's checks).
--lookup-max-t N (up to 16) lifts only the prompt-lookup windows to N tokens; the MTP drafts keep --spec. A window
past kVerifyMaxT tokens runs as two token groups of at most kVerifyMaxT (the --spec-split layout), the head runs in
column chunks, the batched GDN commit takes up to 16 kept rows, MtpDrafter::draft_long catches the drafter up past its
window, and every lookup size is its own window graph (--lookup-sizes A,B picks them; one that finds no VRAM is dropped).
A build whose windows already hold 16 rows (STRATA_WINDOW_MAX_T=16, the HIP default) runs the 16 as one group.
Batch, pipelined and layer-split windows stay at kVerifyMaxT. Without the flag nothing changes.
Port notes: kVerifyLongT = 16 for both window widths, the per-window-size graph arrays keep the trunk's
[sel_long_][T] layout, the head chunk loop keeps fused_gr_read_multi's own slicing, the penalty history is sized for
the widest window only when --lookup-max-t is past 8, and draft_long forwards the gated-proposal view.
Port of pull request 3382 by Vop3p (commit 2b65710f9 on v0.2.0-beta), rebased by hand on the 0.2.9 trial tree.
Conflict in generate.cpp: the share gate goes first in run(), before the co-read branch, and the kva_floors / set_cell_floor bookkeeping after the read is
kept as it is. Addition to the original: a read run on its own thread beside decoding slots (STRATA_BATCH_CONCURRENT_READ=1, run_conc) keeps the share
blocked for the whole read and does not touch the slots' state from the reader thread. Off unless the variable is set.
Ports the two RDNA4 pull requests onto this tree by hand (three-way merge of the rejected hunks). Where the contributor made a
default depend on a device query, the default is the gfx12 table or the env variable instead, so STRATA_GFX12_DEFAULTS=0 still
gives the old behaviour; the LDS-only barriers and the S2 and head changes that were only run on gfx1201 apply to gfx12 only,
every other target keeps what it ran before.
- iq_kernels.cu (3259): LDS-only barriers in the grid / activation staging, first-pass barrier skipped, vectorised ed_stage_entry,
expdec_gu_kernel launch bounds in waves per EU on HIP, EXPDEC down read at first use; expdec_bench builds on HIP.
- s2_expert_grouped.cu (3264): single-entry gu/down specialisation for the draft layer, fused swiglu + q8_0 on the hit paths,
LDS-only barrier on gfx12.
- verify_kernels.cu, quantize_act.cu, elementwise.cu, shared_expert.cu, fused_gr.cu, mtp.cpp, verify.cpp (3264): multi-block argmax
and top-prob on gfx12, warp-per-block quantize_q8_0_scaled, mapped copies in one launch on HIP, shared expert reuses the q8_1
image, counter-free q8_mixed in the hyper-connection read, the draft layer and the head write the q8_1 image themselves.
- parity tests of both pull requests (verify_parity on HIP, s2/shared/gr parity cases).
Ports three community changes that we have no card to run (no gfx1010, no gfx1030 here), on by default under the one
setting STRATA_AMD_COMMUNITY_TUNING (=0 turns the tables off; cmake -DSTRATA_AMD_COMMUNITY_TUNING=OFF compiles the portable
dot for gfx1010 again). Each is limited to the card or exact library build it was made for.
- gfx1010 (RX 5700 / 5700 XT) signed dot4 takes the SDWA byte-select sequence gfx1012 already used: PR 4855, author Keoian.
Same arithmetic, four signed byte products accumulated modulo 2^32.
- rocBLAS table gfx1030 / 5.4.0.ad84daa81c (TheRock gfx103X-all 7.13 wheels): PR 3343, author elightcap.
- rocBLAS table gfx1030 / 5.2.0.dabb6df2b98-dirty (Arch Linux ROCm 7.2.4): PR 3924, author schmittcn.
include/strata/core/amd_community.hpp holds the switch, the list of community tables and the engine log line (printed only
on gfx1010 and gfx1030); gemm.cu refuses a community table with the switch off, the existing loader still refuses any table
whose architecture or full rocBLAS version string differs. amd_community_test checks that the switch turns each one off.
The contributors' numbers are labelled as theirs in docs/AMD_HIP.md and have not been reproduced.
Port of PR 4039 by merbanan (rebased onto f-0291-int-trial; same bits).
The hyper-connection read's scratch (xn16, lo, lo16, gated and the BF16x2 low halves) is taken as a fourth set of the
region the attention half and the MoE half share: a half's read is done with it before that half's set is written, and
the fused write at a half's end (gr_write_norm_rs: the next half's xn16) runs after the half's last region reader on the
same stream. The same kernels on the same values: the same bits, ~63 KB a token less outside the region, so --prefill
auto finds a bigger chunk. Off by default, and off with the unfused hyper-connection path.
bytes_needed also counts STRATA_PF_GROUP_SCRATCH's larger bounds table (bounds_ints), as carve takes it.
Port of PR 4039 by merbanan (rebased onto f-0291-int-trial; the group-scratch path stays off for weight-stationary layers, whose launches write the whole chunk's rows into GU; the WS offsets follow bounds_ints).
A streamed prompt chunk sends every non-resident expert of every layer over PCIe once, so the chunk size sets
how often an expert crosses the link per token. The chunk is capped by its buffers, which live in the cache
slots the prompt path borrows; on an 8 GB card (RTX 2060 SUPER, IQ3_XXS, 1453 slots) a 4096-token chunk took
1198 of the 1325 lendable slots, and --prefill 8192 fell back to 4096.
The MMQ path's gate/up output, its SwiGLU and its q8_1 copy were sized for the whole chunk (T*K rows, ~84 KB
a token), but a group's products read them and are done before the next group starts. With the switch they
hold one group's rows: gate/up writes its absolute rows at GU - ra, SwiGLU / quantize / down read them from 0.
The cap is T (one expert's most: a token routes to an expert once) plus three times a group's average; a
group that passes it runs as sub-ranges of its experts, each with its own relative bounds appended to the
bounds table. Only on a layout whose every layer takes MMQ (no fused experts, no FP16 fallback); a layer
that would take another path stops the prompt with a message instead of overrunning the smaller buffers.
STRATA_PF_GROUP_CAP=<rows> forces a smaller cap (test knob).
Measured (RTX 2060 SUPER, Ryzen 9 3900X, Qwen3.8-Flash-Next GSQ-RCO IQ3_XXS, resident RAM, k8v4 + KV
streaming; v11 at --prefill 4096 vs this at 5120, 3 pairs, 3 requests per load):
- --prefill auto: 4096 tokens (1197 slots) -> 5120 tokens (1219 slots); 6144 and 8192 still do not fit
- prefill 4K 445.4 -> 449.3, 16K 456.8 -> 493.5 (+8.1%), 32K 456.2 -> 504.5 (+10.6%); decode +0.1 / +0.4 / -0.9
- first-token logits after a 15.6K-token prompt at the same chunk size: bitwise equal to the full buffers;
with STRATA_PF_GROUP_CAP forcing sub-ranges on most groups KL 0.001 (MMQ's stream-k order per launch)
Ported from glorysdj's draft pull request #4185 (commits 961f62963 and 0726ef5ad of that branch; the documentation commit is not taken).
A compile option STRATA_SYCL_IQ_DECODE_FAST (default OFF: packed Q2 expansion, IQ4 bit-select lookup, fast IQ3_S sign arithmetic) and fourteen runtime controls, all default off:
STRATA_IQ3S_GU_SLM, STRATA_IQ4NL_DOWN_SLM, STRATA_RESIDENT_PLAN_COMPACT, STRATA_IQ2S_GU_ROW_REUSE, STRATA_IQ3XXS_GU_ROW_REUSE, STRATA_GR_CACHED_NORM, STRATA_GR_LO_REUSE, STRATA_MMVQ_SHAPE_RPW, STRATA_GDN_QK_REG, STRATA_EXPERT_ROW_E3, STRATA_Q6_SHAPE_GRF, STRATA_IQ3S_GU_ROW_REUSE, STRATA_GR_REDUCE_ILP, STRATA_ATTN_MERGE_ACTIVE.
Conflicts resolved against this tree: the kprof launch wrappers kept (new launches in the conflicted hunks use strata::kprof::kp), the resident-plan launch keeps the kprof wrapper with the compact thread count, native_expert_bench.cpp keeps this tree's fixed bench (the PR's older version of the same hunks is dropped), qsa_prompt_attn_parity.cpp keeps the per-cell check and adds the PR's FP32HASH line and QA_FP32_GRAPH timing.
Also in the diff: docs/INTEL.md section from the PR.
Port of pull request 4380 by moses-sun (commit 5d5e380c4 on v0.2.7), applied without conflict. Refs issue 3577. Homogeneous rigs answer what the single cached bool used to;
not run on a heterogeneous split (no such box here).
Port of pull request 4715 by cedrik-fuoco (commits bd1410701 and d5a0d8294 on 0.2.7.1). The three bf16_proj calls of the layer-major walk (now in Prefill::lm_run)
omitted the stream argument; under STRATA_HC_REQ8=1 the int8 hyper-connection codes were expanded on the default stream while the GEMMs read them on another.
Port of pull request 3382 by Vop3p (commit 2b65710f9 on v0.2.0-beta), rebased by hand on the 0.2.9 trial tree.
Conflict in generate.cpp: the share gate goes first in run(), before the co-read branch, and the kva_floors / set_cell_floor bookkeeping after the read is
kept as it is. Addition to the original: a read run on its own thread beside decoding slots (STRATA_BATCH_CONCURRENT_READ=1, run_conc) keeps the share
blocked for the whole read and does not touch the slots' state from the reader thread. Off unless the variable is set.
Port of pull request 4283 by bitaccesscomau (two commits 3956b94a7 and fcb0e399c on v0.2.8), applied without conflict on the 0.2.9 trial tree.
Off unless the variable is set; STRATA_PREFILL_ROUTED_STREAM_DEPTH sets the copies queued ahead (default 16).
Ported from maxfridbe's pull request #3248 (commits e55a11c08 and 8cc654f23 of that branch).
- STRATA_ATTN_PERCELL (default on): qsa_prompt_attn_percell scores one work-item per cell in the prompt path's fallback attention (reached when the Battlemage ESIMD kernel refuses the pools, e.g. FP16 or Q4_0 KV, or STRATA_PROMPT_ATTN_ESIMD=0). =0 keeps the warp kernel. Not bitwise: FP32 in another summation order.
- STRATA_SELECT_GEMM (default: from 2048 blocks): QSA block scores as oneMKL FP32 GEMM tiles plus qsa_block_scores_reduce; =0 the warp kernel, =1 always. Not bitwise.
- qsa_prompt_attn_parity also checks the per-cell kernel and is registered with ctest.
Conflict resolution against the tree: qsa_select.dp.cpp keeps qsa_block_scores_rows beside the new reduce kernel; docs/INTEL.md keeps this tree's text (the PR's table is not copied; measurements are in the Intel notes).
Ported from maxfridbe's pull request #3248 (commits d95a9c4b0, 5fbef34f9, 344e43e1d, c44fb63be of that branch), cherry-picked without the prompt-kernel commits.
- conversation_snapshot: a copy with neither end in device memory (pinned host <-> pageable host) is a memcpy on the CPU; through the queue it runs on the Arc's copy engine, which the PR reports hanging (Engine reset: engine_class=bcs).
- s2_expert_grouped: #pragma clang fp contract(off) in the two transposed grouped kernels (fixups.py 9d keeps it across re-migrations; 9c the snapshot test's sycl::free).
- gr_parity: the STRATA_QFUSE read check is skipped when the SYCL fused GR read writes no q8_1.
- strata-sycl.sh: STRATA_CACHE_DIR defaults to the mounted data root so the int8 crossover cache survives the --rm container.
Port of PR 3095 by Vop3p (rebased onto f-0291-int-trial; the slot drafter load and bind
conflicts with STRATA_PIPED_MTP resolved by keeping both).
With --batch slots a request that is alone runs the solo path, and that path can keep
--pipeline-windows: both parities' verifiers share the stage's session, so copy_to_slot reads a
whole state after the pipeline drains. With --batch-mtp the slot drafters follow the solo drafter
to the first stage's card when mtp_first. Opt-in; off it is unchanged.
The previous commit stopped whenever the dense weights did not fit in free memory minus --reserve-mib. They would load in the margin, so that case is now a warning (the load goes on) and only need > free memory, where the cudaMalloc cannot succeed, ends the start.
The router's stand-in pick (previous commit) keeps a NaN from becoming a memory fault, but the layer's output is garbage and the request would stream nonsense. route / route_publish now set a device flag when no pick came out; ds4_main polls it at the first token and every 8 (one small async copy and a stream wait, not a per-token sync), and the model says once in the log which device met it. The request ends with 'ERR the model's numbers went non-finite ...' and the conversation is dropped (reset), the same ERR-then-DONE shape as a failed prompt. The --parallel loop is not covered; the host-routed window path already errors on a non-finite weight.
Starting never reaches the feed again (test_no_acquisition passes unchanged). What a start does now, locally: a saved profile whose DeepSeek engine
folder an update removed is pointed at the newest installed package of its own kind (strata_control/dsv41_paths.py, called from runtime.start), which
fixes the 'engine could not start: file not found' after a strata launch updated the engine beside the app.
The explicit update (strata update, the app's Update engine, updater.run_update with the engine) now also updates an installed DeepSeek engine
(updater.update_dsv41): the feed's newer package of the same kind is downloaded, checked (signature, every file), unpacked next to the old one,
started once (--version), and only then every saved run config is pointed at it; older folders go, except one a running engine uses. update --check
reports it; the check kept after any update or check also feeds engine_component.cached_offers / status()['update_available'], which the app reads
without the network to show one card. strata launch's own upgrade (dsv41_engine.py) is left as it was.
Tests: tests/terminal/test_dsv41_update_path.py (the old keep_current tests, adapted, plus a start that makes no request).
qsa_prompt_attn_batch_gated writes half(attn * sigmoid(gate)) straight from the epilogue of
prompt_attn_wmma_kernel, with the expressions of gate_attn_kernel (a * (1 / (1 + expf(-g))), round to
FP16), so the image the o-projection reads is the same bits and the FP32 attention buffer is neither
written nor read back (about 0.4 GB a QSA layer at 8K tokens). Used only where that kernel runs (HIP,
int8 KV, STRATA_HIP_WMMA) and the output is not rotated back (kv_rot / hybrid K8V4 keep gate_attn);
anything else, and STRATA_QSA_FUSE_GATE=0, runs the two-step path. Both prompt-layer loops use it.
Exactness is checked end to end (first-token logits and greedy ids with the switch on and off); there is no
kernel-level test for it yet. Idea from halo-box/strix-llama.cpp PR 213 (attention gate fused into the QSA
prefill epilogue, MIT, The ggml authors); the code is ours.
When every score left is NaN (the gate's input went non-finite), nothing beats -FLT_MAX in route_kernel / route_publish_kernel, the pick stays at its 0x7fffffff start, and the next lines read sc[0x7fffffff] (shared memory), vram[0x7fffffff] and, in the prompt path, vs[0x7fffffff] in moe_sort_kernel: a memory fault in a kernel far from the cause. The thread that picks now takes the lowest expert not picked yet when no pick came out (one compare a pick on the finite path); the weights stay NaN as before. The host-routed window path, which has the picks on the host anyway, ends the request with a clear error on a non-finite weight.
tests/dsv41/ds4_route_guard_test.cu (target ds4_route_guard_test, needs a GPU, no model): valid and distinct picks for NaN / inf / partly NaN logits, the host's top-k when finite, and launch times (DS4_ROUTE_TIMING=1).
rms_rope_rows replaces split_q + rms_rows + rope for the query and rms_rows + rope for K and the indexer query.
It uses the same block_sum, the same (s * x) * w product and rope_kernel's own angle and rotation expressions, so
the outputs are the same bits; the traffic drops from about 1.1 GB to 0.4 GB a QSA layer at 8K tokens. On by
default, STRATA_QSA_FUSE_NR=0 runs the old chain.
hip_qsa_nr_parity compares every output element bit for bit against the old chain (T 1..8191, four start
positions, NaN-poisoned outputs); hip_pf_cover_test poisons every output of the gfx11 prompt GEMMs (wmma_gemm.cu)
and checks that each element is written and that sampled rows match an FP64 reference. Neither has run on a GPU yet.
Idea from halo-box/strix-llama.cpp PR 213 (fused RMS norm + RoPE, MIT, The ggml authors); the code is ours.
Before runtime start, a saved profile that runs strata-dsv41 from an engine-dsv41 folder is pointed at the newest installed package of its kind
(every saved profile's run config, not only the one started), and the signed feed is asked for a newer package at most once in six hours (half an hour
after a failed check). The package is fetched by a separate background process the start waits for at most 90 seconds; after that the start goes on
with the installed engine and the fetch ends in the background. Nothing in it can fail a start. A package folder a running engine uses is never removed
(also in upgrade() and install(), so a strata launch beside the app is safe), and two fetches never run at once.
Known: tests/terminal/test_no_acquisition.py::test_the_deepseek_engine_check_is_reached_only_from_launch_and_setup fails by design, because a start now
reaches the feed (owner rule: starting never acquires). It needs the owner's decision before this branch merges.
Port of Project Maya 8b56730ad (MIT): the dense weights (attention, shared experts, head) always stay in VRAM, so a device whose free memory cannot hold its whole layers' dense weights is told so before anything loads. Devices with tensor-parallel members are not checked.
Random legal blocks against a CPU reference: iq::dequant_rows through both dequantizers (DS4_DQ_OLD=0 / 1 / the default), iq::mv with 1..16 columns, f16_rows, ds4_gguf.cu's rows and mv on Q8_0 / BF16 / ggml types, the library GEMM and the WMMA kernel after the dequantizer, and a repeat path that runs one call many times. Shapes include k % 1024 == 0, which ds4_iq_test never ran.
one_gpu_plan may turn ln_one / ln_land off after the lane timing, but recent_n and the slots' fills stay (recent_n is set at the load, before the plan). borrow_begin then skipped rc_drain: a fill done but not yet mapped, or still writing, met the run's buffers in the tail, and the tick after the hand-back mapped its expert over what the run wrote (NaN routing, then an illegal memory access on a long prompt after shorter ones on one GPU). rc_in_tail() (recent_n > 0 and the fill stream exists) now also triggers rc_drain.
Port of the idea of Project Maya v1.1.45 (c968b9486, MIT): our rc_drain has no for_lend argument and this tree has no x3 / copy_flush, so only the condition is taken.
dq_rows_kernel, the default dense dequantizer on gfx11 (Strix Halo), stages a warp's 32 sub-blocks in a 44-uint4 (704 byte) shared buffer.
Four Q6_K blocks are 840 bytes, so the staging ran into the next warp's buffer and the dense rows came out wrong: NaN in layer 0 of every
chunked prompt (96 tokens and more) of a UD-IQ1_S or UD-IQ1_M set, whose attention is Q6_K. Q3_K (440 bytes, not a multiple of 16) read 8
bytes past what was staged. Q5_K (704 bytes, the ISTA-DAS sets' dense type) fits and keeps the staged path. Stage only when the span fits
and is a multiple of 16 bytes.
(from d6be7ff6a on f-042-strix-ds)
dq_rows_kernel (the default on gfx11, e.g. Strix Halo) stages a warp's 32 sub-blocks in a 44-uint4 (704 byte) shared buffer. Four Q6_K
blocks are 840 bytes, so the staging ran into the next warp's buffer and the dense rows came out wrong (NaN in layer 0 of every chunked
prompt of an Unsloth UD-IQ1_S/UD-IQ1_M set, whose attention is all Q6_K; windows of 16 use the GEMV and were fine). Q5_K (704 bytes, the
IST-DAS sets' dense type) fits and keeps the staged path. Stage only when the span fits and is a multiple of 16 bytes.
M5 Max, MTP, 5 interleaved pairs, fixed harness (each run's spec, min-p and context read from the engine's own log), greedy ids identical in every run, over --spec 2:
Q2_0 story 104.4 -> 104.7 (+0.4%), code 114.4 -> 127.3 (+11.5%), JSON 124.6 -> 132.5 (+6.3%), agent turn 115.9 -> 117.9 (+1.9%, 3/5 pairs).
IQ3_S story 94.2 -> 93.3 (-0.9%), code 103.8 -> 106.1 (+2.5%), JSON 105.1 -> 122.6 (+16.7%), agent turn 102.1 -> 107.1 (+4.9%).
The earlier Q2_0 'flat' result and IQ3_S '+1.4..3.6%' came from a harness that gave the spec flags to the first pair of an arm only. Other sets keep --spec 2.
Window::init accepts 512 or 256 experts a layer; the decode router (router_route / moe_route), the prompt router (route_topk) and the plan buffers use the model's count, the SSD stream reads it from the GGUF. The route kernels are templated on the logits a lane holds (16 for 512, 8 for 256); the 512 path is the old code token for token, the logits rows keep their 512-float stride, the predictor's unused columns are -inf. The MTP layer keeps its own 512 experts.
Tests: prompt_parity route_topk and fuse_parity router_route run both counts (all pass on the M5 Max). IQ3_S 512 experts: greedy ids identical to the build before in every class and prompt (3 interleaved pairs), speed equal (decode +/-0.5% median, prompts -0.5..+5.9%, inside the spread). Not run: an end-to-end 256-expert model (58 GB download).
Since the conversation-cache fix gave sample_host_memory a macOS branch, the 0.2.7 policy (table in RAM when it fits) started firing on the Mac: the 26.8 GiB n-gram table was locked in RAM, the footprint went from 8 to 35-42 GB and free memory dipped to 19 GB. M5 Max, IQ3_S, 3 interleaved pairs, same binary, same tokens: a cold 4K prompt after decode work is 10.6% slower with the table in RAM (-10.7 -10.6 -10.0% against the v0.2.9 binary, +2.4 +15.2 +12.2% for direct reads against RAM), decode is the same inside the spread. Direct reads are what the Mac had before; an explicit --ple-io still wins.
The Qwen3.6 engine (src/qwen35/q36_engine.cu) started its penalty history from the whole prompt (`hist = ids`), so its model-card presence penalty 1.5 counted the prompt's last tokens as in the other engines. The history now starts at the last prompt token and grows with the tokens written (penalty_history_start in include/strata/qwen35/sampling.hpp), the same window as src/program/generate.cpp; batch slots copy that history. STRATA_PENALTY_PROMPT=1 keeps the whole prompt.
Tested: qwen35_sampling_test (276 checks, 9 new for the default and the opt-out), and a CUDA sm_86 build on z440. Replay on z440, one A4000, UD-Q6_K_XL, the reporter's conversation cut to 250 records, 400 tokens: 16 sampled seeds and 2 greedy requests per arm. This model did not return the old JSON in either arm (0/16 with the default, 0/16 with STRATA_PENALTY_PROMPT=1; 0/2 greedy in both), so this replay does not show a difference between the two windows; no answer in either arm started with the old JSON or a code fence.
docs/DETAILS.md: the sentence saying the Qwen3.6 path still counts the prompt is replaced.
Same FMAs in the same order as the runtime-bounded loops (bitwise); nsys: gdn_ab 352 -> 290 us/round, router mmvf 533 -> 492 us/round. Engine A/B pending.
sycl/src/program/generate.cpp is its own copy of the serve loop, so the change in ed76b12f9 did not reach Intel cards. Same window: the fed-back last prompt token and the reply's tokens, built from the request's prompt ids on every request (a restored conversation gets the same window as a cold read). The helpers are added to sycl/include/strata/kernels/sampler.hpp, which that build includes first.
Measured on two Arc Pro B65 (SYCL AOT bmg-g31), Qwen3.8-Flash-Next IQ3_S, the reporter's 4-message conversation (32,951 tokens, cached), --spec 2, 16 sampled seeds, 500 tokens: old JSON 0/16 and numbered plan 16/16 with the new default; old JSON 8/16, plan 7/16, other 1/16 with STRATA_PENALTY_PROMPT=1. Greedy, 2 requests per arm: numbered plan 2/2 in both arms on this model.
M5 Max, MTP, IQ3_S, 5 interleaved pairs (2 + 3), greedy tokens identical in every run: --spec 3 --spec-min-p 0.5 against --spec 2:
story 93.8 vs 92.4 tok/s (+1.4%), code 103.4 vs 100.4 (+3.0%), JSON 105.3 vs 101.6 (+3.6%), agent turn 100.8 vs 98.0 (+2.8%).
Q2_0 measured flat (-1.0% to 0.0%, 5 pairs; 800-token story -0.7%), so it keeps --spec 2; --spec 3/4 without min-p lose on both.
mac_fit.spec_args picks the flags by model; engine_args takes the model name (setup.py and metal/setup_mac.py pass it); a streaming
Mac keeps STREAM_SPEC, a Mac without the draft layer keeps --spec 2. The engine's own default (--spec 2) and an explicit --spec in a
config are unchanged. Test: tools/test_setup_mac.py.
With the model-card presence penalty 1.5 over the last 64 tokens, the prompt's own tokens (the digits of a new task, a quoted JSON answer) were penalised, and a conversation whose last turn asks for something new could get its previous answer back. The window now starts at the end of the prompt: the fed-back last prompt token and the reply's tokens. Earlier assistant turns inside the prompt are not counted. The history is built from the request's prompt ids on every request, so a conversation restored from the conversation cache or a saved slot gets the same window as a cold read. The STRATA_ADMIT_MERGE_FIRST merged first window is not used for a request with a penalty window, since its prompt rows would join the reply window.
Measured with the opt-in switch (618cd7a4b) on a P100, IQ2_XS, the reporter's saved 4-message conversation, 32 seeds per arm: old JSON returned 13/32 as shipped, 4/32 reply-only, 3/32 with presence 0. Qwen IQ3_S on an R9700, 16 seeds: old JSON 8/16 as shipped, 0/16 reply-only. 61-task quality027, greedy, one run per arm: 56 and 55 passed as shipped (two runs), 55 reply-only, 54 with presence 0.
Added penalty_window_test (header-only, CPU) and a line in docs/DETAILS.md.
Same FMAs in the same order as the runtime-bounded loops (bitwise); nsys: gdn_ab 352 -> 290 us/round, router mmvf 533 -> 492 us/round. Engine A/B pending.
DeepSeek: an illegal memory access, launch failure or hardware exception in a prompt read or a decode round used to turn into an ERR/cancel while the engine kept running on a dead GPU context, so every later request failed with HTTP 400 until someone restarted it. strata-dsv41 now prints 'ERR fatal GPU error: ...' and exits (code 3). Qwen: a failed decode window, commit or final commit wait that leaves a sticky fatal runtime error does the same (the prompt-read path already did, #224), by the runtime's error text so CUDA, HIP and SYCL builds agree.
Server: that ERR line fails the request in flight as an engine death (503, retryable) and marks the engine ended, so the next request starts it again at once, with one plain log line and a plain death note. Restart-loop guard: a third fatal death within ten minutes stops the restarts and answers 503 with the reason until the window has passed; one death counts once.
Test hooks (off unless set): DS4_INJECT_FATAL=N / STRATA_INJECT_FATAL=N fail the Nth request / decode window with a real fatal error's text; *_ONCE=<file> fires it once across restarts.
Tested: serve/test_fatal_gpu_restart.py (7 tests); through the real server, request 2 injected, DeepSeek on boston (IQ1_M pack, 2x A6000): request 2 got a 503, the restart took 44 s, requests 1, 3, 4, 5, 6 answered 42; Qwen on a P100 (IQ2_XS), fault injected mid-decode in request 4: 503, restart, requests 1-3, 5, 6 answered 42. The guard counted '1 of 3' there and '2 of 3' in an earlier run where the injection fired on every restart. NOT tested: a real GPU fault (none reproduces), the Windows and Metal builds, the third-death refusal through a running server (unit-tested only), conversations coming back from saved slots after the restart (no check made).
Bitwise the plain v3 read (gr_v3_screen, T 1-8, 0 differing); 4-8% faster per read on cold weights. Weight prefetch and 2/4 rows a warp measured slower and stay off. Engine A/B pending.
hcf.metal (gr_norm folded into the producer of bo with a last-arrival threadgroup: bit-exact, +1.2 ms slower), gemvu.metal and
moeu.metal (unit loops unrolled x2: +2.3 / +1.5 ms), qsab.metal (batched qsa_attn: +0.07 ms). None is used by the engine; they
stay with their numbers (phaseA-tests/mac-q/RESULTS.md section 7). token_chain gets the CHAIN_* switches that measured them
(NORM2, COMB2, NONORM, NOOPS, SKIPK, HCF, KEEPNORM, GEMVU, MOEU, QB, DEFS).
Window::prompt ran each chunk's MTP-layer K/V rows (9% of an 8K prompt) after the chunk, with the GPU otherwise idle for them.
They now go to a second command queue (Context::run_graph_async / wait_async) while the next chunk's main work runs on the
first. The chunk's final residual rows are double-buffered (pb_.R / pb_.R2); the rows of chunk c+1 wait for chunk c's (shared
drafter cache and scratch); every way out of prompt() joins. Not used with M-RoPE (images). STRATA_METAL_MTP_OVERLAP=0
restores the sequential rows.
mtp_rms_b: mtp_rms with the row loaded up front (same sum, same products, same order); under STRATA_METAL_LDB=0 the original.
Engine, Q2_0, MTP spec 2, 5 interleaved pairs, same binary: p8k 1936 vs 1910 tok/s (+1.4%, 3/5), p32k 1747 vs 1711 (+2.0%, 4/5),
greedy ids identical in every run. The overlap is partial: the two queues share the GPU.
With the model-card default presence penalty 1.5 over the last 64 tokens, the prompt's own tokens (digits of a new task, a quoted JSON) were penalised, and a conversation whose last turn asks for something new could get its previous answer back. Measured on a P100, IQ2_XS, the reporter's saved 4-message conversation, 32 seeds per arm, 400-500 tokens: as shipped 13/32 returned the old JSON and 16/32 a numbered plan; reply-only penalty 4/32 and 25/32; presence 0 3/32 and 26/32. quality027 --quick (19 tasks, greedy, one run each, no noise arm): shipped 15, reply-only 16, presence 0 17 passed; the 1-2 task differences are single tasks and inside run-to-run noise. Default unchanged (switch off). The old JSON still came back in about 1 of 10 in the best arms, so the penalty is a large part of the cause, not all of it.
bit 0-1 down (batched staging loads, weight prefetch), bits 2-3 up, bits 4-5 rows per warp 2/4 for T<=4. Screen bit-for-bit against the plain v3 read; speed not established yet.
gr_norm stored R in place inside its loop, so each of its ten iterations (and the ten of the second loop) waited a full
memory latency for the next load: 98 times a token, ~6 us of a 7.7 us dispatch. gr_norm2 loads R, bo and w_norm first, then
sums, reduces and stores; moe_combine_sh2 loads the ten plan entries, then the ten rows, then sums. The arithmetic and its
order are the originals', so the outputs are bitwise the same. STRATA_METAL_LDB=0 selects the original kernels.
token_chain (Q2_0, 1 token, graph): 10.65 -> 10.14 ms. Engine, MTP spec 2, 4 interleaved pairs: story 100.97 -> 103.90 tok/s
(+2.9%, 4/4), code 108.63 -> 110.84 (+2.0%, 4/4), greedy ids identical in every run.
Off by default. With STRATA_STALL_STACKS=1 the stall report suspends each other thread in turn, unwinds it with the unwind tables
(no heap, no loader lock, no stdio) and prints the frames, named from the PDB when it lies beside strata.exe. The minidump flags now
include process thread data, handle data and memory info. Used to locate the Windows gfx1151 UD-Q4_K_XL stall (host thread inside
the HIP runtime after a graph launch).
A systemd service without LimitMEMLOCK may lock 8 MiB, so the 26.8 GiB mlock of the PLE table was refused and the engine ran with a reclaimable table (the reporter measured up to 6% slower decode) behind a two-line message. Before mlock the engine now raises RLIMIT_MEMLOCK's soft value to the hard one (needs no privilege) and says so; if the table still does not fit, the message gives the allowed size and the remedy (LimitMEMLOCK=infinity, or ulimit -l unlimited). The residency policy itself is unchanged. Tested here: the CUDA engine builds (sm_60). The run under 8 MiB soft/hard, 8 MiB soft with a higher hard limit, and unlimited is queued, not yet seen.
The Mac now reads its free RAM for the conversation-cache guard (arena_avail's rule), the window's checkpoint uses the shared
contract (3 tail rows a layer, a scratch block_pos), and the session's QsaStates point at the window's K/V buffers (and the
drafter's), which the generic snapshot copies as cell-major byte prefixes. Before, the session's own pools were never written on
Metal. Two conversations taking turns (11K and 6.5K, 24K and 3K tokens): the second request of each re-read its prompt (6.3 s,
14.9 s); now it reuses it (46-65 ms), with the same answer ids.
Heretic v2 IQ4_NL keeps blk.0.attn_gate.weight as BF16 (GGUF type 30) while the other 35 gates are Q8_0. NativeDense has no kernel for type 30 and skipped it, so the prompt path and the verify window, which read the gate through the native route, stopped with 'blk.0.attn_gate.weight has no native GGUF blocks (run with --native)' on the first request. NativeDense::load now makes such a matrix (BF16 in the file, BF16 row in the pack, 2-D, a multiple of 32 wide) into Q8_0 on the host (the same rounding the engine already uses for STRATA_HC_Q8) and binds it, so every dispatcher finds a native projection; weight_bytes_for counts it. The pack's BF16 copy stays. This is not bit-exact against reading the BF16 values (a Q8_0 rounding on one 2560x6144 matrix, the same format its siblings have); reading the BF16 directly would need changes in the prompt path, the verify window (with its side-stream branches) and gdn_layer. Tested here: the CUDA engine builds for sm_60 (p1004). NOT tested: any run, because the 109 GB Heretic IQ4_NL model is not on a test box; the reporter's own run is the first test.
A Windows process starts with the I/O priority of the program that started it. Run from a Task Scheduler task, a service, or a terminal that an IDE or coding agent keeps at low priority, the engine inherited Low I/O priority: the reporter measured PLE direct reads about 15x slower, cold prompt reads 3-5x slower and decode 14% slower on the same drive, and the engine blamed the drive. Both engines (strata and strata-dsv41) now read ProcessIoPriority at start and set it to Normal when it is lower (no privilege needed to go from Low to Normal). STRATA_KEEP_IO_PRIORITY=1 leaves it as inherited. The CPU priority class is not touched. New io_priority_test (Windows): started at Low it ends at Normal, started at Normal it is untouched, KEEP leaves Low alone. Tested here: that test passes on Windows; the HIP engine builds. Not tested: the reporter's slowdown itself (needs their drive and a low-priority launcher with an engine run).
A worker going to sleep at the moment a batch started could miss both the new generation and the wake-up (the caller then runs the batch alone, slower, not wrong). Taken from Project Maya v1.1.36 (#618, Cody Harrison), MIT, credited in third_party/project-maya/NOTICE.md.
On Windows the HIP runtime gives every cudaHostRegister'ed range a second address for the GPU and takes any host pointer inside it for the registered memory, but it does not reserve that range in the process's address space. In about one start in ten the stack of the thread that pins the expert arena lay there: small cudaMemcpy calls from local variables then read (and wrote) the arena, and every long prompt was read wrong until a restart.
- pinned.cu: each registered range's alias is reserved (MEM_RESERVE); a range whose alias already covers memory in use is unregistered and treated as refused by the driver, so it stays resident but unpinned (logged).
- generate.cpp: main keeps the free address space below 1 TiB reserved for the life of the process and runs the engine on a new thread, so its stack and later allocations lie above the aliases (STRATA_STACK_ISOLATE=0 turns it off).
- pinned_alias_test, docs/AMD_HIP.md.
Windows HIP only; other backends keep their behaviour. Author's measurements: 0 bad starts in 110 (main + change 0 in 70, main alone 5 in 70) on an RX 7900 XTX. Here: the HIP objects, pinned_alias_test and pin_budget_test build for gfx1100 with ROCm 10.2 (no gfx1100 card to run them on). The hunk in pinned.hpp was applied by hand because v0.2.9 has the shared-file fields between the lines the PR touched.
The screen looked for strata-vision in the engine folder for every model; DeepSeek carries its own picture tower (--vision on|off), so the check always failed for it and Read pictures stayed off and locked.
A programmatic-tool-calling harness shows the model its tools as tools.write(...) inside code, and the model then wrote the native call with that spelling; the call came back as text and the run stopped. The prefix is read like the other unambiguous aliases (a declared name always wins, an undeclared name stays text). Also: a tool_reference with a non-string name no longer fails the request. Tests for the prefixes and for the history rule on the Anthropic and Responses shapes.
The 0.2.9 rule read only assistant messages' tool_calls. It now also reads tool_use blocks (Anthropic) and function_call / custom_tool_call input items (Responses, with the namespace in front of the name). Test on the /v1/messages route added.
Claude Code declares ToolSearch and a defer_loading placeholder and names its MCP tools only in the conversation; when the model
calls one of them straight away the call came back as text and the session stopped. Ported from PR #4700 by xrip onto v0.2.9
(the server.py hunk for the parser call moved); the PR's own tests are included.
Co-authored-by: Niko1221 <[email protected]>
A read from token 0 that reaches the shared start (shared=, pin=) no longer stops at it: the start is a restore point inside
the one run of chunks (feed_marks), and the state at it is saved apart for new conversations after the read, from that mark
(Model::slot_save(out, err, at): the window rings and partial groups from the mark, the compressed KV and index keys of rows
0 .. at / ratio, the tokens up to it). The stop cost a whole extra pass over the streamed experts: a fresh 6000-token prompt
with the shared start at 4500 and a tail point, DGX Spark first token 21.0 s -> 16.4 s (14.35 s with no keys at all).
STRATA_DS4_PREFIX_STOP=1 stops at the start as before. A new chat behind the start still takes it back in 0.11 s and reads
the same tokens as a cold read; the agent loop total is unchanged (188.8 s against 187.9 s, 3 runs each).
Credit: Project Maya #607 (mw00), MIT, third_party/project-maya/NOTICE.md.
serve/server.py (0.2.9) sets the nine qualified switches only on Linux; Windows had none. Measured on a Strix Halo Windows PC
(Qwen3.8-Flash-Next IQ3_S, serve/server.py --parallel 4 over the engine, tools/par_load.py w2 at 4 clients, 3 rounds, interleaved,
download paused): 51.0 / 51.2 / 51.7 / 50.7 tok/s without, 55.0 / 55.6 / 55.2 / 55.1 with (+8%), TTFT p50 2.57 -> 2.43 s,
max chunk gap p95 1.97 / 1.37 -> 1.27 s, no errors. Not STRATA_BATCH_CONCURRENT_READ.
parallel_default_env() in arch_defaults.cpp: CKPT_POOL, CKPT_POOL_READY=4, SLOT_CKPT_MOVE, SLOT_COPY_DIRECT, SLOT_COPY_BATCHSYNC,
BATCH_GDN_GROUPS, BATCH_ATTN_ROWS, BATCH_SELF_COMMIT, QSA_SLOTS, set from generate.cpp with --batch / --slots of 2 or more on a
Windows gfx1151; a switch the user set is kept; STRATA_GFX1151_DEFAULTS=0 turns it off. Unit test in arch_defaults_test.cpp.
verify.cpp read STRATA_BATCH_SELF_COMMIT and STRATA_QSA_SLOTS into namespace-scope constants, before main() sets the defaults, so
two of the nine would never have seen them (the first engine build measured 52.2 / 52.6 tok/s, the base). They are read at the
first use now. Also logs the time of each verify window graph capture.
Strata 0.2.9 (engines and the SYCL package built from 99866a47a2b8a21b131aad162a584ee9da92f8f0; the Windows and Linux x86 app, Terminal, Control and installers from c62d5e3a15b40ea51c96aae9e8d36928965f53ab; the Mac set from 4cf0d8cd3 and the Spark Control from 38f56619c, which the later card fix does not touch; the later commits change the Python Control, server, setup, tests, tools, docs, the app host and the Mac Metal engine only)
Strata 0.2.9 (engines and the SYCL package built from 99866a47a2b8a21b131aad162a584ee9da92f8f0; the Windows and Linux x86 app, Terminal, Control and installers from c62d5e3a15b40ea51c96aae9e8d36928965f53ab; the Mac set from 4cf0d8cd3 and the Spark Control from 38f56619c, which the later card fix does not touch; the later commits change the Python Control, server, setup, tests, tools, docs, the app host and the Mac Metal engine only)
Measured on Boston (one A6000 as a 12 GB card, ballast leaving 64 GB of RAM, UD-IQ1_M): the first 1000-token prompt read took 17.0-17.5 s with the
B1 default against 11.1 s before B1 and 11.2 s with DS4_FLOW_BUFFERED=0. The setting is Project Maya #312's (Peasant Smith / mw00); it stays available
as DS4_FLOW_BUFFERED=<tokens> until it is measured to win here.
With a layer's slots partly in the lent tail region (B1, Project Maya #312 tail lending), a swap that hands its outgoing expert to the RAM store
(DS4_STORE_PUT, on by default) read vram_base + slot * unit past the layer's own allocation: on HIP (p620, short prompt, windows path, cache on)
the first swap raised an illegal memory access and poisoned the context. The DBG slot dump had the same address. Adds tests/dsv41/short_prompt_check.py
(a 40-token prompt twice with the default sampler, then greedy; fails on ERR or illegal-access / router-picked lines).
Project Maya #691 (needmorevram, c7d08c5), the "a chunk streams only the experts it picks" part, onto src/dsv41's batches
path (Streamer / copy_ahead; DS4_FLOW=0 or a pack the flow does not take): with DS4_SKIP_UNPICKED_ROWS=N (or
DS4_SKIP_UNPICKED=1 for every chunk) a chunk of at most N rows waits for its layer's routing and then reads and copies only
the experts some row of it picked, into the same slots and batches, so the summing order is the same. Off by default.
Strata's flow path already streams only routed experts, so the default path gains nothing from it.
DS4_UNPICKED_STAT=1 prints, per chunk size, the share of a layer's experts no row of the chunk picked.
Credit: Project Maya #691 (needmorevram), MIT, third_party/project-maya/NOTICE.md.
Project Maya #691 (needmorevram, c7d08c5), the restore-point part, onto src/dsv41: Model::feed_marks() keeps the restore
points inside a run of chunks from snapshot sets the layers fill on the way (kSnapN = 8 sets; feed_mark's one set became
an array), each the state a read up to that point leaves, instead of a run up to each point; Model::marks_inside() says
whether that is worth it (the long-prompt skip's extra rows at most a chunk, inner_cost); the serve loop reads the rest
of a prompt that way when the points left are inside it (not past a shared start a read from token 0 stops at, #607).
STRATA_DS4_MARKS_INSIDE=0 stops at each point as before. feed()'s `after` (the rest of the prompt still to come) moves
the long-prompt skip's kept rows to the prompt's end. Not ported from #691: the chunk streaming only the experts it
picks, the EXL3 lend, DMA read-back of host sums.
Credit: Project Maya #691 (needmorevram), MIT, third_party/project-maya/NOTICE.md.
Project Maya #607 (mw00, b2d4c30) onto src/dsv41: when a prompt read from token 0 reaches its shared start (shared=,
pin=) the state the read keeps there is also copied apart from the conversation, in RAM (slot_save). A prompt that does
not continue the conversation in the model takes back the longest kept start it begins with, when that covers more than
the model's own restore points (a kept conversation slot only when it covers more still), and reads only the rest. With
or without STRATA_DS4_SLOT_KEEP. STRATA_DS4_PREFIX_KEEP (2 starts, LRU), STRATA_DS4_PREFIX_GB (2), STRATA_DS4_PREFIX_MIN
(1024); 0 keeps none. Starts with pictures are not kept.
Credit: Project Maya #607 (mw00), MIT, third_party/project-maya/NOTICE.md.
With the lane on (DS4_STREAM=1) and one discrete NVIDIA GPU, the CPU lane's RAM copies are pinned and the fetch kernel pulls a streamed
expert where it lies; a verify window's experts stream too and land in a recent slot, so the link's bytes stay in VRAM.
Gating and follow-ups: one-GPU path only where experts stream (1b60249, 482e46c #496, 7c0ceb1 #551, mw00), no landing while the swap is pending
and a table-update wait (32f3c8c handmade0octopus), a failed fill is never published (#646 handmade0octopus), Windows with pin by need (#567 TheSmokeDev),
HIP pull clock on gfx11/gfx12 and DS4_IQ1S_ROWS (#563 Cass, mw00). Everything stays behind DS4_STREAM=1; DS4_RAM_SKIP_W (changes answers) is opt-in.
Not ported: the helper-GPU condition (#415). Built on CUDA sm_120; model identity/A-B not run yet.
The layer keeps DS4_RECENT=<n> VRAM slots (taken from its coldest experts) for CPU-lane experts it used twice in its last 8 ticks.
A thread copies each fill on its own stream, the next tick maps it by one tab_set launch a device; a swap leaves recent slots and
experts being filled alone, and a run of chunks that lends the tail waits for the fills first. Opt-in (default 0), values unchanged.
Not taken from #344: all-reduce over DMA, segmented top-k, DS4_INDEX_TC, the cuBLAS workspace pin.
ds4_gguf_test: its Q8_K default now follows the engine (opt-in, DS4_IQ_Q8K=1).
A worker going to sleep at the moment a batch started could miss both the new generation and the wake-up (the caller then runs the batch alone, slower, not wrong). Taken from Project Maya v1.1.36 (#618, Cody Harrison), MIT, credited in third_party/project-maya/NOTICE.md.
Taken from Project Maya (MIT), staged against v1.1.42:
- A2: the tail restore point is left out where its read stop costs more than
reading its tokens again (TailCost wired into the serve loop; one in 32 still
kept to measure again; STRATA_DS4_RESTORE_TAIL_AUTO=0 keeps it always). Maya
#668 (mw00, 8721412 and 66fb17b).
- A5: the flow timing correction of the opt-in timing pick stays in the prompt
size class that measured it (ds4_path_policy.hpp + test). Maya #612 (Will
Hampson, 6e0d27d). Our default pick is by length already.
- A sampler CUDA error ends the request instead of sending token -1; logits()
copies on the head stream. Maya #619 (Cody Harrison, 2040174).
- A CPU-lane layer that cannot compute its experts stops the engine instead of
answering zeros; a layer with no GPU kernel is refused at load, any other
warns. Maya #620 (Cody Harrison, e598716).
- bf16_rn by arithmetic on CUDA too. Maya #676 (rilken, 79628d7).
- DS4_ASYNC_T default 8 -> 16: windows of 9-16 rows take the same device path
as 1-8. Maya #608 (mw00, b9c07e7). DS4_ASYNC_T=8 gives the old path.
Project Maya #639 (needmorevram) and v1.1.37 / v1.1.39 (mw00), written onto Strata's server: deepseek_restore_points()
reads the last user turn, the first user turn (the end of the system prompt and tools) and the tail from the prompt's ids
as the template wrote them; Service.prepare puts them in the request's _restore (never a client's own), StrataEngine
sampling_keys turns them into marks=<pos>,... tail=<pos> shared=<pos>. The shared start stays Strata's pin=N where the
request already has one (the system messages); shared= is asked only without it. STRATA_DS4_RESTORE_POINTS (4, 0 = off)
and STRATA_DS4_RESTORE_TAIL (512, 0 = none). serve/test_ds4_restore_points.py.
Credit: Project Maya #639 (needmorevram), mw00; third_party/project-maya/NOTICE.md.
Ports Project Maya's restore points (#639, needmorevram, 50a3313; ds4_marks.hpp v1.1.42 by mw00 and needmorevram) onto
src/dsv41 as a per-file merge, not a commit apply:
- ds4_marks.hpp as in Maya v1.1.42 (latest_point, mark_plan, mark_victim, TailCost; TailCost is not wired in yet, #668).
- Model::mark(shared) / marks_cap() / check_bytes(): up to STRATA_DS4_RESTORE_POINTS (4, 0 = none, at most 16) restore
points besides checkpoints 0 and 1 and the anchor, the least recently used giving way, the shared start last.
restore() and best_check() go back to a mark; a slot keeps the marks with their roles (3 mark, 4 shared start), slot
identity strata-ds4-session-4; the .meta file takes up to 3 + 16 checkpoint positions.
- Replaces Strata's fourth checkpoint (ck[3], the pin=N shared start) by a shared mark: pin=N is now shared=N.
- GEN keys marks=<pos>,... tail=<pos> shared=<pos> (pin=N kept): the prompt read stops at each point mark_plan keeps and
keeps the state there.
- Tests: tests/dsv41/marks_test.cpp (Maya's ds4_marks_test), slot_test for the longer checkpoint list.
Credit: Project Maya #639 (needmorevram), #668 / ds4_marks.hpp (mw00), MIT, third_party/project-maya/NOTICE.md.
2026-10-31 00:56:53 +01:00
1056 changed files with 119003 additions and 5652 deletions
description:Tell us whether the all-cards split engine works on your AMD cards (it prints a line asking for this on cards Strata has not run it on)
title:"[Engine] AMD split engine: <your card> x <count>"
labels:["engine","amd-split-report"]
body:
- type:markdown
value:|
Strata runs the all-cards split engine by itself on Radeon AI PRO R9700 / RX 9070 (gfx1201). On other AMD cards (RX 9060, RX 7000, the 780M and similar) it only starts when you ask for it with `strata launch --split-engine on`, and it prints one line asking you to report whether it works. We cannot test those cards, so your report is how we learn.
Write only what you saw. A report that says "it did not start" is as useful as one that says "it works".
- type:input
id:card
attributes:
label:Card model and count
description:For example "2 x RX 7900 XTX 24 GB" or "3 x RX 9060 XT 16 GB"
validations:
required:true
- type:input
id:arch
attributes:
label:Architecture (the gfx name)
description:From the engine's line, or `rocminfo | grep gfx`. For example gfx1100.
validations:
required:true
- type:input
id:rocm
attributes:
label:ROCm / driver version
description:Strata's engine package brings its own ROCm libraries; the kernel driver is yours (`uname -r`, `amd-smi version`).
validations:
required:true
- type:input
id:model
attributes:
label:Model and quantization
description:The model Strata loaded, for example Qwen3.8-Flash-Next UD-Q4_K_XL or the IQ3_S set.
validations:
required:true
- type:textarea
id:selftest
attributes:
label:The self-test line
description:Paste the line from the engine's log that starts "The split engine's self-test" (passed or failed). The log is in the run folder shown at launch; `strata launch` also prints it.
render:text
validations:
required:true
- type:dropdown
id:result
attributes:
label:What happened
options:
- It started and the answers look right
- It started and the answers look wrong
- The self-test failed, so the normal layer split started
- It failed to start (not the self-test)
- It started and crashed or hung later
validations:
required:true
- type:input
id:speed
attributes:
label:Speed, tok/s (the split engine, then the layer split with `--split-engine off` if you tried both)
description:For example "decode 61 tok/s, layer split 48 tok/s, one user".
- type:textarea
id:more
attributes:
label:Anything else
description:Wrong answers (the prompt and what you got), crashes (the last lines of the engine log), or what you changed.
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."
Version 0.2.9, engine 0.1.55. The public model families are DeepSeekV4.1-Flash-1017,
Version 0.2.12, engine 0.1.58. 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
# below refuses anything older: silently building for a pre-Ampere arch would produce a binary that runs
# and is wrong about its own performance, which is the worst of both outcomes.
cmake_minimum_required(VERSION3.24)
project(strataVERSION0.1.55LANGUAGESCXX)
project(strataVERSION0.1.58LANGUAGESCXX)
# EXPERIMENTAL: the Intel Arc engine, the SYCL port in sycl/ (PR #423; icpx + oneMKL, docs/INTEL_ARC.md). It is its
# own project that takes the unported sources from src/; with this on, nothing below is configured.
option(STRATA_ENABLE_SYCL"EXPERIMENTAL: build the Intel Arc (SYCL) engine from sycl/ - docs/INTEL_ARC.md"OFF)
@@ -92,6 +92,13 @@ if(STRATA_HIP_GFX906)
endif()
option(STRATA_ENABLE_HIP"Build the HIP targets for wave32 AMD GPUs (gfx1100, gfx1201; gfx1012/gfx1030 unvalidated)"OFF)
option(STRATA_PREFILL_MMQ"Build the optional GGML MMQ prefill path for HIP"OFF)
# The community-made AMD changes (include/strata/core/amd_community.hpp): on by default. This option is the build half of the one
# switch STRATA_AMD_COMMUNITY_TUNING: OFF compiles the portable signed-byte dot for gfx1010 again (the SDWA sequence of #4855).
# The rocBLAS tables are switched at run time (STRATA_AMD_COMMUNITY_TUNING=0).
option(STRATA_AMD_COMMUNITY_TUNING"Compile the community-made gfx1010 SDWA dot product (the runtime switch of the same name controls the gfx1030 rocBLAS tables)"ON)
# the DFlash2 drafter (src/core/dflash2.cpp, src/kernels/cuda/dflash2.cu) exists in the CUDA and HIP builds; the Metal and SYCL builds compile without it
@@ -590,6 +645,13 @@ if(STRATA_ENABLE_CUDA OR STRATA_ENABLE_HIP)
add_executable(dflash2_ring_testEXCLUDE_FROM_ALLsrc/core/dflash2_ring_test.cpp)# the drafter's ring log and rebuild against an uninterrupted update sequence
add_executable(draft_vocab_testsrc/spec/draft_vocab_test.cpp)# the DFlash2-alone start's draft-vocabulary choice (a build's packaged default subset; the error elsewhere)
<a href="https://github.com/sponsors/Niko1221"><img src="https://img.shields.io/badge/Sponsor-EA4AAA?logo=githubsponsors&logoColor=fff" alt="Strata über GitHub Sponsors unterstützen"></a>
<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>
@@ -56,7 +57,7 @@ Wir empfehlen **zuerst ISTA-DASLab**, danach **Unsloth** und andere Anbieter, so
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 |
| Set (ISTA-DAS) | Download | Freier Speicherplatz nötig |
| --- | ---: | ---: |
| 1.75 bpw | 239 GB | etwa 245 GB |
| 2.00 bpw | 257 GB | etwa 265 GB |
@@ -175,10 +176,23 @@ Führe **`strata connect`** aus, um die Einrichtung für dein Tool zu sehen. [Ve
## Strata unterstützen
**Hilf mit, lokale KI für alle offen zu halten.** Ein Kaffee hilft, Entwicklung und Tests auf mehr Hardware zu finanzieren.
**Hilf mit, lokale KI für alle offen zu halten.** Ein GitHub-Sponsoring oder ein Kaffee hilft, Entwicklung und Tests auf mehr Hardware zu finanzieren.
<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>
### Treue Unterstützer
Danke an alle, die Strata unterstützen.
<!-- supporters:start (one line per public supporter, newest last; copy a line and change the GitHub name in all four places) -->
<a href="https://github.com/sponsors/Niko1221"><img src="https://img.shields.io/badge/Sponsor-EA4AAA?logo=githubsponsors&logoColor=fff" alt="Patrocina Strata en GitHub Sponsors"></a>
<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>
@@ -56,7 +57,7 @@ Recomendamos **primero ISTA-DASLab**, después **Unsloth** y otros proveedores,
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 |
| Conjunto (ISTA-DAS) | Descarga | Espacio libre necesario |
| --- | ---: | ---: |
| 1.75 bpw | 239 GB | unos 245 GB |
| 2.00 bpw | 257 GB | unos 265 GB |
@@ -175,10 +176,23 @@ Ejecuta **`strata connect`** para ver las instrucciones de tu herramienta. [Guí
## Apoya Strata
**Ayuda a mantener la IA local abierta a todos.** Un café ayuda a financiar el desarrollo y las pruebas en más hardware.
**Ayuda a mantener la IA local abierta a todos.** Un patrocinio en GitHub o 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>
### Apoyos fieles
Gracias a todas las personas que apoyan Strata.
<!-- supporters:start (one line per public supporter, newest last; copy a line and change the GitHub name in all four places) -->
<a href="https://github.com/sponsors/Niko1221"><img src="https://img.shields.io/badge/Sponsor-EA4AAA?logo=githubsponsors&logoColor=fff" alt="Soutenir Strata sur GitHub Sponsors"></a>
<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>
@@ -56,7 +57,7 @@ Nous recommandons **d'abord ISTA-DASLab**, puis **Unsloth** et d'autres fourniss
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.
@@ -175,10 +176,23 @@ Lancez **`strata connect`** pour voir la configuration de votre outil. [Guide de
## Soutenir Strata
**Aidez à garder l'IA locale ouverte à tous.** Un café aide à financer le développement et les tests sur davantage de matériel.
**Aidez à garder l'IA locale ouverte à tous.** Un parrainage sur GitHub ou un café aide à financer le développement et les tests sur davantage de matériel.
<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>
### いつも支えてくださる方々
Strata を支えてくださるすべての方に感謝します。
<!-- supporters:start (one line per public supporter, newest last; copy a line and change the GitHub name in all four places) -->
<a href="https://github.com/sponsors/Niko1221"><img src="https://img.shields.io/badge/Sponsor-EA4AAA?logo=githubsponsors&logoColor=fff" alt="Sponsor Strata on GitHub Sponsors"></a>
<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>
@@ -57,11 +58,12 @@ We recommend **ISTA-DASLab first**, then **Unsloth** and other providers, where
In the **desktop app** and the **Terminal**. What does not fit in memory is read from your SSD, so it needs a lot of free
disk space.
| Set (IST-DAS) | Download | Free disk needed |
| Set (ISTA-DAS) | Download | Free disk needed |
| --- | ---: | ---: |
| 1.75 bpw | 239 GB | about 245 GB |
| 2.00 bpw | 257 GB | about 265 GB |
| 2.25 bpw | 274 GB | about 280 GB |
| ISTA-DAS Coder (half of the experts removed) | 191 GB | about 197 GB |
Runs on NVIDIA RTX 30, 40 and 50 series (Windows and Linux), AMD Radeon AI PRO R9700 (Linux), AMD Ryzen AI Max — Strix Halo
(Windows), and Apple Silicon Macs with 64 GB or more, text only ([Mac](docs/DEEPSEEK_MAC.md)). Setup downloads the files and the DeepSeek engine, then starts the model.
@@ -180,10 +182,23 @@ Run **`strata connect`** to see setup instructions for your tool. [Connection gu
## Support Strata
**Help keep local AI open to everyone.** A coffee helps fund development and testing on more hardware.
**Help keep local AI open to everyone.** A GitHub sponsorship or 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>
### Loyal supporters
Thank you to everyone who supports Strata.
<!-- supporters:start (one line per public supporter, newest last; copy a line and change the GitHub name in all four places) -->
<a href="https://github.com/sponsors/Niko1221"><img src="https://img.shields.io/badge/Sponsor-EA4AAA?logo=githubsponsors&logoColor=fff" alt="Wesprzyj Stratę na GitHub Sponsors"></a>
<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>
</p>
@@ -57,7 +58,7 @@ Najpierw polecamy **ISTA-DASLab**, potem **Unsloth** i innych dostawców — o i
W **aplikacji na komputer** i w **Terminalu**. To, co nie mieści się w pamięci, jest czytane z dysku SSD, więc potrzebujesz
dużo wolnego miejsca na dysku.
| Zestaw (IST-DAS) | Do pobrania | Potrzebne wolne miejsce |
| Zestaw (ISTA-DAS) | Do pobrania | Potrzebne wolne miejsce |
| --- | ---: | ---: |
| 1.75 bpw | 239 GB | około 245 GB |
| 2.00 bpw | 257 GB | około 265 GB |
@@ -179,10 +180,23 @@ Uruchom **`strata connect`**, aby zobaczyć instrukcję konfiguracji dla swojego
## Wesprzyj Stratę
**Pomóż zachować lokalną sztuczną inteligencję otwartą dla wszystkich.**Kawa pomaga finansować rozwój i testy na większej liczbie sprzętu.
**Pomóż zachować lokalną sztuczną inteligencję otwartą dla wszystkich.**Sponsoring na GitHubie lub 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>
### Wierni wspierający
Dziękujemy wszystkim, którzy wspierają Stratę.
<!-- supporters:start (one line per public supporter, newest last; copy a line and change the GitHub name in all four places) -->
<a href="https://github.com/sponsors/Niko1221"><img src="https://img.shields.io/badge/Sponsor-EA4AAA?logo=githubsponsors&logoColor=fff" alt="Apoie o Strata no GitHub Sponsors"></a>
<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>
@@ -56,7 +57,7 @@ Recomendamos **primeiro a ISTA-DASLab**, depois a **Unsloth** e outros provedore
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 |
| Conjunto (ISTA-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 |
@@ -175,10 +176,23 @@ Execute **`strata connect`** para ver as instruções da sua ferramenta. [Guia d
## Apoie o Strata
**Ajude a manter a IA local aberta para todos.** Um café ajuda a financiar o desenvolvimento e os testes em mais hardware.
**Ajude a manter a IA local aberta para todos.** Um patrocínio no GitHub ou um café ajuda a financiar o desenvolvimento e os testes em mais hardware.
<a href="https://github.com/sponsors/Niko1221"><img src="https://img.shields.io/badge/Sponsor_on_GitHub-EA4AAA?style=for-the-badge&logo=githubsponsors&logoColor=white" alt="GitHub Sponsors: apoie o Strata" height="48"></a>
<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>
### Apoiadores fiéis
Obrigado a todas as pessoas que apoiam o Strata.
<!-- supporters:start (one line per public supporter, newest last; copy a line and change the GitHub name in all four places) -->
<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>
### 忠实支持者
感谢每一位支持 Strata 的人。
<!-- supporters:start (one line per public supporter, newest last; copy a line and change the GitHub name in all four places) -->
"""Is every static-PIE program inside an AppImage still the program it was staged as? (#4337)
Why: the bundled ripgrep (`harness/node_modules/@vscode/ripgrep-linux-x64/bin/rg`) is a statically linked PIE. Such a program relocates itself at
start-up by reading its own `.dynamic` section through a fixed offset in its code. linuxdeploy, which Tauri runs over the AppDir, treats every ELF file
as a library to fix up and rewrites its RPATH; for a static PIE that moves `.dynamic` to a new segment and leaves the old place filled with a filler
byte, so the start-up code reads garbage. Reporters on #4337 see the same crash offset in every run (`rg + 0x3ffcf1`) and filler bytes (0x5A) where
`.rela.dyn` and `.dynamic` were. A static PIE whose `.dynamic` is filled does not start: tools/test_appimage_elf.py shows that on a real one.
apps/desktop/scripts/repack-appimage.sh puts the staged bytes back; this file is the check that says whether it did.
appimage_elf.py scan <folder> every static PIE under <folder> must have a sound .dynamic (exit 1 if not)
appimage_elf.py compare <staged-folder> <folder> the same files, byte for byte, plus what differs in their ELF headers
What is checked: only static PIE programs. A shared library (.so, Python's and Node's native modules, Pillow, numpy, sharp) is not one, however it is
named: it has no entry point, or a DT_SONAME and no DF_1_PIE flag, and its RPATH/RUNPATH is normal. A static PIE is an ET_DYN with no interpreter, an entry
point, and DF_1_PIE in DT_FLAGS_1; one whose .dynamic cannot be read at all (overwritten) is reported as damaged, since the overwrite takes the flags too.
Standard library only; reads ELF64 little endian (x86-64 and arm64), anything else is ignored.
out.append(f"dynamic section: {len(da)} -> {len(db)} entries"+("; an RPATH/RUNPATH was added"ifany(tin(DT_RPATH,DT_RUNPATH)fort,_indb)andnotany(tin(DT_RPATH,DT_RUNPATH)fort,_inda)else""))
returnout
def_files(folder):
forbase,dirs,namesinos.walk(folder):
dirs.sort()
fornameinsorted(names):
path=os.path.join(base,name)
ifos.path.islink(path)ornotos.path.isfile(path):
continue
yieldpath
defscan(folder):
"""{relative path: problems} for every static PIE under `folder` that has any."""
found={}
forpathin_files(folder):
try:
withopen(path,"rb")asf:
head=f.read(4)
ifhead!=b"\x7fELF":
continue
data=head+f.read()
exceptOSError:
continue
problems=check_static_pie(data)
ifproblems:
found[os.path.relpath(path,folder)]=problems
returnfound
defcompare(staged_folder,folder):
"""{relative path: sentences} for every staged ELF file that is missing or differs in `folder`."""
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(saved.prefs.contentFontSize===19&&saved.prefs.language==='de','text size and language survive a restart on the same port',JSON.stringify(saved.prefs));
description="Strata Desktop — local AI, tuned to one model."
edition="2021"
rust-version="1.77"
@@ -11,6 +11,11 @@ license = "MIT"
gtk="0.18"
webkit2gtk="2.0"
# Windows: the shell listens for the page's renderer failing (WebView2 ProcessFailed, see hook_renderer_failures in main.rs); these are the versions wry itself uses.
on_renderer_failure(format!("http://127.0.0.1:{port}"),recovery::Failure::RendererExited,"web content process terminated".to_string(),failed,move|url|{
recovery::Plan::Stop=>say("the page's renderer failed again within two minutes of two recoveries; the window is left as it is (Reload in the page loads it again)"),
recovery::Plan::Reload{attempt,url}=>{
say(&format!("loading the page again (recovery {attempt}): {url}"));
@@ -6,7 +6,7 @@ Implemented 14 October 2026 in the React/Node app under `backups/strata`.
The default is one model at a time. First launch uses the visual `GuidedSetup` funnel: **Welcome → Your model → Prepare → Make it yours → Chat**. Welcome has animated vector artwork and one expert skip at the bottom. Model selection offers friendly version cards, context, RAM/VRAM inventory with an aggregate total, and an existing-folder browser. Unified CPU/GPU memory counts once. Memory verdicts are guidance: **May run slower** does not block selection or Continue. There is no compatibility alternatives panel or picture-reading question at this stage. The preparation screen confirms the download size before **Download model**, then shows engine-reported progress alongside a clickable Chat / Files / Your setup tour. Vision-capable families always prepare their mmproj/encoder files. Preparation does not load the model automatically.
After preparation, users choose **Leave 20% for my work** or **Full performance**, and choose whether to **Read pictures when this model loads** (off initially). The optional longer native calibration measures engine settings for approximately 5–10 minutes. **Tune & start chatting** applies the resource and vision preferences, loads one model, performs a short benchmark, and completes setup only when that model is healthy. Chat opens automatically with starter prompts for a new user; selecting a prompt fills the composer without sending it. Progress, choices and operation IDs survive reloads. Finish later preserves the current stage and keeps completed files available.
After preparation, users choose **Leave room for my work** or **Full performance**, and choose whether to **Read pictures when this model loads** (off initially). The optional longer native calibration measures engine settings for approximately 5–10 minutes. **Tune & start chatting** applies the resource and vision preferences, loads one model, performs a short benchmark, and completes setup only when that model is healthy. Chat opens automatically with starter prompts for a new user; selecting a prompt fills the composer without sending it. Progress, choices and operation IDs survive reloads. Finish later preserves the current stage and keeps completed files available.
Normal Models shows Version, optional picture reading, context, Computer use and Load model. The resource preference can be changed here after onboarding. Image generation is a separate collapsed option. The selected model and context remain visible in the toolbar; saved setups and Manage models are available there. Local GGUF import is a secondary entry in Version. Imported models retain the existing metadata/shard validation and reference their files without copying them.
constlongPrompt=`Create the file big.txt in the current folder with the write tool, in ONE call. It must hold exactly ${LINES} lines. Line N is "N: " followed by one short true sentence about a different city of the world (a new city on every line, no repeats, no pattern). Write every line out yourself, do not use a script or a loop, and do not shorten it.`;
if(want('raw')){
// What the model server itself sends when a reply is cut inside a tool call (no Harness): the stream of tool_calls pieces, then finish_reason "length".
tools:[{type:'function',function:{name:'write',description:'Write a file.',parameters:{type:'object',properties:{file_path:{type:'string'},content:{type:'string'}},required:['file_path','content']}}}],
note('raw: a reply cut inside a tool call reaches the client as pieces of the call and finish_reason length',finish==='length'&&name==='write'&&pieces>5,`finish ${finish}; tool ${name}; ${pieces} call pieces, ${args.length} argument characters, closed JSON ${(()=>{try{JSON.parse(args);returntrue;} catch { return false; } })()}; plain content ${JSON.stringify(content.slice(0,80))}; ids ${ids.size}; usage ${JSON.stringify(usage)}`);
}
if(want('mcp')){
// Strata's own tool loop: the server runs the MCP tool itself. A call that finishes is run; a call the reply is cut inside is announced and never run.
constok=awaitask('Use the fake__echo tool to echo the text "hello from the tool", then tell me what it returned.',600);
note('mcp: a short call is run by the server and answered',ok.mcp.some(m=>m.event==='result'&&m.ok)&&ok.finish==='stop',`events ${JSON.stringify(ok.mcp.map(m=>m.event))}; finish ${ok.finish}; said ${JSON.stringify(ok.said.slice(-120))}`);
constcut=awaitask('Use the fake__echo tool once. Its text must be the sentence "the quick brown fox jumps over the lazy dog" written out 300 times in a row, separated by spaces.',300);
note('mcp: a call the reply is cut inside is announced and never run (#211), and the stream says length',cut.mcp.some(m=>m.event==='start')&&!cut.mcp.some(m=>m.event==='call')&&cut.finish==='length',`events ${JSON.stringify(cut.mcp.map(m=>m.event))}; finish ${cut.finish}; said ${JSON.stringify(cut.said.slice(0,100))}`);
}
if(want('pagoda')){
constt=awaitturn('pagoda','Write me a 100 voxel pagoda in one html file with garden.');
note('pagoda: a file was written or the turn says why not',!!html||/cut|too long|ran out|limit/i.test(t.text),`file ${html?html.name+' '+html.bytes+' B':'none'}; finish ${t.done?.finishReason}; text tail: ${JSON.stringify(t.text.slice(-240))}`);
* LOAD-PICS end to end: the Models page of a running host, driven in a real Chrome (DevTools protocol, no packages), pressing "Load model" on a model that is
* downloaded, with Read pictures on and the picture files not on the computer.
* It looks at the page, presses the button, and writes down once per half second what the page shows (the progress card's words and bar, any dialog, the job's kind
* and state), until the job ends or LIMIT_S passes. It writes <OUT>/timeline.json and screenshots; the files on disk are checked by the shell script around it.
try{constlist=await(awaitfetch(`http://127.0.0.1:${PORT}/json/list`)).json();constpage=list.find(t=>t.type==='page');if(page)returnpage.webSocketDebuggerUrl;}catch{/* not up yet */}
constbaseDialogs=awaitrun("document.querySelectorAll('[role=dialog], dialog[open]').length");// dialogs the page always has (closed ones the app keeps mounted)
constafter=awaitrun(`(() => ({ text: [...document.querySelectorAll('.native-spill-note, .native-ready, .toast, [role=status]')].map(n => n.innerText.trim()).filter(Boolean), buttons: [...document.querySelectorAll('.native-model-actions button')].map(b => b.textContent.trim()), pictureOffer: document.body.innerText.includes('Picture reading needs a few more files') }))()`);
constposted=awaitrun("fetch('/__fixture/posted').then(r => r.ok ? r.json() : null).catch(() => null)");// only the UI-preview fixture keeps what the page posted
"""Writes src/locales/sav-<lang>.json: the notice for chats the host could not write (owner wording 11-04, #5192).
Run from apps/host: python scripts/make-chatsave-locales.py
Every entry names all nine languages; the keys are sav01.. in the order of S. sav-en.json is the English source. Placeholders ({0}) must be kept in every
language. A separate file keeps this work from colliding with the other catalogs (the same reason upd-, df2-, pick- and the others exist)."""
"Certaines conversations n'ont pas pu être enregistrées")
add("Strata's chat storage is full, so the newest changes to {0} chat(s) are only in this window. Delete old chats to make room; they then save by themselves.",
"Der Chat-Speicher von Strata ist voll, daher gibt es die neuesten Änderungen an {0} Chat(s) nur in diesem Fenster. Löschen Sie alte Chats, um Platz zu schaffen; dann werden sie automatisch gespeichert.",
"El almacenamiento de chats de Strata está lleno, así que los cambios más recientes de {0} chat(s) solo están en esta ventana. Elimina chats antiguos para liberar espacio; después se guardarán solos.",
"Хранилище чатов Strata заполнено, поэтому последние изменения в чатах ({0}) есть только в этом окне. Удалите старые чаты, чтобы освободить место; после этого они сохранятся сами.",
"O armazenamento de chats do Strata está cheio, por isso as alterações mais recentes de {0} chat(s) só existem nesta janela. Apague chats antigos para libertar espaço; depois eles guardam-se sozinhos.",
"Strata의 채팅 저장 공간이 가득 차서 {0}개 채팅의 최신 변경 내용이 이 창에만 있습니다. 오래된 채팅을 삭제해 공간을 확보하세요. 그러면 자동으로 저장됩니다.",
"L'espace de stockage des conversations de Strata est plein : les dernières modifications de {0} conversation(s) ne sont que dans cette fenêtre. Supprimez d'anciennes conversations pour faire de la place ; elles s'enregistreront alors d'elles-mêmes.")
"""Writes src/locales/df2-<lang>.json: the strings of the Drafting choice that came with the DFlash2 option (branch f-0291-df2-option): the plain-words
paragraph under the dropdown, the three lines about the person's graphics card, the "not on this card" and "no internet" lines, and the SSD line of the
This computer panel.
Run from apps/host: python scripts/make-df2-locales.py
Every entry names all nine languages; the keys are df01.. in the order of S. df2-en.json is the English source. Placeholders ({0}) must be kept in every
language. A separate file keeps this work from colliding with the other catalogs (the same reason pick-, tune-, tools-, fix-, gh-, ui- and engine- exist).
The English lines of df03-df07 and df09 are also written in strata_control/drafting.py and strata_control/drafting_fetch.py; tests/drafting.test.mjs keeps the two the same."""
add("MTP is the default. DFlash2 is another way to guess the next words ahead, for Qwen3.8-Flash-Next only. On an RTX PRO 5000 it was 7.8% faster than MTP "
"on 19 short prompts and 1.1% faster on long thinking, and faster on 21 of 23 prompts. On the short prompts the answers were identical.",
"MTP ist die Voreinstellung. DFlash2 ist eine andere Art, die nächsten Wörter vorauszuraten, nur für Qwen3.8-Flash-Next. Auf einer RTX PRO 5000 war es bei 19 kurzen "
"Prompts 7,8 % schneller als MTP, bei langem Nachdenken 1,1 % schneller und bei 21 von 23 Prompts schneller. Bei den kurzen Prompts waren die Antworten identisch.",
"MTP es la opción predeterminada. DFlash2 es otra forma de adivinar las siguientes palabras por adelantado, solo para Qwen3.8-Flash-Next. En una RTX PRO 5000 fue un 7,8 %"
"más rápido que MTP en 19 prompts cortos y un 1,1 % más rápido en razonamientos largos, y más rápido en 21 de 23 prompts. En los prompts cortos las respuestas fueron idénticas.",
"По умолчанию используется MTP. DFlash2 — другой способ угадывать следующие слова наперёд, только для Qwen3.8-Flash-Next. На RTX PRO 5000 он был на 7,8 % быстрее MTP "
"на 19 коротких запросах и на 1,1 % быстрее на длинных рассуждениях, а на 21 из 23 запросов — быстрее. На коротких запросах ответы были идентичны.",
"MTP é o padrão. DFlash2 é outra forma de adivinhar as próximas palavras antecipadamente, só para o Qwen3.8-Flash-Next. Numa RTX PRO 5000 foi 7,8 % mais rápido que o MTP "
"em 19 prompts curtos e 1,1 % mais rápido em raciocínios longos, e mais rápido em 21 de 23 prompts. Nos prompts curtos as respostas foram idênticas.",
"기본값은 MTP입니다. DFlash2는 다음 단어를 미리 추측하는 또 다른 방식이며 Qwen3.8-Flash-Next 전용입니다. RTX PRO 5000에서 짧은 프롬프트 19개에서는 MTP보다 7.8% 빨랐고, "
"긴 사고 과정에서는 1.1% 빨랐으며, 23개 중 21개 프롬프트에서 더 빨랐습니다. 짧은 프롬프트에서는 답변이 동일했습니다.",
"MTP est le réglage par défaut. DFlash2 est une autre façon de deviner à l'avance les mots suivants, uniquement pour Qwen3.8-Flash-Next. Sur une RTX PRO 5000, il a été 7,8 %"
"plus rapide que MTP sur 19 prompts courts et 1,1 % plus rapide en réflexion longue, et plus rapide sur 21 prompts sur 23. Sur les prompts courts, les réponses étaient identiques.")
add("It downloads about 1.2 GB once, works with one graphics card and one request at a time, and uses MTP otherwise.",
"Auf Grafikkarten wie Ihrer war DFlash2 in unseren Tests bei manchen Textarten schneller als MTP und bei anderen langsamer.",
"En tarjetas gráficas como la suya, en nuestras pruebas DFlash2 fue más rápido que MTP con algunos tipos de texto y más lento con otros.",
"На видеокартах, подобных вашей, в наших тестах DFlash2 был быстрее MTP на одних видах текста и медленнее на других.",
"Em placas de vídeo como a sua, nos nossos testes o DFlash2 foi mais rápido que o MTP em alguns tipos de texto e mais lento em outros.",
"고객님과 비슷한 그래픽 카드에서 저희 테스트 결과 DFlash2는 일부 유형의 글에서는 MTP보다 빨랐고 다른 유형에서는 더 느렸습니다.",
"Sur des cartes graphiques comme la vôtre, DFlash2 a été plus rapide que MTP sur certains types de texte et plus lent sur d'autres lors de nos tests.")
add("On graphics cards like yours, DFlash2 was slower than MTP in our tests.",
"""Writes src/locales/engine-<lang>.json: the strings of Settings > Engine (the list of installed engines: switch, delete) and the words that now point to
About for updating (branch f-0291-engine-tab). Run from apps/host: python scripts/make-engine-locales.py
Every entry names all nine languages; the keys are et01.. in the order of S. engine-en.json is the English source. Placeholders ({0}, {1}) must be kept in
every language. A separate file keeps this work from colliding with the other catalogs (the same reason tune-, tools-, fix-, gh- and ui- exist)."""
"Wechsle zu einer älteren Engine, um etwas zu testen, und danach zurück zur neuesten. Das Update auf die neueste Version findest du unter „Über“.",
"Cambia a un motor anterior para probar algo y vuelve después al más reciente. La actualización a la última versión está en «Acerca de».",
"Переключитесь на более старый движок, чтобы что-то проверить, а потом вернитесь на самый новый. Обновление до последней версии — в разделе «О программе».",
"Troque para um motor mais antigo para testar algo e depois volte ao mais recente. A atualização para a versão mais recente fica em «Sobre».",
"이전 엔진으로 바꿔 테스트한 뒤 다시 최신 엔진으로 돌아올 수 있습니다. 최신 버전으로의 업데이트는 '정보'에서 합니다.",
"Passez à un moteur plus ancien pour tester quelque chose, puis revenez au plus récent. La mise à jour vers la dernière version se trouve dans « À propos ».")
add("Engines can be switched and deleted in the installed Strata app.",
"Engines lassen sich in der installierten Strata-App wechseln und löschen.","Los motores se pueden cambiar y eliminar en la aplicación Strata instalada.",
"Переключать и удалять движки можно в установленном приложении Strata.","Os motores podem ser trocados e excluídos no aplicativo Strata instalado.",
"엔진 전환과 삭제는 설치된 Strata 앱에서 할 수 있습니다.","Les moteurs peuvent être changés et supprimés dans l'application Strata installée.")
add("The installed engines couldn't be read. Try again in a moment.",
"Die installierten Engines konnten nicht gelesen werden. Versuche es gleich noch einmal.","No se pudieron leer los motores instalados. Inténtalo de nuevo en un momento.",
"Не удалось прочитать список установленных движков. Попробуйте ещё раз через минуту.","Não foi possível ler os motores instalados. Tente novamente em instantes.",
"설치된 엔진을 읽지 못했습니다. 잠시 후 다시 시도하세요.","Les moteurs installés n'ont pas pu être lus. Réessayez dans un instant.")
add("No engine is installed on this computer yet. Update in About downloads the right one.",
"Auf diesem Computer ist noch keine Engine installiert. Das Update unter „Über“ lädt die passende herunter.",
"Todavía no hay ningún motor instalado en este equipo. Actualizar en «Acerca de» descarga el adecuado.",
"На этом компьютере ещё не установлен движок. Обновление в разделе «О программе» скачает подходящий.",
"Ainda não há nenhum motor instalado neste computador. Atualizar em «Sobre» baixa o adequado.",
"이 컴퓨터에는 아직 설치된 엔진이 없습니다. '정보'에서 업데이트하면 알맞은 엔진을 내려받습니다.",
"Aucun moteur n'est encore installé sur cet ordinateur. La mise à jour dans « À propos » télécharge le bon.")
add("Engines take {0} on this computer.",
"引擎在此电脑上共占用 {0}。","エンジンはこのコンピューターで {0} を使っています。","Die Engines belegen auf diesem Computer {0}.","Los motores ocupan {0} en este equipo.",
"Движки занимают на этом компьютере {0}.","Os motores ocupam {0} neste computador.","엔진이 이 컴퓨터에서 {0}을(를) 차지합니다.","Les moteurs occupent {0} sur cet ordinateur.")
add("Newest","最新","最新","Neueste","Más reciente","Новейший","Mais recente","최신","Le plus récent")
add("Use this","使用此引擎","これを使う","Diese verwenden","Usar este","Использовать","Usar este","이 엔진 사용","Utiliser celui-ci")
add("Qwen and other models",
"Qwen 及其他模型","Qwen とその他のモデル","Qwen und andere Modelle","Qwen y otros modelos","Qwen и другие модели","Qwen e outros modelos","Qwen 및 기타 모델","Qwen et autres modèles")
add("A running program is using this engine. Unload your models first.",
"Ein laufendes Programm benutzt diese Engine. Entlade zuerst deine Modelle.","Un programa en ejecución está usando este motor. Descarga primero tus modelos.",
"Этот движок используется работающей программой. Сначала выгрузите свои модели.","Um programa em execução está usando este motor. Descarregue seus modelos primeiro.",
"실행 중인 프로그램이 이 엔진을 사용하고 있습니다. 먼저 모델을 언로드하세요.","Un programme en cours utilise ce moteur. Déchargez d'abord vos modèles.")
add("Engine {0} is now in use.",
"现在使用引擎 {0}。","エンジン {0} を使用中です。","Engine {0} wird jetzt verwendet.","Ahora se usa el motor {0}.","Теперь используется движок {0}.","O motor {0} está em uso agora.",
"이제 엔진 {0}을(를) 사용합니다.","Le moteur {0} est maintenant utilisé.")
add("Engine {0} was deleted.",
"引擎 {0} 已删除。","エンジン {0} を削除しました。","Engine {0} wurde gelöscht.","Se eliminó el motor {0}.","Движок {0} удалён.","O motor {0} foi excluído.",
"엔진 {0}을(를) 삭제했습니다.","Le moteur {0} a été supprimé.")
add("Use engine {0}?",
"使用引擎 {0}?","エンジン {0} を使いますか?","Engine {0} verwenden?","¿Usar el motor {0}?","Использовать движок {0}?","Usar o motor {0}?","엔진 {0}을(를) 사용할까요?","Utiliser le moteur {0} ?")
add("Use engine {0}",
"使用引擎 {0}","エンジン {0} を使う","Engine {0} verwenden","Usar el motor {0}","Использовать движок {0}","Usar o motor {0}","엔진 {0} 사용","Utiliser le moteur {0}")
add("Models you start from now on run on engine {0}. Your models and settings stay as they are. You can switch back at any time.",
"Modelle, die du ab jetzt startest, laufen mit Engine {0}. Deine Modelle und Einstellungen bleiben unverändert. Du kannst jederzeit zurückwechseln.",
"Los modelos que inicies a partir de ahora usarán el motor {0}. Tus modelos y ajustes no cambian. Puedes volver cuando quieras.",
"Модели, которые вы запустите с этого момента, будут работать на движке {0}. Ваши модели и настройки останутся как есть. Вернуться можно в любой момент.",
"Os modelos que você iniciar a partir de agora usarão o motor {0}. Seus modelos e ajustes continuam como estão. Você pode voltar quando quiser.",
"지금부터 시작하는 모델은 엔진 {0}에서 실행됩니다. 모델과 설정은 그대로 유지됩니다. 언제든 다시 바꿀 수 있습니다.",
"Les modèles que vous démarrez à partir de maintenant utilisent le moteur {0}. Vos modèles et réglages restent inchangés. Vous pouvez revenir en arrière à tout moment.")
add("These models need a newer engine and may not start on it: {0}.",
"Diese Modelle brauchen eine neuere Engine und starten damit eventuell nicht: {0}.","Estos modelos necesitan un motor más reciente y puede que no arranquen con este: {0}.",
"Этим моделям нужен более новый движок, и на этом они могут не запуститься: {0}.","Estes modelos precisam de um motor mais novo e podem não iniciar com este: {0}.",
"다음 모델은 더 새로운 엔진이 필요해 이 엔진에서는 시작되지 않을 수 있습니다: {0}.","Ces modèles demandent un moteur plus récent et pourraient ne pas démarrer avec celui-ci : {0}.")
add("A model is running. Unload it in Models first, then switch.",
"Ein Modell läuft. Entlade es zuerst unter „Modelle“ und wechsle dann.","Hay un modelo en ejecución. Descárgalo primero en «Modelos» y luego cambia.",
"Работает модель. Сначала выгрузите её в разделе «Модели», затем переключитесь.","Há um modelo em execução. Descarregue-o primeiro em «Modelos» e depois troque.",
"모델이 실행 중입니다. 먼저 '모델'에서 언로드한 다음 전환하세요.","Un modèle est en cours d'exécution. Déchargez-le d'abord dans « Modèles », puis changez de moteur.")
add("Delete engine {0}?",
"删除引擎 {0}?","エンジン {0} を削除しますか?","Engine {0} löschen?","¿Eliminar el motor {0}?","Удалить движок {0}?","Excluir o motor {0}?","엔진 {0}을(를) 삭제할까요?","Supprimer le moteur {0} ?")
add("Keep engine","保留引擎","エンジンを残す","Engine behalten","Conservar motor","Оставить движок","Manter motor","엔진 유지","Garder le moteur")
add("Delete engine","删除引擎","エンジンを削除","Engine löschen","Eliminar motor","Удалить движок","Excluir motor","엔진 삭제","Supprimer le moteur")
add("This frees {0}. Your models and settings are kept.",
"这将释放 {0}。你的模型和设置会保留。","{0} が空きます。モデルと設定はそのまま残ります。","Das gibt {0} frei. Deine Modelle und Einstellungen bleiben erhalten.",
"Esto libera {0}. Tus modelos y ajustes se conservan.","Это освободит {0}. Ваши модели и настройки сохранятся.","Isso libera {0}. Seus modelos e ajustes são mantidos.",
"{0}의 공간이 확보됩니다. 모델과 설정은 그대로 유지됩니다.","Cela libère {0}. Vos modèles et réglages sont conservés.")
add("The newest engine can be downloaded again with Update in About.",
"Die neueste Engine lässt sich über das Update unter „Über“ erneut herunterladen.","El motor más reciente se puede volver a descargar con Actualizar en «Acerca de».",
"Новейший движок можно скачать снова через обновление в разделе «О программе».","O motor mais recente pode ser baixado de novo com Atualizar em «Sobre».",
"최신 엔진은 '정보'의 업데이트로 다시 내려받을 수 있습니다.","Le moteur le plus récent peut être retéléchargé avec la mise à jour dans « À propos ».")
add("Strata can only download the newest engine, so a deleted older engine can't be brought back.",
"Strata kann nur die neueste Engine herunterladen. Eine gelöschte ältere Engine lässt sich deshalb nicht wiederherstellen.",
"Strata solo puede descargar el motor más reciente, así que un motor anterior eliminado no se puede recuperar.",
"Strata может скачать только новейший движок, поэтому удалённый старый движок вернуть нельзя.",
"O Strata só pode baixar o motor mais recente, então um motor antigo excluído não pode ser recuperado.",
"Strata는 최신 엔진만 내려받을 수 있으므로, 삭제한 이전 엔진은 되돌릴 수 없습니다.",
"Strata ne peut télécharger que le moteur le plus récent : un ancien moteur supprimé ne peut donc pas être récupéré.")
# ---- why an engine cannot be used on this computer
add("This engine was made for a different operating system.",
"此引擎是为其他操作系统制作的。","このエンジンは別のオペレーティングシステム向けです。","Diese Engine wurde für ein anderes Betriebssystem gebaut.",
"Este motor se creó para otro sistema operativo.","Этот движок создан для другой операционной системы.","Este motor foi feito para outro sistema operacional.",
"이 엔진은 다른 운영체제용입니다.","Ce moteur a été conçu pour un autre système d'exploitation.")
add("This engine was made for a different kind of graphics card than this computer has.",
"Diese Engine wurde für eine andere Art von Grafikkarte gebaut als die in diesem Computer.","Este motor se creó para un tipo de tarjeta gráfica distinto del de este equipo.",
"Этот движок создан для другого типа видеокарты, чем в этом компьютере.","Este motor foi feito para um tipo de placa de vídeo diferente da deste computador.",
"이 엔진은 이 컴퓨터와 다른 종류의 그래픽 카드용입니다.","Ce moteur a été conçu pour un autre type de carte graphique que celle de cet ordinateur.")
add("This engine needs Strata {0} or newer.",
"此引擎需要 Strata {0} 或更新版本。","このエンジンには Strata {0} 以降が必要です。","Diese Engine braucht Strata {0} oder neuer.","Este motor necesita Strata {0} o posterior.",
"Для этого движка нужна Strata {0} или новее.","Este motor precisa do Strata {0} ou mais recente.","이 엔진에는 Strata {0} 이상이 필요합니다.","Ce moteur nécessite Strata {0} ou plus récent.")
add("This engine doesn't support this computer's graphics card.",
"此引擎不支持这台电脑的显卡。","このエンジンは、このコンピューターのグラフィックスカードに対応していません。","Diese Engine unterstützt die Grafikkarte dieses Computers nicht.",
"Este motor no admite la tarjeta gráfica de este equipo.","Этот движок не поддерживает видеокарту этого компьютера.","Este motor não oferece suporte à placa de vídeo deste computador.",
"이 엔진은 이 컴퓨터의 그래픽 카드를 지원하지 않습니다.","Ce moteur ne prend pas en charge la carte graphique de cet ordinateur.")
add("This engine needs NVIDIA driver {0} or newer. This computer has {1}.",
"Diese Engine braucht den NVIDIA-Treiber {0} oder neuer. Dieser Computer hat {1}.","Este motor necesita el controlador NVIDIA {0} o posterior. Este equipo tiene {1}.",
"Для этого движка нужен драйвер NVIDIA {0} или новее. На этом компьютере {1}.","Este motor precisa do driver NVIDIA {0} ou mais recente. Este computador tem o {1}.",
"이 엔진에는 NVIDIA 드라이버 {0} 이상이 필요합니다. 이 컴퓨터는 {1}입니다.","Ce moteur nécessite le pilote NVIDIA {0} ou plus récent. Cet ordinateur a le {1}.")
add("Some of this engine's files are missing or changed, so it can't be started.",
"Einige Dateien dieser Engine fehlen oder wurden verändert, deshalb lässt sie sich nicht starten.","Faltan archivos de este motor o han cambiado, así que no se puede iniciar.",
"Часть файлов этого движка отсутствует или изменена, поэтому его нельзя запустить.","Alguns arquivos deste motor estão ausentes ou foram alterados, então ele não pode ser iniciado.",
"이 엔진의 일부 파일이 없거나 변경되어 시작할 수 없습니다.","Certains fichiers de ce moteur sont absents ou modifiés : il ne peut pas démarrer.")
add("This engine's description is missing or damaged, so it can't be started.",
"Die Beschreibung dieser Engine fehlt oder ist beschädigt, deshalb lässt sie sich nicht starten.","Falta la descripción de este motor o está dañada, así que no se puede iniciar.",
"Описание этого движка отсутствует или повреждено, поэтому его нельзя запустить.","A descrição deste motor está ausente ou danificada, então ele não pode ser iniciado.",
"이 엔진의 설명 파일이 없거나 손상되어 시작할 수 없습니다.","La description de ce moteur est absente ou endommagée : il ne peut pas démarrer.")
# ---- updating moved to About: the words that sent people to Settings > Engine
add("Strata didn’t recognise an engine for this computer. The model will download, but it may not run until the right engine is installed (Settings → About).",
"Strata hat für diesen Computer keine Engine erkannt. Das Modell wird heruntergeladen, läuft aber möglicherweise erst, wenn die passende Engine installiert ist (Einstellungen → Über).",
"Strata no reconoció ningún motor para este equipo. El modelo se descargará, pero puede que no funcione hasta que se instale el motor adecuado (Ajustes → Acerca de).",
"Strata не распознала движок для этого компьютера. Модель скачается, но может не запуститься, пока не установлен подходящий движок (Настройки → О программе).",
"O Strata não reconheceu um motor para este computador. O modelo será baixado, mas pode não funcionar até que o motor certo seja instalado (Configurações → Sobre).",
"Strata가 이 컴퓨터용 엔진을 인식하지 못했습니다. 모델은 내려받지만, 알맞은 엔진이 설치되기 전에는 실행되지 않을 수 있습니다(설정 → 정보).",
"Strata n’a pas reconnu de moteur pour cet ordinateur. Le modèle sera téléchargé, mais il risque de ne pas fonctionner tant que le bon moteur n’est pas installé (Réglages → À propos).")
add("Strata didn’t recognise an engine for this computer. The model is downloaded, but it can’t run until the right engine is installed. Open Settings → About.",
"Strata hat für diesen Computer keine Engine erkannt. Das Modell ist heruntergeladen, läuft aber erst, wenn die passende Engine installiert ist. Öffne Einstellungen → Über.",
"Strata no reconoció ningún motor para este equipo. El modelo está descargado, pero no puede funcionar hasta que se instale el motor adecuado. Abre Ajustes → Acerca de.",
"Strata не распознала движок для этого компьютера. Модель скачана, но не запустится, пока не установлен подходящий движок. Откройте Настройки → О программе.",
"O Strata não reconheceu um motor para este computador. O modelo foi baixado, mas não pode funcionar até que o motor certo seja instalado. Abra Configurações → Sobre.",
"Strata가 이 컴퓨터용 엔진을 인식하지 못했습니다. 모델은 내려받았지만, 알맞은 엔진이 설치되기 전에는 실행할 수 없습니다. 설정 → 정보를 여세요.",
"Strata n’a pas reconnu de moteur pour cet ordinateur. Le modèle est téléchargé, mais il ne peut pas fonctionner tant que le bon moteur n’est pas installé. Ouvrez Réglages → À propos.")
add("Update the engine (Settings, About), then try again.",
"请更新引擎(设置,关于),然后重试。","エンジンを更新して(設定、情報)、もう一度お試しください。","Aktualisiere die Engine (Einstellungen, Über) und versuche es dann erneut.",
"Actualiza el motor (Ajustes, Acerca de) y vuelve a intentarlo.","Обновите движок (Настройки, О программе) и попробуйте снова.","Atualize o motor (Configurações, Sobre) e tente novamente.",
"엔진을 업데이트(설정, 정보)한 뒤 다시 시도하세요.","Mettez le moteur à jour (Réglages, À propos), puis réessayez.")
# ---- answers of the host (shown as they are, so each one is a catalog entry)
add("A model is running, so nothing was changed. Unload the model in Models, then try again.",
"Ein Modell läuft, deshalb wurde nichts geändert. Entlade das Modell unter „Modelle“ und versuche es dann erneut.","Hay un modelo en ejecución, así que no se cambió nada. Descarga el modelo en «Modelos» y vuelve a intentarlo.",
"Работает модель, поэтому ничего не изменено. Выгрузите модель в разделе «Модели» и попробуйте снова.","Há um modelo em execução, então nada foi alterado. Descarregue o modelo em «Modelos» e tente novamente.",
"모델이 실행 중이라 아무것도 바뀌지 않았습니다. '모델'에서 모델을 언로드한 다음 다시 시도하세요.","Un modèle est en cours d'exécution, rien n'a donc été modifié. Déchargez le modèle dans « Modèles », puis réessayez.")
add("This is the engine in use. Switch to another one first, then delete it.",
"Das ist die verwendete Engine. Wechsle zuerst zu einer anderen und lösche sie dann.","Este es el motor en uso. Cambia primero a otro y luego elimínalo.",
"Это используемый движок. Сначала переключитесь на другой, затем удалите его.","Este é o motor em uso. Troque primeiro para outro e depois exclua-o.",
"사용 중인 엔진입니다. 먼저 다른 엔진으로 전환한 다음 삭제하세요.","C'est le moteur utilisé. Passez d'abord à un autre, puis supprimez-le.")
add("A program is still using this engine, so it was not deleted. Unload your models and try again.",
"Ein Programm benutzt diese Engine noch, deshalb wurde sie nicht gelöscht. Entlade deine Modelle und versuche es erneut.","Un programa todavía usa este motor, así que no se eliminó. Descarga tus modelos y vuelve a intentarlo.",
"Этот движок ещё используется программой, поэтому он не удалён. Выгрузите модели и попробуйте снова.","Um programa ainda está usando este motor, então ele não foi excluído. Descarregue seus modelos e tente novamente.",
"프로그램이 아직 이 엔진을 사용하고 있어 삭제하지 않았습니다. 모델을 언로드한 다음 다시 시도하세요.","Un programme utilise encore ce moteur : il n'a pas été supprimé. Déchargez vos modèles, puis réessayez.")
add("That engine is already gone. The list has been refreshed.",
"该引擎已经不存在。列表已刷新。","そのエンジンはすでにありません。一覧を更新しました。","Diese Engine ist schon nicht mehr da. Die Liste wurde aktualisiert.",
"Ese motor ya no está. La lista se ha actualizado.","Этого движка уже нет. Список обновлён.","Esse motor já não existe. A lista foi atualizada.","해당 엔진은 이미 없습니다. 목록을 새로 고쳤습니다.","Ce moteur n'existe déjà plus. La liste a été actualisée.")
add("An update is running. Wait for it to finish, then try again.",
"正在进行更新。请等它完成后再试。","更新を実行中です。終わるまで待ってから、もう一度お試しください。","Ein Update läuft. Warte, bis es fertig ist, und versuche es dann erneut.",
"Hay una actualización en curso. Espera a que termine y vuelve a intentarlo.","Идёт обновление. Дождитесь его окончания и попробуйте снова.","Uma atualização está em andamento. Aguarde o fim e tente novamente.",
"업데이트가 진행 중입니다. 끝날 때까지 기다린 다음 다시 시도하세요.","Une mise à jour est en cours. Attendez qu'elle se termine, puis réessayez.")
add("This engine can't run on this computer, so nothing was changed.",
"此引擎无法在这台电脑上运行,所以没有做任何更改。","このエンジンはこのコンピューターでは動かないため、何も変更していません。","Diese Engine läuft auf diesem Computer nicht, deshalb wurde nichts geändert.",
"Este motor no puede funcionar en este equipo, así que no se cambió nada.","Этот движок не может работать на этом компьютере, поэтому ничего не изменено.","Este motor não pode funcionar neste computador, então nada foi alterado.",
"이 엔진은 이 컴퓨터에서 실행할 수 없어 아무것도 바뀌지 않았습니다.","Ce moteur ne peut pas fonctionner sur cet ordinateur : rien n'a été modifié.")
add("The engine wasn't deleted. Nothing else was changed.",
"引擎没有被删除。其他内容均未更改。","エンジンは削除されませんでした。ほかには何も変更していません。","Die Engine wurde nicht gelöscht. Sonst wurde nichts geändert.",
"El motor no se eliminó. No se cambió nada más.","Движок не удалён. Больше ничего не изменено.","O motor não foi excluído. Nada mais foi alterado.","엔진이 삭제되지 않았습니다. 다른 것은 바뀌지 않았습니다.","Le moteur n'a pas été supprimé. Rien d'autre n'a été modifié.")
add("The engine wasn't switched. Nothing was changed.",
"引擎没有被切换。没有做任何更改。","エンジンは切り替わりませんでした。何も変更していません。","Die Engine wurde nicht gewechselt. Es wurde nichts geändert.",
"No se cambió el motor. No se modificó nada.","Движок не переключён. Ничего не изменено.","O motor não foi trocado. Nada foi alterado.","엔진이 전환되지 않았습니다. 아무것도 바뀌지 않았습니다.","Le moteur n'a pas été changé. Rien n'a été modifié.")
add("Choose an engine from the list.",
"请从列表中选择一个引擎。","一覧からエンジンを選んでください。","Wähle eine Engine aus der Liste.","Elige un motor de la lista.","Выберите движок из списка.","Escolha um motor da lista.","목록에서 엔진을 선택하세요.","Choisissez un moteur dans la liste.")
add("A newer engine is already on this computer. Update switches to it; nothing is downloaded.",
"Auf diesem Computer ist schon eine neuere Engine. Das Update wechselt zu ihr; es wird nichts heruntergeladen.","Este equipo ya tiene un motor más reciente. Actualizar cambia a él; no se descarga nada.",
"На этом компьютере уже есть более новый движок. Обновление переключит на него; ничего не скачивается.","Este computador já tem um motor mais novo. Atualizar troca para ele; nada é baixado.",
"이 컴퓨터에 이미 더 새로운 엔진이 있습니다. 업데이트하면 그 엔진으로 전환되며, 내려받는 것은 없습니다.","Cet ordinateur a déjà un moteur plus récent. La mise à jour bascule dessus ; rien n'est téléchargé.")
"""Writes src/locales/gui-<lang>.json: the strings of the GUI feedback round 2 (branch f-0291-gui-fb2): the new name and hint of the "Leave room for my work"
choice (it keeps 5-8 GB of memory free; it was "Leave 20% for my work"), the memory-error sentence that names it, and the words of the compact files block
("Delivered to", "Working folder", "{0} files").
Run from apps/host: python scripts/make-gui-locales.py
Every entry names all nine languages; the keys are gf01.. in the order of S. gui-en.json is the English source. Placeholders ({0}) must be kept in every language.
A separate file keeps this work from colliding with the other catalogs (the same reason pick-, tune-, tools-, fix-, gh-, ui-, engine- and df2- exist)."""
"为我的工作留出空间","作業用の余裕を残す","Platz für meine Arbeit lassen","Dejar espacio para mi trabajo",
"Оставить запас для моей работы","Deixar espaço para o meu trabalho","내 작업을 위한 여유 남기기","Garder de la place pour mon travail")
add("Keeps 5-8 GB of memory free for other programs.",
"为其他程序保留 5-8 GB 空闲内存。","他のプログラム用に 5~8 GB のメモリを空けておきます。","Hält 5-8 GB Arbeitsspeicher für andere Programme frei.",
"Mantiene libres 5-8 GB de memoria para otros programas.","Оставляет 5-8 ГБ памяти свободными для других программ.",
"Mantém 5-8 GB de memória livres para outros programas.","다른 프로그램을 위해 메모리 5-8 GB를 비워 둡니다.","Garde 5-8 Go de mémoire libres pour les autres programmes.")
add("Close other programs and try again, or choose “Leave room for my work”. In Expert mode you can also turn on the low-RAM option, so part of the model is read from the SSD.",
"Schließen Sie andere Programme und versuchen Sie es erneut, oder wählen Sie „Platz für meine Arbeit lassen“. Im Expertenmodus können Sie außerdem die Option für wenig Arbeitsspeicher aktivieren, sodass ein Teil des Modells von der SSD gelesen wird.",
"Cierre otros programas e inténtelo de nuevo, o elija «Dejar espacio para mi trabajo». En el modo experto también puede activar la opción de poca RAM, para que una parte del modelo se lea desde el SSD.",
"Закройте другие программы и повторите попытку или выберите «Оставить запас для моей работы». В экспертном режиме можно также включить режим малого объёма ОЗУ, и часть модели будет читаться с SSD.",
"Feche outros programas e tente novamente, ou escolha “Deixar espaço para o meu trabalho”. No modo especialista, você também pode ativar a opção de pouca RAM, para que parte do modelo seja lida do SSD.",
"다른 프로그램을 닫고 다시 시도하거나 “내 작업을 위한 여유 남기기”를 선택하세요. 전문가 모드에서는 저RAM 옵션을 켜서 모델의 일부를 SSD에서 읽게 할 수도 있습니다.",
"Fermez les autres programmes et réessayez, ou choisissez « Garder de la place pour mon travail ». En mode expert, vous pouvez aussi activer l'option de faible RAM : une partie du modèle est alors lue depuis le SSD.")
"""Writes src/locales/pick-<lang>.json: the strings of the model version list (VersionPicker), the code-block preview button and the This computer panel (branch f-0291-ui-fixes2).
Run from apps/host: python scripts/make-pick-locales.py
Every entry names all nine languages; the keys are vp01.. in the order of S. pick-en.json is the English source. Placeholders ({0}, {1}) must be kept
in every language. A separate file keeps this work from colliding with the other catalogs (the same reason tune-, tools-, fix-, gh-, ui- and engine- exist)."""
"下载 {0}","ダウンロード {0}","{0} Download","Descarga de {0}","Загрузка {0}","Download de {0}","다운로드 {0}","Téléchargement de {0}")
add("Needs {0} of memory",
"需要 {0} 内存","{0} のメモリーが必要","Braucht {0} Speicher","Necesita {0} de memoria","Нужно {0} памяти","Precisa de {0} de memória","메모리 {0} 필요","Nécessite {0} de mémoire")
add("Needs {0} of memory and about {1} GB of disk",
"需要 {0} 内存和约 {1} GB 磁盘空间","{0} のメモリーと約 {1} GB のディスク容量が必要","Braucht {0} Speicher und etwa {1} GB Festplattenplatz","Necesita {0} de memoria y unos {1} GB de disco",
"Нужно {0} памяти и около {1} ГБ на диске","Precisa de {0} de memória e cerca de {1} GB de disco","메모리 {0}와 디스크 약 {1} GB 필요","Nécessite {0} de mémoire et environ {1} Go de disque")
"可以运行,部分内容从 SSD 读取","SSD から読み込みながら動きます","Passt mit SSD-Streaming","Cabe con streaming desde el SSD","Помещается с потоковой подгрузкой с SSD","Cabe com streaming do SSD","SSD 스트리밍으로 실행됩니다","Tient avec streaming depuis le SSD")
"显示详情","詳細を表示","Details anzeigen","Mostrar detalles","Показать подробности","Mostrar detalhes","자세히 보기","Afficher les détails")
add("Hide details",
"隐藏详情","詳細を隠す","Details ausblenden","Ocultar detalles","Скрыть подробности","Ocultar detalhes","자세히 숨기기","Masquer les détails")
add("{0} RAM + VRAM",
"{0} 内存 + 显存","{0} の RAM + VRAM","{0} RAM + VRAM","{0} de RAM + VRAM","{0} RAM + VRAM","{0} de RAM + VRAM","RAM + VRAM {0}","{0} de RAM + VRAM")
add("Close preview",
"关闭预览","プレビューを閉じる","Vorschau schließen","Cerrar vista previa","Закрыть предпросмотр","Fechar pré-visualização","미리보기 닫기","Fermer l'aperçu")
add("Free right now",
"当前可用","現在の空き","Gerade frei","Libre ahora","Свободно сейчас","Livre agora","지금 비어 있음","Libre en ce moment")
add("{0} free",
"可用 {0}","空き {0}","{0} frei","{0} libres","свободно {0}","{0} livres","{0} 사용 가능","{0} libres")
add("{0}: {1} free of {2}",
"{0}:{2} 中有 {1} 可用","{0}:{2} のうち {1} が空き","{0}: {1} frei von {2}","{0}: {1} libres de {2}","{0}: свободно {1} из {2}","{0}: {1} livres de {2}","{0}: {2} 중 {1} 사용 가능","{0} : {1} libres sur {2}")
add("It takes about",
"它大约占用","使用量の目安","Es belegt etwa","Ocupa aproximadamente","Занимает примерно","Ocupa cerca de","대략 사용하는 양","Il occupe environ")
add("Nothing. It fits in memory.",
"没有。它完全放得进内存。","ありません。すべてメモリーに収まっています。","Nichts. Es passt in den Speicher.","Nada. Cabe en la memoria.","Ничего. Всё помещается в память.","Nada. Cabe na memória.","없음. 메모리에 모두 들어갑니다.","Rien. Tout tient en mémoire.")
add("Reading from your SSD now: {0}",
"此刻正从 SSD 读取:{0}","現在 SSD から読み込み中:{0}","Gerade vom SSD gelesen: {0}","Leyendo del SSD ahora: {0}","Сейчас читается с SSD: {0}","Lendo do SSD agora: {0}","지금 SSD에서 읽는 중: {0}","Lecture depuis le SSD en cours : {0}")
add("This version would take about {0} of graphics memory and {1} of RAM.",
"这个版本大约会占用 {0} 显存和 {1} 内存。","このバージョンはグラフィックスメモリーを約 {0}、RAM を約 {1} 使います。","Diese Version würde etwa {0} Grafikspeicher und {1} RAM belegen.",
"Esta versión ocuparía aproximadamente {0} de memoria gráfica y {1} de RAM.","Эта версия займёт примерно {0} видеопамяти и {1} оперативной памяти.","Esta versão ocuparia cerca de {0} de memória gráfica e {1} de RAM.",
"이 버전은 그래픽 메모리 약 {0}와 RAM 약 {1}을 사용합니다.","Cette version occuperait environ {0} de mémoire graphique et {1} de RAM.")
"""Writes src/locales/r3-<lang>.json: the strings added by the owner's GUI feedback round 3 (branch f-0291-ui-r3).
Run from apps/host: python scripts/make-r3-locales.py
Every entry names all nine languages; the keys are r3_01.. in the order of S. r3-en.json is the English source. Placeholders ({0}, {1}) must be kept in every
language. A separate file keeps this work from colliding with the other catalogs (the same reason pick-, upd-, df2- and the others exist)."""
# 2: the Read pictures row while the installed engine has no picture reader (a tooltip, not a paragraph)
add("Needs an engine update",
"需要更新引擎","エンジンの更新が必要です","Braucht ein Engine-Update","Necesita actualizar el motor","Нужно обновить движок","Precisa de uma atualização do motor","엔진 업데이트가 필요합니다","Nécessite une mise à jour du moteur")
# 5: the one word the version list writes for a version that does not fully fit in memory
add("SSD streaming · will run slower",
"SSD 流式读取 · 运行会更慢","SSD ストリーミング · 動作は遅くなります","SSD-Streaming · läuft langsamer","Streaming desde el SSD · irá más lento","Потоковая подгрузка с SSD · будет работать медленнее",
"Streaming do SSD · vai rodar mais devagar","SSD 스트리밍 · 더 느리게 실행됩니다","Streaming depuis le SSD · plus lent")
# 6: the warning under the conversation context when a long context leaves little graphics memory for the model (nothing is refused or lowered)
add("This much context takes about {0} GB of graphics memory and leaves little room for the model on this card. Strata will try it; if it does not start, choose a shorter context.",
"So viel Kontext belegt etwa {0} GB Grafikspeicher und lässt auf dieser Karte wenig Platz für das Modell. Strata versucht es; startet es nicht, wähle einen kürzeren Kontext.",
"Este contexto ocupa unos {0} GB de memoria gráfica y deja poco espacio para el modelo en esta tarjeta. Strata lo intentará; si no arranca, elige un contexto más corto.",
"Такой контекст занимает около {0} ГБ видеопамяти и оставляет модели мало места на этой карте. Strata попробует запустить; если не получится, выберите контекст короче.",
"Esse contexto ocupa cerca de {0} GB de memória gráfica e deixa pouco espaço para o modelo nesta placa. O Strata vai tentar; se não iniciar, escolha um contexto menor.",
"이 정도의 컨텍스트는 그래픽 메모리를 약 {0} GB 사용하며 이 카드에서는 모델이 쓸 공간이 거의 남지 않습니다. Strata가 시도하지만 시작되지 않으면 더 짧은 컨텍스트를 선택하세요.",
"Ce contexte occupe environ {0} Go de mémoire graphique et laisse peu de place au modèle sur cette carte. Strata va essayer ; s'il ne démarre pas, choisissez un contexte plus court.")
# 9: a chat that is working while the person is elsewhere (the ring on its row, and the model chip's line)
add("Working · {0}",
"进行中 · {0}","作業中 · {0}","In Arbeit · {0}","Trabajando · {0}","В работе · {0}","Trabalhando · {0}","작업 중 · {0}","En cours de travail · {0}")
add("{0} chats working",
"{0} 个对话进行中","{0} 件のチャットが作業中","{0} Chats in Arbeit","{0} chats trabajando","В работе чатов: {0}","{0} chats trabalhando","대화 {0}개 작업 중","{0} conversations en cours")
de="Strata konnte die Abstimmungsdaten dieses Modells nicht lesen, deshalb lässt sich die Abstimmung nicht starten. Schließe dieses Fenster und öffne es erneut.",
es="Strata no pudo leer los datos de ajuste de este modelo, así que el ajuste no puede empezar. Cierra esta ventana y ábrela de nuevo.",
ru="Strata не смогла прочитать данные настройки этой модели, поэтому настройку нельзя запустить. Закройте это окно и откройте его снова.",
pt="O Strata não conseguiu ler os dados de ajuste deste modelo, então o ajuste não pode começar. Feche esta janela e abra-a novamente.",
ko="Strata가 이 모델의 최적화 정보를 읽지 못해 최적화를 시작할 수 없습니다. 이 창을 닫았다가 다시 여세요.",
fr="Strata n’a pas pu lire les données d’optimisation de ce modèle, l’optimisation ne peut donc pas démarrer. Fermez cette fenêtre et rouvrez-la.")
add(en="Checking for saved tunings\u2026",
zh="正在检查已保存的调优\u2026",ja="保存済みチューニングを確認しています\u2026",de="Gespeicherte Abstimmungen werden geprüft\u2026",es="Comprobando los ajustes guardados\u2026",
ru="Проверяем сохранённые настройки\u2026",pt="Verificando os ajustes salvos\u2026",ko="저장된 최적화를 확인하는 중\u2026",fr="Vérification des optimisations enregistrées\u2026")
# #4802 the progress card
add(en="Still working for {0}. You don't need to do anything.",
zh="仍在处理,已用时 {0}。你无需做任何操作。",
ja="処理中です({0}経過)。操作は必要ありません。",
de="Läuft noch seit {0}. Du musst nichts tun.",
es="Sigue trabajando desde hace {0}. No tienes que hacer nada.",
ru="Всё ещё работает: {0}. От вас ничего не требуется.",
pt="Ainda trabalhando há {0}. Você não precisa fazer nada.",
ko="{0}째 작업 중입니다. 따로 할 일은 없습니다.",
fr="Toujours en cours depuis {0}. Vous n'avez rien à faire.")
add(en="The buttons below wait until this finishes.",
zh="下方的按钮会在此操作完成后恢复可用。",
ja="この処理が終わるまで、下のボタンは使えません。",
de="Die Schaltflächen unten warten, bis das fertig ist.",
es="Los botones de abajo esperan a que esto termine.",
ru="Кнопки ниже подождут, пока это не закончится.",
pt="Os botões abaixo esperam até isto terminar.",
ko="아래 버튼은 이 작업이 끝날 때까지 기다립니다.",
fr="Les boutons ci-dessous attendent la fin de cette opération.")
# #4798 two addresses
add(en="API base URL on this computer",
zh="此电脑上的 API 基础地址",ja="このパソコンでの API ベース URL",de="API-Basis-URL auf diesem Computer",es="URL base de la API en este equipo",
ru="Базовый URL API на этом компьютере",pt="URL base da API neste computador",ko="이 컴퓨터의 API 기본 URL",fr="URL de base de l'API sur cet ordinateur")
add(en="Address for other devices",
zh="其他设备使用的地址",ja="他のデバイス用のアドレス",de="Adresse für andere Geräte",es="Dirección para otros dispositivos",
ru="Адрес для других устройств",pt="Endereço para outros dispositivos",ko="다른 기기용 주소",fr="Adresse pour les autres appareils")
add(en="Other devices use this address. It has its own port, separate from the one Strata's window uses, because only the model's API is shared on the network, not the app.",
ja="他のデバイスはこのアドレスを使います。ネットワークで共有されるのはモデルの API だけでアプリ本体ではないため、Strata のウィンドウが使うポートとは別のポートを使います。",
de="Andere Geräte verwenden diese Adresse. Sie hat einen eigenen Port, getrennt von dem des Strata-Fensters, weil im Netzwerk nur die API des Modells freigegeben wird, nicht die App.",
es="Los demás dispositivos usan esta dirección. Tiene su propio puerto, distinto del que usa la ventana de Strata, porque en la red solo se comparte la API del modelo, no la aplicación.",
ru="Другие устройства используют этот адрес. У него свой порт, отдельный от порта окна Strata, потому что в сети доступен только API модели, а не само приложение.",
pt="Os outros dispositivos usam este endereço. Ele tem a própria porta, separada da que a janela do Strata usa, porque só a API do modelo é compartilhada na rede, não o aplicativo.",
ko="다른 기기는 이 주소를 사용합니다. 네트워크에는 앱이 아니라 모델의 API만 공유되므로, Strata 창이 쓰는 포트와는 별도의 포트를 씁니다.",
fr="Les autres appareils utilisent cette adresse. Elle a son propre port, distinct de celui de la fenêtre de Strata, car seule l'API du modèle est partagée sur le réseau, pas l'application.")
# #3723 Context memory and the out-of-graphics-memory reason
add(en="INT8 is the standard: good quality and a medium amount of memory.",
zh="INT8 是标准选项:质量好,内存占用适中。",
ja="INT8 は標準の設定です。品質が良く、メモリ使用量は中程度です。",
de="INT8 ist der Standard: gute Qualität und mittlerer Speicherbedarf.",
es="INT8 es el estándar: buena calidad y un uso de memoria medio.",
ru="INT8 — стандартный вариант: хорошее качество и средний расход памяти.",
pt="INT8 é o padrão: boa qualidade e uso médio de memória.",
ko="INT8은 표준 설정입니다. 품질이 좋고 메모리 사용량은 보통입니다.",
fr="INT8 est le réglage standard : bonne qualité et une consommation de mémoire moyenne.")
add(en="Q4 uses about half the memory of INT8, but is less precise on very long documents.",
zh="引用助手的话",ja="アシスタントの発言を引用",de="Zitat des Assistenten",es="Citando al asistente",
ru="Цитата ассистента",pt="Citando o assistente",ko="어시스턴트의 말을 인용",fr="Citation de l'assistant")
add(en="Quoting you",
zh="引用你的话",ja="あなたの発言を引用",de="Zitat von dir",es="Citándote a ti",
ru="Ваша цитата",pt="Citando você",ko="내 말을 인용",fr="Citation de vous")
add(en="Remove this quote",
zh="移除这条引用",ja="この引用を外す",de="Dieses Zitat entfernen",es="Quitar esta cita",
ru="Убрать эту цитату",pt="Remover esta citação",ko="이 인용 제거",fr="Retirer cette citation")
add(en="Go to the quoted message",
zh="跳到被引用的消息",ja="引用元のメッセージへ移動",de="Zur zitierten Nachricht springen",es="Ir al mensaje citado",
ru="Перейти к цитируемому сообщению",pt="Ir para a mensagem citada",ko="인용한 메시지로 이동",fr="Aller au message cité")
add(en="Quoted",
zh="引用",ja="引用",de="Zitiert",es="Citado",
ru="Цитата",pt="Citado",ko="인용됨",fr="Cité")
add(en="Show or hide the rest of the quote",
zh="显示或隐藏引用的其余部分",ja="引用の続きを表示または非表示",de="Rest des Zitats ein- oder ausblenden",es="Mostrar u ocultar el resto de la cita",
ru="Показать или скрыть остальную часть цитаты",pt="Mostrar ou ocultar o resto da citação",ko="인용의 나머지 부분 보기 또는 숨기기",fr="Afficher ou masquer la suite de la citation")
# #4244 the agent's own questions
add(en="Skip",
zh="跳过",ja="スキップ",de="Überspringen",es="Omitir",
ru="Пропустить",pt="Pular",ko="건너뛰기",fr="Passer")
add(en="The assistant has a question for you",
zh="助手有一个问题要问你",ja="アシスタントからの質問があります",de="Der Assistent hat eine Frage an dich",es="El asistente tiene una pregunta para ti",
ru="У ассистента есть вопрос к вам",pt="O assistente tem uma pergunta para você",ko="어시스턴트가 질문이 있습니다",fr="L'assistant a une question pour vous")
add(en="Type your own answer",
zh="输入你自己的回答",ja="自分の答えを入力",de="Eigene Antwort eingeben",es="Escribe tu propia respuesta",
ru="Введите свой ответ",pt="Digite a sua própria resposta",ko="직접 답변 입력",fr="Saisissez votre propre réponse")
"""Writes src/locales/upd-<lang>.json: the strings of the engine update beside a loaded model (the dialog of Settings > About, branch f-0291-ui-final).
Run from apps/host: python scripts/make-update-locales.py
Every entry names all nine languages; the keys are upd01.. in the order of S. upd-en.json is the English source. Placeholders ({0}) must be kept in every
language. A separate file keeps this work from colliding with the other catalogs (the same reason pick-, tune-, tools-, fix-, gh-, ui-, engine- and df2- exist)."""
"Um die Engine zu aktualisieren, muss Strata das geladene Modell anhalten: {0}.",
"Para actualizar el motor, Strata tiene que detener el modelo cargado: {0}.",
"Чтобы обновить движок, Strata должна остановить загруженную модель: {0}.",
"Para atualizar o motor, o Strata precisa parar o modelo carregado: {0}.",
"엔진을 업데이트하려면 Strata가 불러온 모델을 중지해야 합니다: {0}.",
"Pour mettre à jour le moteur, Strata doit arrêter le modèle chargé : {0}.")
add("The new engine downloads first while the model keeps answering. Then the model stops (answers in progress finish first), the engine is updated, and you can start the model again with the same settings. Your models and settings are kept.",
"Zuerst wird die neue Engine heruntergeladen, während das Modell weiter antwortet. Danach hält das Modell an (laufende Antworten werden zuerst beendet), die Engine wird aktualisiert, und Sie können das Modell mit denselben Einstellungen wieder starten. Ihre Modelle und Einstellungen bleiben erhalten.",
"Primero se descarga el motor nuevo mientras el modelo sigue respondiendo. Después el modelo se detiene (las respuestas en curso terminan antes), se actualiza el motor y puedes volver a iniciar el modelo con los mismos ajustes. Tus modelos y ajustes se conservan.",
"Сначала новый движок скачивается, пока модель продолжает отвечать. Затем модель останавливается (ответы в процессе сначала завершаются), движок обновляется, и вы можете снова запустить модель с теми же настройками. Ваши модели и настройки сохраняются.",
"Primeiro o novo motor é baixado enquanto o modelo continua a responder. Depois o modelo para (as respostas em curso terminam antes), o motor é atualizado e pode iniciar o modelo de novo com as mesmas definições. Os seus modelos e definições são mantidos.",
"먼저 모델이 계속 응답하는 동안 새 엔진을 다운로드합니다. 그다음 모델이 중지되고(진행 중인 답변은 먼저 끝납니다) 엔진이 업데이트되며, 같은 설정으로 모델을 다시 시작할 수 있습니다. 모델과 설정은 그대로 유지됩니다.",
"Le nouveau moteur est d'abord téléchargé pendant que le modèle continue de répondre. Ensuite le modèle s'arrête (les réponses en cours se terminent d'abord), le moteur est mis à jour, et vous pouvez redémarrer le modèle avec les mêmes réglages. Vos modèles et réglages sont conservés.")
add("Stop and update","停止并更新","停止して更新","Anhalten und aktualisieren","Detener y actualizar","Остановить и обновить","Parar e atualizar","중지하고 업데이트","Arrêter et mettre à jour")
add("Getting the new engine ready. {0} keeps working until it is ready.",
"正在准备新引擎。在准备好之前,{0} 会继续工作。",
"新しいエンジンを準備しています。準備ができるまで {0} は動き続けます。",
"Die neue Engine wird vorbereitet. {0} arbeitet weiter, bis sie bereit ist.",
"Preparando el motor nuevo. {0} sigue funcionando hasta que esté listo.",
"Готовим новый движок. {0} продолжает работать, пока он не будет готов.",
"A preparar o novo motor. {0} continua a funcionar até estar pronto.",
"새 엔진을 준비하는 중입니다. 준비될 때까지 {0}은(는) 계속 작동합니다.",
"Préparation du nouveau moteur. {0} continue de fonctionner jusqu'à ce qu'il soit prêt.")
add("Stopping {0}. Answers in progress finish first.",
"正在停止 {0}。正在进行的回答会先完成。",
"{0} を停止しています。進行中の回答は先に完了します。",
"{0} wird angehalten. Laufende Antworten werden zuerst beendet.",
"Deteniendo {0}. Las respuestas en curso terminan antes.",
"Останавливаем {0}. Ответы в процессе сначала завершаются.",
"A parar {0}. As respostas em curso terminam antes.",
"{0}을(를) 중지하는 중입니다. 진행 중인 답변은 먼저 끝납니다.",
"Arrêt de {0}. Les réponses en cours se terminent d'abord.")
add("{0} is stopped. Updating the engine…",
"{0} 已停止。正在更新引擎…",
"{0} を停止しました。エンジンを更新しています…",
"{0} ist angehalten. Die Engine wird aktualisiert…",
"{0} está detenido. Actualizando el motor…",
"{0} остановлена. Обновляем движок…",
"{0} está parado. A atualizar o motor…",
"{0}이(가) 중지되었습니다. 엔진을 업데이트하는 중입니다…",
"{0} est arrêté. Mise à jour du moteur…")
add("Updating the engine","正在更新引擎","エンジンを更新しています","Engine wird aktualisiert","Actualizando el motor","Обновление движка","A atualizar o motor","엔진 업데이트 중","Mise à jour du moteur")
add("The engine is updated","引擎已更新","エンジンを更新しました","Die Engine ist aktualisiert","El motor está actualizado","Движок обновлён","O motor está atualizado","엔진이 업데이트되었습니다","Le moteur est à jour")
add("The engine was not updated","引擎未更新","エンジンは更新されませんでした","Die Engine wurde nicht aktualisiert","El motor no se actualizó","Движок не обновлён","O motor não foi atualizado","엔진이 업데이트되지 않았습니다","Le moteur n'a pas été mis à jour")
add("The engine is updated. Start {0} again with the same settings?",
"引擎已更新。要用相同的设置再次启动 {0} 吗?",
"エンジンを更新しました。同じ設定で {0} をもう一度起動しますか?",
"Die Engine ist aktualisiert. {0} mit denselben Einstellungen wieder starten?",
"El motor está actualizado. ¿Iniciar {0} de nuevo con los mismos ajustes?",
"Движок обновлён. Запустить {0} снова с теми же настройками?",
"O motor está atualizado. Iniciar {0} de novo com as mesmas definições?",
"엔진이 업데이트되었습니다. 같은 설정으로 {0}을(를) 다시 시작할까요?",
"Le moteur est à jour. Redémarrer {0} avec les mêmes réglages ?")
add("The update didn't finish. Your old engine is unchanged and still works. Start {0} again on it?",
add("Starting {0}. Loading can take a few minutes.",
"正在启动 {0}。加载可能需要几分钟。",
"{0} を起動しています。読み込みには数分かかることがあります。",
"{0} wird gestartet. Das Laden kann einige Minuten dauern.",
"Iniciando {0}. La carga puede tardar unos minutos.",
"Запускаем {0}. Загрузка может занять несколько минут.",
"A iniciar {0}. O carregamento pode demorar alguns minutos.",
"{0}을(를) 시작하는 중입니다. 불러오는 데 몇 분이 걸릴 수 있습니다.",
"Démarrage de {0}. Le chargement peut prendre quelques minutes.")
add("Starting {0} again","正在再次启动 {0}","{0} をもう一度起動しています","{0} wird wieder gestartet","Iniciando {0} de nuevo","Снова запускаем {0}","A iniciar {0} de novo","{0}을(를) 다시 시작하는 중","Redémarrage de {0}")
add("The model did not start again","模型没有重新启动","モデルを再起動できませんでした","Das Modell wurde nicht wieder gestartet","El modelo no se inició de nuevo","Модель не запустилась снова","O modelo não voltou a iniciar","모델이 다시 시작되지 않았습니다","Le modèle n'a pas redémarré")
add("You can load it again from Models.","你可以在“模型”中再次加载它。","「モデル」からもう一度読み込めます。","Sie können es unter „Modelle“ erneut laden.","Puedes cargarlo de nuevo desde Modelos.","Вы можете загрузить её снова в разделе «Модели».","Pode carregá-lo de novo em Modelos.","모델 화면에서 다시 불러올 수 있습니다.","Vous pouvez le recharger depuis Modèles.")
add("A model that was not started from this app is running, so nothing was changed. Stop it, then update the engine again.",
"Ein Modell, das nicht aus dieser App gestartet wurde, läuft noch, daher wurde nichts geändert. Halten Sie es an und aktualisieren Sie die Engine dann erneut.",
"Hay un modelo en ejecución que no se inició desde esta aplicación, así que no se cambió nada. Deténlo y vuelve a actualizar el motor.",
"Работает модель, запущенная не из этого приложения, поэтому ничего не изменено. Остановите её и обновите движок снова.",
"Está em execução um modelo que não foi iniciado a partir desta aplicação, por isso nada foi alterado. Pare-o e atualize o motor de novo.",
"이 앱에서 시작하지 않은 모델이 실행 중이어서 아무것도 변경하지 않았습니다. 그 모델을 중지한 다음 엔진을 다시 업데이트하세요.",
"Un modèle qui n'a pas été démarré depuis cette application est en cours d'exécution, rien n'a donc été modifié. Arrêtez-le, puis mettez à nouveau le moteur à jour.")
// A saved API connection to a scripted model gives the app a model to talk to, so it stays on the chat screen (no model loaded would send it to Models).
constlongReply=Array.from({length:70},(_,i)=>`Paragraph ${i+1}. This is part of a long answer from the assistant that goes on for many screens before it asks to change a file in the project.`).join('\n\n');
constaskTool=(id,state='proposed',file='notes.txt')=>({id,name:'write',args:{file_path:file},preview:`write ${file}\n\na\nb\nc`,effect:'Creates a file.',scope:'write',verb:'Create a file',target:file,state,ask:{id:'create-file',params:[file],why:'replaces-if-exists'},live:false});
constplainTool=(id,state='proposed')=>({id,name:'bash',args:{},preview:'npm install left-pad',effect:'Runs a command on your computer.',scope:'shell',verb:'Run a command',target:'npm install left-pad',state});
check(awaitpage.locator('.askcard').count()===1,`${tag}: one Approve/Deny card on the page`);
check(awaitpage.getByRole('button',{name:'Approve'}).count()===1&&awaitpage.getByRole('button',{name:'Deny'}).count()===1,`${tag}: one Approve and one Deny`);
check(awaitinView('.approvaldock'),`${tag}: the card is inside the window at the end of the reply`);
constsessions=[chat('s1','Copy',[user('u1','Show me a command'),reply('a1',codeReply),user('u2','Thanks'),reply('a2','You are welcome. Second reply.')])];
constc=page.getByRole('button',{name:'Copy API base URL'});
awaitc.waitFor({timeout:15000});
awaitc.click();
awaitpage.waitForTimeout(250);
check(awaitc.evaluate(el=>el.hasAttribute('data-copied')&&(el.querySelector('use')?.getAttribute('href')??'').endsWith('i-check')),`${scheme}: Serving, Copy API base URL turns into a check mark`);
check(/^http:\/\/127\.0\.0\.1:\d+\/v1$/.test(awaitpage.evaluate(()=>navigator.clipboard.readText())),`${scheme}: Serving, the address is on the clipboard`);
j.jobs=[...(j.jobs??[]),{id:'job-x',kind:'download',state:'running',startedAt:newDate().toISOString(),summary:'Downloading the model',error:null,result:null,progress:{text:'Downloading',percent:12}}];
check(/still checking this model for saved tunings/.test(why),`${tag}: loading: one sentence says why`,why);
check(awaitpage.locator('.tune-why').count()===1,`${tag}: loading: only one such sentence`);
check(!/No saved tuning for these choices yet/.test(awaitpage.getByRole('dialog').innerText()),`${tag}: loading: it does not claim there is no saved tuning`);
check(awaitseen(page,w,h),`${tag}: loading: the sentence is on screen next to the button`);
awaitshot(page,`4804-${tag}-loading`);
awaitpage.context().close();
// 2. Reading them failed.
page=awaitopenDialog(scheme,w,h,'fail');
awaitpage.waitForTimeout(1200);
check(awaitstart(page).isDisabled(),`${tag}: unreadable: Start tuning is greyed out`);
constsessions=()=>[chat('s1','Links',[user('u1','Show links'),reply('a1',`Read [the docs](https://example.com/docs) or open [the logo](${base}/icons/sprite.svg).`)])];
for(constschemeof['light','dark']){
// A browser tab: the browser opens the address in a new tab; a file stays in the side panel.
check(JSON.stringify(opened)===JSON.stringify([['https://example.com/docs','_blank','noopener,noreferrer']]),`${scheme}: a browser tab: a plain click on a web address opens it in a new tab`,JSON.stringify(opened));
check(awaitpage.locator('.artifact-panel').count()===0,`${scheme}: a browser tab: and no side panel opens`);
check(JSON.stringify(posts)===JSON.stringify([{action:'open',url:'https://example.com/docs'}]),`${scheme}: the app window: the host is asked to open it in the default browser`,JSON.stringify(posts));
check((awaitpage.evaluate(()=>window.__opened)).length===0&&awaitpage.locator('.artifact-panel').count()===0,`${scheme}: the app window: nothing else happens`);
check(page.problems.length===0,`${scheme}: no page error`,page.problems.join('; '));
j.jobs=[...(j.jobs??[]),{id:'job-x',kind:'install',model:'iq2_xs',state:'running',startedAt:started,summary:'Preparing',error:null,result:{install:{step:'6/7',label:'Preparing the model data for the engine (one time)',parts:[{label:'Model files',done_gb:0.6,total_gb:0.6,state:'done'}],download:{file:'model files',done_gb:0.6,total_gb:0.6,remaining_gb:0,percent:99}}},progress:null}];
returnroute.fulfill({response:r,json:j});
});
awaitpage.reload();
constcard=page.locator('.native-progress');
awaitcard.waitFor({timeout:20000});
awaitpage.waitForTimeout(1500);
check(awaitcard.locator('.spinner').count()===1,`${tag}: a spinner shows that it is working`);
check(/^Still working for 3m \d+s\. You don't need to do anything\.$/.test(a1),`${tag}: it says how long and that nothing is needed`,a1);
awaitpage.waitForTimeout(2200);
consta2=awaitalive();
check(a1!==a2,`${tag}: the time keeps counting`,`${a1} -> ${a2}`);
check(/The buttons below wait until this finishes/.test(awaitpage.locator('.native-wait-note').innerText()),`${tag}: a sentence says why the buttons are greyed out`);
check(/API base URL on this computer/.test(text)&&text.includes('http://127.0.0.1:8079/v1'),`${tag}: sharing on: the app's own address is shown`);
check(/Address for other devices/.test(text)&&text.includes('http://100.76.61.73:8080/v1'),`${tag}: sharing on: and the address for other devices`);
check(awaitpage.locator('.serving-two-addresses').count()===1&&/its own port/.test(awaitpage.locator('.serving-two-addresses').innerText()),`${tag}: sharing on: one sentence says why they differ`);
constt=page.getByRole('button',{name:'Copy local network URL'});
awaitt.click();awaitpage.waitForTimeout(250);
check((awaitpage.evaluate(()=>navigator.clipboard.readText()))==='http://100.76.61.73:8080/v1',`${tag}: sharing on: the copy button gives the network address`);
check((awaitpage.evaluate(()=>document.documentElement.scrollWidth))<=w,`${tag}: sharing on: no sideways scroll`);
check(!/Address for other devices/.test(text)&&awaitpage.locator('.serving-two-addresses').count()===0,`${tag}: sharing off: one address, no extra line`);
}
check(page.problems.length===0,`${tag}: no page error`,page.problems.join('; '));
consttext='用中文写一段九十字左右的段落,说明本地模型为什么适合处理私人文件,并且不需要把任何内容上传到网络上去,这样可以让人放心。\n\n- 第一条要点同样写得比较长,用来观察列表项在气泡里到底会排到多宽才换行,看看它是否与上面的段落一样。\n- 第二条要点也一样长,看看两条要点是不是排得整齐,不会比段落更宽也不会更窄。\n\nAnd an English paragraph that is long enough to wrap onto at least two lines so that the measure of the prose can be read off it by the test.\n\n1. A numbered point in English that is also long enough to wrap onto a second line in the same bubble as the paragraph above it.\n2. Another numbered point.\n\n| Name | Value |\n| --- | --- |\n| a long cell that is wide | another long cell that is wide too |\n\n```bash\nnpm install a-very-long-package-name another-very-long-package-name yet-another-long-package-name --save-dev --legacy-peer-deps\n```';
check(!brand||box.x>=brand.x+brand.width-1||box.x+box.width<=brand.x+1||box.y>=brand.y+brand.height||box.y+box.height<=brand.y,`${tag}: the choice does not cover the logo`,JSON.stringify({brand,box}));
awaitshot(page,`3723-${tag}-first-zh`);
// Choosing German changes the screen at once, and it is kept.
check(/[A-Za-zäöü]/.test(awaitpage.locator('.guided-onboarding h1').first().innerText())&&!/[\u4e00-\u9fff]/.test(awaitpage.locator('.guided-onboarding h1').first().innerText()),`${tag}: choosing Deutsch changes the first screen`);
j.jobs=[...(j.jobs??[]),{id:'job-f',kind:'load',model:'iq2_xs',state:'error',startedAt:newDate().toISOString(),summary:'Load failed',error:"server_exited (exit code 1): EngineDied: the engine exited before it was ready | the engine process ended: exit code 1: \nthe engine log's last lines:\n strata generate: expert cache auto: 1.92 GiB free, 2048 MiB reserved -> 0 slots\n strata generate: no VRAM is left for the expert cache: it needs at least 332 slots (737 MiB), about 1000 MiB more than this card has free.",result:null,progress:null}];
check(/Q4 uses about half the memory/.test(awaithint.innerText()),`${tag}: Context memory: the line follows the choice`);
awaithint.scrollIntoViewIfNeeded();
awaitshot(page,`3723-${tag}-kv`);
constnotice=page.locator('.error-notice');
awaitnotice.first().waitFor({timeout:10000});
constsaid=awaitnotice.first().innerText();
check(/did not fit in your graphics card.s memory/.test(said)&&/Context memory to Q4/.test(said),`${tag}: a start that ran out of graphics memory says so in plain words`,said.replace(/\n+/g,' | ').slice(0,200));
awaitnotice.first().scrollIntoViewIfNeeded();
awaitshot(page,`3723-${tag}-vram`);
check((awaitpage.evaluate(()=>document.documentElement.scrollWidth))<=w,`${tag}: no sideways scroll`);
check(page.problems.length===0,`${tag}: no page error`,page.problems.join('; '));
awaitp.route('**/api/chat/helper',asyncr=>{posts.push(r.request().postDataJSON());if(refuse)returnr.fulfill({status:409,json:{error:'This helper cannot take a message any more. Ask the assistant in the chat to start a new one.'}});returnr.fulfill({json:{ok:true,messageId:'m1'}});});
check(JSON.stringify(posts[0])===JSON.stringify({sessionId:'s1',helperId:'child-1',message:'use ISO dates in the file you just wrote',delivery:'queue'}),`${tag}: a finished helper gets the message as a new turn (queue)`,JSON.stringify(posts[0]));
check(/Sent to Check the tests\./.test(awaitrows.nth(0).innerText())&&awaitinput.inputValue()==='',`${tag}: the row says it was sent and the box is empty again`);
// A second helper (a failed one is listed too).
awaitrows.nth(1).getByRole('button',{name:'Message this helper'}).click();
awaitrows.nth(1).getByRole('textbox').fill('also add the date');
awaitpage.locator('textarea').first().fill('SCRIPT:echo the second answer');
check(/Enter sends it when the answer is done/.test(awaitpage.locator('.composer__busy-hint').innerText()),`${tag}: Send after: the hint says what Enter will do`);
check(/Waiting to send/.test(awaitpage.locator('.parked').innerText())&&/second answer/.test(awaitpage.locator('.parked').innerText()),`${tag}: Send after: the waiting message is shown`);
awaitpage.locator('textarea').first().fill('SCRIPT:echo interrupted and answered');
check(/Enter stops the answer here and sends it now/.test(awaitpage.locator('.composer__busy-hint').innerText()),`${tag}: Interrupt: the hint says what Enter will do`);
constlong='First point: local models keep your files on your own computer. Second point: nothing has to be uploaded to a service. Third point: you can work without a network connection.';
constpage=awaitopen(scheme,w,h,[chat('s1','Quote',[user('u1','Why run a model locally?'),reply('a1',long),user('u2','Thanks, one more question'),reply('a2','Ask away.')])]);
check(/Quoting the assistant/.test(awaitpage.locator('.quote').first().innerText())&&/First point/.test(awaitpage.locator('.quote').first().innerText()),`${tag}: Quote in the message menu puts the turn above the box`);
// Just a passage: select the second sentence and quote it.
check(!!sbox&&sbox.x>=0&&sbox.y>=0&&sbox.x+sbox.width<=w,`${tag}: a Quote button appears by the selected words, on screen`,JSON.stringify(sbox));
awaitpage.locator('.quote-selection').click();
check(awaitpage.locator('.quote').count()===2&&/^Second point: nothing has to be uploaded to a service\.$/.test((awaitpage.locator('.quote .quote__text').nth(1).innerText()).trim()),`${tag}: the second quote is just the selected passage, listed after the first`);
check(awaitpage.locator('.quote-selection').count()===0,`${tag}: the button goes away`);
awaitshot(page,`4785-${tag}-quotes`);
// Remove one, quote it again, jump to the message.
awaitpage.locator('.quote').first().getByRole('button',{name:'Remove this quote'}).click();
check(awaitpage.locator('.quote').count()===1&&/Second point/.test(awaitpage.locator('.quote').first().innerText()),`${tag}: one quote can be removed without touching the others`);
check(/Second point: nothing has to be uploaded/.test(sent)&&/SCRIPT:echo thanks for the quote/.test(sent)&&!/Quoting:/.test(sent)&&!/「/.test(sent),`${tag}: the sent message shows the quote apart from the reply, without the markup`,sent.replace(/\n+/g,' | ').slice(0,160));
constgot=worker.requests.at(-1)?.raw??'';
check(got.includes('Quoting:\\n「Second point: nothing has to be uploaded to a service.」\\n→ Reply: SCRIPT:echo thanks for the quote'),`${tag}: the model receives the quote in its fixed form`,got.slice(-220));
check(awaitpage.locator('.quote').count()===0,`${tag}: the quotes are cleared after sending`);
awaitpage.locator('.sent-quote').first().click();
check(awaitpage.locator('.sent-quotes[data-open]').count()===1,`${tag}: a click opens the rest of a sent quote`);
awaitshot(page,`4785-${tag}-sent`);
check((awaitpage.evaluate(()=>document.documentElement.scrollWidth))<=w,`${tag}: no sideways scroll`);
check(page.problems.length===0,`${tag}: no page error`,page.problems.join('; '));
awaitpage.locator('textarea').first().fill('Build me a page');
awaitpage.keyboard.press('Enter');
constcard=page.locator('.askq');
awaitcard.waitFor({timeout:15000});
consttext=awaitcard.innerText();
check(/Which colour should the page be\?/.test(text)&&/Which extras do you want\?/.test(text)&&/Warm and strong/.test(text)&&/It sets the whole theme/.test(text),`${tag}: the question, its detail and the option descriptions are shown`);
check(awaitpage.locator('.askcard').count()===1&&awaitpage.locator('.approvaldock').count()===1,`${tag}: it is one card in the dock above the box`);
check(awaitpage.getByRole('radio',{name:/Red/}).getAttribute('aria-checked')==='false'&&awaitpage.getByRole('radio',{name:/Blue/}).getAttribute('aria-checked')==='true',`${tag}: a single-select question takes one choice`);
check(sb.y>=0&&sb.y+sb.height<=h&&/Send answer/.test(top),`${tag}: the answer buttons are in view and reachable without scrolling the card`,JSON.stringify(sb));
awaitshot(page,`4244-${tag}`);
check((awaitpage.evaluate(()=>document.documentElement.scrollWidth))<=w,`${tag}: no sideways scroll`);
check(JSON.stringify(got.at(-1))===JSON.stringify({sessionId:'s1',callId:'call-ask-1',answers:[{id:'q1',selected:['Blue']},{id:'q2',selected:['Dark mode','Animations']}]}),`${tag}: the answer goes to the host as chosen`,JSON.stringify(got.at(-1)));
check(awaitpage.locator('.askq').count()===0,`${tag}: the card goes away, and the turn ends`);
check(JSON.stringify(own?.answers)===JSON.stringify([{id:'q1',selected:[],custom:'Green, like the logo'},{id:'q2',selected:['Animations']}]),`${tag}: the person's own words are sent as such`,JSON.stringify(own));
/* ------------------------------------------------------------------------------------------------------------------- #3818 (a probe, no fix) */
cases.select=async()=>{
constparas=Array.from({length:40},(_,i)=>`Paragraph ${i+1}. This is a sentence of a long answer that goes on for a while so that the reply overflows the window.`).join('\n\n');
check(got.includes(first)&&got.includes(last)&&got.split('\n').filter(Boolean).length>=visible.length,`finished reply: a drag across ${visible.length} paragraphs selects all of them`,`${got.length} characters`);
awaitpage.context().close();
// While a reply streams: select a passage of the older reply, and see whether the selection and the view stay.
constpage2=awaitopen('light',1280,860,[chat('s1','Select',[user('u1','Write a lot'),reply('a1',paras)])]);
INFO after: ${JSON.stringify(sel1.slice(0,40))} ... ${JSON.stringify(sel1.slice(-40))}`);
check(sel1===sel0,'streaming: a selection in the older reply stays as it was while the new reply is written (a selection inside the paragraph that is being written does not: React replaces its text node on every token)',`${sel0.length} -> ${sel1.length}`);
check(Math.abs(top1-top0)<40,'streaming: the view is not pulled away from the selection',`${Math.round(top0)} -> ${Math.round(top1)}`);
}elseconsole.log('INFO streaming: the older reply was not on screen, nothing to select');
awaitpage2.context().close();
};
/* ------------------------------------------------------------------------------------------------------------------ run */
@@ -101,7 +101,7 @@ records the patch identity, the dependency version and the before/after SHA-256.
Source and already-installed Harness copies are not changed.
- A local route is reasoning-capable (`reasoning: true`) with `compat.thinkingFormat: 'chat-template'`: the request carries `chat_template_kwargs {enable_thinking, reasoning_effort}`, which the Strata server reads (a top-level `enable_thinking` is ignored by it, which is why pi-ai's `qwen` format left the model thinking). The chat's Reasoning menu travels as `thinking` in the stream body; the bridge sends it with `session/selectModel` (`reasoningEffort`) whenever the model or the level changed; a level a route does not have is dropped (never an error). API routes keep the provider default (the menu is disabled for them in the UI).
-`localOutputBudget(context)` is 30 percent of the window, at least 4096 and at most 32768 (9,830 tokens at 32K, 19,660 at 64K). The Harness's automatic compaction reads it as the room a reply reserves. pi-ai sends the window minus the prompt. App-loaded models get `fit_max_tokens: true` so the engine clamps instead of answering 400. Sampling is not set here: the Harness sends none, so the server's own defaults apply.
-`localOutputBudget(context)` is 30 percent of the window, at least 4096 and at most 32768 (9,830 tokens at 32K, 19,660 at 64K). The Harness's automatic compaction reads it as the room a reply reserves (the request header), and it is NOT the ceiling of a request: `plugins/strata-context.mjs` raises what the adapter sends to half the window, at most 64K tokens (`reply-cut.mjs``replyCap`; pi-ai clamps it to the window minus the prompt), because a thinking model that writes a whole file in one call was cut inside the call at 19,660 and lost it (AGENT-LOST). A reply that is cut anyway is announced in the chat, the agent is asked to go on in smaller pieces (twice at most), and the file being written is streamed to the page as `agent-tool-draft` frames (name, size, lines, tail of the text). App-loaded models get `fit_max_tokens: true` so the engine clamps instead of answering 400. Sampling is not set here: the Harness sends none, so the server's own defaults apply.
constpending=newMap();// `${sessionId}:${callId}` -> { eventId, sessionId, callId, at }
constquestions=newMap();// `${sessionId}:${callId}` -> the agent's own question waiting for the person: { eventId, sessionId, callId, items, at, clientId } (#4244)
// The agent of a chat that is running in front of the person asks the person: the question goes to the page (the question card above the message box) and the
// Harness waits for the answer. Anything else (a helper's question, a chat that is stopping, a request with nothing to show) is declared
// unanswerable, which makes the tool return an error the model reads and carries on from, instead of a turn that waits forever.
constr=value.request??{};
constchatId=chatOfSession(value.agentId);
constturn=chatId?chats.get(chatId)?.turn:null;
constitems=parseQuestions(r.questions);
// A goal or a plan step runs without anyone there: a question would stall it, so it is declined at once (the model reads "could not be answered" and goes on).
// A reply the length limit cut (AGENT-LOST): the agent lost what it was writing. Say so in the chat and ask it to carry on in smaller pieces, twice at most.
if(!clean)thrownewAgentUnavailable('Type a few words for the helper.','helper-empty');
if(!/^[\w.:@-]{1,160}$/.test(String(helperId??'')))thrownewAgentUnavailable('That helper is not known.','helper-unknown');
constparent=sessionMap[chatId]?.sessionId;
if(!parent||!rt?.hp.running)thrownewAgentUnavailable('The assistant is not running in this chat, so it cannot pass a message on. Send a message in the chat first.','helper-idle');
log(`agent · message for a helper refused: ${String(e?.message??e).slice(0,200)}`);
thrownewAgentUnavailable(/continuable|one-shot|not.*(found|live|open)|unavailable|closed|ended/i.test(String(e?.message??''))?'This helper cannot take a message any more. Ask the assistant in the chat to start a new one.':'The message could not be delivered right now. Try again in a moment.','helper-refused');
}
}
// -- messages typed while a goal runs ----------------------------------------------------------------------
/** Queue a message for the running goal. It is delivered between steps, or handed back to the page when the goal ends or pauses. */
return/request exceeds the context window|leaves no room to answer|context[_ ]length|context window|too many tokens|maximum context/i.test(text);
}
/** The Harness asks for "concise English engineering prose", so a Chinese or Russian chat came back summarised in English (#4750). One sentence is added to the
* summary request's last message: write it in the language the person used. Paths, commands and identifiers stay exact. Messages are not changed in place. */
exportconstSUMMARY_LANGUAGE_RULE='Write the summary in the language the person used in this conversation (English only when they wrote English). The instruction above is in English; that does not change the language of the summary. Keep file paths, commands, error strings, identifiers and numbers exactly as they appear.';
@@ -139,7 +180,14 @@ export class DeliverableTracker {
// modified during the turn; or created during it (a copy that kept its old modified time), which is only certain when the first look saw the whole folder
returnthis.#card(callId,{state:'denied',result:outcome==='rejected'?'Denied. Nothing ran.':outcome==='cancelled'?'Cancelled. Nothing ran.':'No one could answer this request, so nothing ran.'});
}
/** A reply the model could not finish (the turn ended on the length limit): what it was writing, so nothing is lost without a word. */
/** Plugins removed from inside the `delegation` group (the group stays: helper agents are a feature). */
exportconstREMOVED_DELEGATION_IDS=[];
@@ -48,6 +48,24 @@ export const AGENT_PROMPT = [
'Tokens prefixed with @ are files or folders the person pointed at. A failed or refused action changed nothing; say so, and do not claim work you did not do.',
].join('\n\n');
/**
* The one sentence that says which computer this is. The Harness's own runtime context is off (see `includeRuntimeContext` below), so without it
* a model on Windows spent its first steps on `ls`, `uname` and `wc` before trying PowerShell commands (and some ran out of steps first).
*/
exportconstOS_LINES={
win32:'This computer runs Windows and the shell tool is PowerShell: uname, wc and grep do not exist here; use Get-ChildItem, Get-Content, Select-String and Measure-Object. There is no need to check which system this is.',
darwin:'This computer runs macOS and the shell tool is a Unix shell. There is no need to check which system this is.',
linux:'This computer runs Linux and the shell tool is a Unix shell. There is no need to check which system this is.',
};
/** AGENT_PROMPT with the sentence for `platform` after its first line. */
* A reply the model could not finish (AGENT-LOST, 11-01): pure helpers, no I/O.
*
* A model reply has a ceiling (the "output budget"). A thinking model that then writes a whole file in one tool call can spend all of it
* before the call is closed. The Harness drops a tool call that was not closed (its arguments are half a JSON text), the turn ends with
* the reason `max-tokens`, and until now the person saw nothing: no file, no card, no sentence. Here:
*
* - `replyCap(window)` how much one reply may produce (the request's ceiling), much larger than the budget the Harness reserves
* for compaction (profile.mjs `localOutputBudget`): the Harness still clamps it to what the window has left.
* - `ToolDraft` what a tool call that is being written looks like so far: its name, the file it is for, a size, a tail of the text.
* - `cutNotice(...)` the plain sentence for the chat, and `cutNudge(...)` the message that lets the agent carry on in smaller pieces.
*/
/** Half the window, at most 64K tokens, and never less than the budget compaction reserves. The Harness clamps it to the room left (window minus prompt). */
/** The sentence for the person, with the catalog slots it needs: { text, key, values }. `again` is true when the agent was already asked to carry on. */
constkey='That reply reached its length limit{0} while writing {1} ({2}), so that write did not happen. I asked the assistant to look at what is on disk and write the rest in smaller parts.';
constkey='The reply reached its length limit{0} while writing {1} ({2}), so that write did not happen. Say “continue” and it will look at what is on disk and go on in smaller parts.';
return'Your last reply was cut off by the output limit before it was finished. Carry on from where it stopped without repeating what you already wrote, and keep your thinking short.';
}
constfile=call.target||'the file';
return`Your last reply was cut off by the output limit${tokens>0?` (${Number(tokens).toLocaleString('en-US')} tokens)`:''} while you were writing ${file} in one tool call, so that call did not run and nothing was changed by it. First look at what is on disk now (list the folder, read the end of the file). Do not delete or start over a file that is already complete. If it is not complete, write what is missing in several smaller parts (about 150 lines or fewer each) with edits or appends, and read the end of the file back when you are done. Keep your thinking short.`;
{id:'search',area:'web',name:'Search the web',about:'Lets the assistant search the web when it needs fresh information.',asks:true,web:'search'},
{id:'helpers',area:'together',name:'Helper assistants',about:'Lets the assistant start helpers that work on parts of a task at the same time.',asks:false,plugins:['delegation']},
{id:'goals',area:'together',name:'Goals',about:'Lets you give a goal with /goal. The assistant keeps working until it is done.',asks:false,plugins:['command-goal','tool-goal']},
{id:'questions',area:'together',name:'Questions for you',about:'Lets the assistant stop and ask you a question, with answers to pick from, when it needs your decision.',asks:false,plugins:['tool-ask-user']},
{id:'todo',area:'together',name:'To-do list',about:'Lets the assistant keep a checklist while it works on a big task.',asks:false,plugins:['tool-todo']},
{id:'skills',area:'together',name:'Skills',about:'Lets the assistant use your saved skills when they fit the task.',asks:false,plugins:['skill-filesystem','tool-skill']},
{id:'present',area:'together',name:'Show results',about:'Lets the assistant show a page or file in the preview next to the chat.',asks:false,plugins:['present']},
if(!['open','reveal'].includes(body.action))thrownewError('Choose Open or Show in file explorer.');
if(typeofbody.url==='string'){
// #4721: a web address goes to the person's default browser (the app window drops window.open: WebView2 has no new-window handler). http and https only, no shell.
if(body.action!=='open')thrownewError('A web address can only be opened.');
letaddress;try{address=newURL(body.url);}catch{thrownewError('This is not a web address.');}
if(!['http:','https:'].includes(address.protocol)||body.url.length>4096)thrownewError('Only http and https addresses can be opened.');
catch(e){if(attempt>=5||!['EPERM','EBUSY','EACCES'].includes(e?.code))throwe;awaitnewPromise(r=>setTimeout(r,40*(attempt+1)));}// an antivirus scan holds the file for a moment on Windows
}
}catch(e){
try{awaitio.unlink(temp);}catch{/* nothing to remove */}
* Save one chat. `sessionText` is the chat's JSON as the page wrote it. Resolves { id, rev, bytes, conflict }.
* Rejects with a ChatStoreError (status 400 for a chat that is not a chat, 507 disk full, 403 refused, 500 other): nothing was changed.
*/
save(id,sessionText,{baseRev=null}={}){
if(!validChatId(id))returnPromise.reject(newChatStoreError('That is not a chat id.',400,'bad-id'));
letsession;
try{session=JSON.parse(sessionText);}catch{returnPromise.reject(newChatStoreError('The chat must be JSON.',400,'bad-json'));}
if(!session||typeofsession!=='object'||Array.isArray(session)||session.id!==id)returnPromise.reject(newChatStoreError('The chat does not match its id.',400,'bad-chat'));
* Small Control programs that are a module of strata_control and not a `strata` command. They run on Control's interpreter with Control's folder first on the
* path (the folder is the working directory), so they work whatever the interpreter does with the script folder.
/** Run one CLI command of Control (or, with `boot`, one of its modules). Resolves { code, json, text, stderr }; rejects only if it cannot start or times out. */
throwfailure(result,'The installed engines could not be read.');
},
/** Switch to an installed engine (`strata components use`): nothing is downloaded. `root` is only for a DeepSeek engine that lives in more than one place. */
if(result.json)returnresult.json;// exit 5 still carries the report and the notice
@@ -262,21 +288,45 @@ export function createControl({ control = findControl(), env = process.env, run
throwObject.assign(failure(result,'The partial copies could not be removed.'),{code:result.json?.error?.code??'leave_failed',details:result.json?.error?.details??''});
},
/**
* The DFlash2 drafter for a model's run config and the drafting choice (`strata drafting plan`): { needed, gb, line, note, drafter }. Control decides
* (the table of strata_control/drafting.py); the app never reads it. Nothing is downloaded.
* The drafter for a model's run config and the drafting choice (`strata drafting plan`): { needed, gb, line, note, drafter, split, mtp_needed, mtp_gb }. Control decides
* (the table of strata_control/drafting.py and the all-cards split engine's rule, strata_control/split_engine.py: the same answer as the Terminal's); the app never reads
* it. Nothing is downloaded. `noSplit` asks as if the split engine will not take the start (only after the draft layer for the split could not be downloaded).
throwfailure(result,'The drafter could not be checked.');
},
/** Fetch and pack the drafter when the choice would use it and it is not here (`strata drafting fetch --if-needed`); `onEvent` gets {step, done, total}. */
throwObject.assign(newControlError(result.json?.error?.message??(result.stderr.split(/\r?\n/).filter(Boolean).pop()||'The DFlash2 drafter could not be downloaded.'),{exitCode:result.code,output:result.json}),{code:'drafting_failed'});
},
/**
* The all-cards split engine for the run config the app is about to start (`strata split-engine apply`, the step `strata launch` does itself): Control checks the rule, the
* engine package and the draft layer on this computer and writes the split engine's run config to `outPath` when the answer is yes. Resolves { split, line, warn, reasons, cards, written };
* `configPath` is never changed, nothing is downloaded.
throwfailure(result,'The all-cards split engine could not be checked.');
},
/**
* The picture files of an installed model that has none (strata_control/pictures.py): found or downloaded once (about 0.9 GB, shared), and the run
* config's `vision` section worked out for this engine. Nothing else of the model is touched and nothing is written to its run config: the caller does.
* Resolves { mmproj, vision, fetched, note, data_dir }; a failure is a ControlError whose `code` is offline | disk | failed | checksum | unsupported | no_reader.
* `onEvent` gets {step, done, total}. This is an explicit step that may reach the network (`acquire`), only for the Load the person pressed.
throwObject.assign(newControlError(result.json?.error?.message??(result.stderr.split(/\r?\n/).filter(Boolean).pop()||'The picture files could not be downloaded.'),{exitCode:result.code,output:result.json}),{code:result.json?.error?.code??'failed'});
@@ -43,12 +43,15 @@ export function createEngineStep({ control, signal = null, onChange = () => {} }
return{...part};
}
constversion=engine.offered?.version??null;
set({state:'downloading',version,name:`Engine ${version??''}`.trim(),total_gb:round((engine.download_bytes??0)/GB),message:'Downloading the engine for this PC…'});
// An engine already works here: say what is about to change and that it can be undone (#4823), not only "downloading".
constbefore=engine.installed?.version??null;
constchange=before&&version&&before!==version?`Updating the engine from ${before} to ${version}. The engine you have now is kept, and Settings can go back to it.`:null;
set({state:'downloading',version,name:`Engine ${version??''}`.trim(),total_gb:round((engine.download_bytes??0)/GB),message:change??'Downloading the engine for this PC…'});
constresult=awaitcontrol.update({
engine:true,control:false,signal,
onEvent:event=>{
if(event.event==='progress'){
set({state:'downloading',percent:event.percent??0,done_gb:round((event.done??0)/GB),total_gb:round((event.total??0)/GB),message:`Downloading the engine for this PC: ${event.percent??0}%`});
set({state:'downloading',percent:event.percent??0,done_gb:round((event.done??0)/GB),total_gb:round((event.total??0)/GB),message:`${change?`Updating the engine from ${before} to ${version}`:'Downloading the engine for this PC'}: ${event.percent??0}%`});
set({state:'done',percent:100,done_gb:part.total_gb,version:done?.installed_after?.version??version,message:'The engine for this PC is ready.'});
set({state:'done',percent:100,done_gb:part.total_gb,version:done?.installed_after?.version??version,message:change?`The engine was updated from ${before} to ${version}. Settings can go back to the one you had.`:'The engine for this PC is ready.'});
}catch(error){
constplain=plainUpdateError(error,'engine');
note=`Your model is ready. The engine for this PC couldn't be downloaded. ${plain.next}`;
if(done.noDialog)return{unavailable:true,failed:true,reason:'The folder window did not open. Type the folder path instead.'};
if(/^NODIALOG\s*$/m.test(String(done.stdout??'')))return{unavailable:true,failed:true,reason:'The folder window could not open. Type the folder path instead.'};
// The share and tunnel listeners take a free port (the next ones above the host's own) instead of a fixed PORT+1 / PORT+2 that Docker, WSL or Hyper-V may hold or reserve.
// The probe listens on the loopback address only: a probe on 0.0.0.0 made Windows Defender Firewall ask "Allow Node.js to communicate on these networks?" on the
// first start of every new install path (QA 0213). Reserved or taken ports are caught on any address; the share listener itself binds 0.0.0.0 only when the person turns sharing on.
if(path==='/api/native/operations'&&method==='POST'){if(storageChanging)returnerror(res,409,'Wait for the model folder check to finish.');constbody=awaitreadBody(req);returnjson(res,202,native.operation(body.kind,body.args));}
try{letat=componentsDirectory();while(at&&!existsSync(at)&&dirname(at)!==at)at=dirname(at);constf=statfsSync(at);info.freeBytes=Number(f.bavail)*Number(f.bsize);}catch{/* unknown: no warning */}
if(typeofb.sessionId!=='string'||!['manual','plan','bypass'].includes(b.mode))returnerror(res,400,'sessionId and mode (manual, plan or bypass) are required');
functionfullPath(input){if(typeofinput!=='string'||!input.trim()||input.includes('\0'))thrownewError('Choose a model file or enter its full path.');constpath=input.trim().replace(/^~(?=$|[\\/])/,homedir());if(!isAbsolute(path))thrownewError('Enter a full path.');try{returnrealpathSync(path);}catch{thrownewError('This file or folder is unavailable. Check its location and permissions.');}}
/** The drive-temperature reader is not started in the first minute of the host: the app's own start-up traffic is not a person looking at the Monitor (#4661). */
exportconstDRIVES_AFTER_MS=60000;
/** Only the Monitor screen's own ask (GET /api/monitor?drives=1) wants drive temperatures; the start-up fetch and the Models page do not show them. */
/* What the load says when the server had to start the normal layer split because the all-cards split engine would not come up (the engine's own reason stays in its log). */
constSPLIT_FELL_BACK='The all-cards split engine could not start, so the normal layer split runs.';
constsafeId=value=>typeofvalue==='string'&&/^[a-z0-9](?:[a-z0-9_-]|\.(?!\.)){0,79}$/i.test(value);/* a dot inside the name: deepseek-v4.1-flash-ist-das-2.00bpw */
/** The plain words for "no usable engine": the model still downloads, but loading needs the engine (owner decision 2026-10-18). */
exportconstENGINE_NOT_RECOGNISED='Strata didn’t recognise an engine for this computer. The model is downloaded, but it can’t run until the right engine is installed. Open Settings → Engine.';
exportconstENGINE_NOT_RECOGNISED='Strata didn’t recognise an engine for this computer. The model is downloaded, but it can’t run until the right engine is installed. Open Settings → About.';
/* QAGUI 0213: until Control has answered once, the state says it is still being asked (`checking`), not that nothing is installed. A failed read during Control's start is read again after a few seconds instead of standing for 45 s; after READ_TRIES failures the answer is final and the error shows. */
letsnapshot={available:false,repo:preferences.repo,summary:'Connect a local Strata installation.',catalog:null,installed:null,hardware:null,error:null};
constsavedJobs=readJson(join(directory,'native-jobs.json'),[]);/* An app update ends the app on purpose: the installer replaces it and starts it again. The job that asked for it was saved as running together with the app version it started from (appUpdateFrom); a start that finds a different version knows the update finished, instead of calling it an interrupted operation. */
@@ -63,17 +67,19 @@ export function createNativeControl({directory,requests=()=>[],changed=()=>{},sp
constupdateLog=line=>{try{console.log(`update · ${line}`);}catch{/* no console */}};
constNO_INSTALL='Installing, updating, tuning with calibration or rolling back model files and engines happens from the Terminal (strata install), never while the app runs.';
/* Does the engine in this release serve DFlash2? Control knows (strata_control/drafting.py); asked once, false until it answers. The Models screen offers the choice only when true. */
if(JSON.stringify(next)!==JSON.stringify(config)){try{writeModelConfig(path,next);}catch{/* read-only folder: the load reports it */}}}}
functioninstalledInfo(info){if(!info)returninfo;return{...info,models:(info.models??[]).map(entry=>{constcfg=/^strata-[a-z0-9_.-]+\.json$/i.test(entry.config??'')?readJson(entry.configPath??join(repo(),entry.config)):{};constat=cfg.args?.indexOf('--native')??-1,nativeFile=at>=0?cfg.args[at+1]:null;constdataFolder=typeofnativeFile==='string'&&basename(dirname(dirname(nativeFile)))==='models'?dirname(dirname(dirname(nativeFile))):info.data_folder;/* A model the person pointed at their own GGUF folder (not <data>/models/<tag>): remembered, so a later load runs setup with that folder again instead of downloading the files (#3199). */constownGgufDir=typeofnativeFile==='string'&&basename(dirname(dirname(nativeFile)))!=='models'&&existsSync(nativeFile)?dirname(nativeFile):undefined;return{...entry,ownGgufDir,contextMax:configContextLimit(cfg)??undefined,diskGb:entry.diskGb??entry.disk_gb,diskBytes:entry.diskBytes??entry.disk_bytes,sharedBytes:entry.sharedBytes??entry.shared_bytes,removableGb:entry.removableGb??entry.removable_gb,dataFolder,visionPrepared:!!cfg.vision,/* The picture files are prepared when the run config has a vision section: the Models screen then loads straight away instead of running setup again (it used to read 'off' for every model, which sent every load of a picture-capable model through "Apply settings and load", and setup writes the arguments, the draft layer and the reader back each time). */images:cfg.vision?'yes':'off',draftLayer:cfg.mtp!==false,drafting:cfg.drafting??'auto',/* Picture reading is ON unless the person turned it off: no saved choice and not loaded is undefined, not false (owner rule: pictures on by default). */visionEnabled:cfg.ui_vision_enabled??(()=>{construnning=[...instances.values()].find(i=>i.model===entry.model&&i.status==='ready');returnrunning?!!running.vision:undefined;})(),visionDevice:cfg.ui_vision_device,resourceProfile:cfg.ui_resource_profile,lowRamMode:cfg.args?.includes('--resident-experts')?'resident':cfg.args?.includes('--mmap-experts')?'mmap':'auto'};})};}
/* DeepSeek reads pictures with the engine's own tower, which is part of the pack (pack_gguf writes the tower's geometry into ds4.txt when the picture file was there): a pack with it is "prepared", a text-only pack (made with --vision off, as the app did before 0.2.9) is not, and the Models screen then offers the one missing shared file. null for every other model. */
/* DeepSeek's picture switch is a pair, --vision on|off, in the saved arguments: set it to the person's choice at every load (on only where the pack carries the tower). */
functioninstalledInfo(info){if(!info)returninfo;return{...info,models:(info.models??[]).map(entry=>{constcfg=/^strata-[a-z0-9_.-]+\.json$/i.test(entry.config??'')?readJson(entry.configPath??join(repo(),entry.config)):{};constat=cfg.args?.indexOf('--native')??-1,nativeFile=at>=0?cfg.args[at+1]:null;constdataFolder=typeofnativeFile==='string'&&basename(dirname(dirname(nativeFile)))==='models'?dirname(dirname(dirname(nativeFile))):info.data_folder;/* The folder this model's own run config keeps its prepared data in: the one that holds its packs/<tag> (or mtp/rt). It is the real data folder of the model, wherever the app's models folder is; a model in the person's own GGUF folder has its pack and draft layer here, not in the app's folder. A layout that only looks like <data>/models/<tag> says nothing: no pack, no folder. */constflagValue=flag=>{consti=cfg.args?.indexOf(flag)??-1;returni>=0&&typeofcfg.args[i+1]==='string'?cfg.args[i+1]:undefined;};constholder=(path,child,name)=>{if(!path||basename(dirname(path)).toLowerCase()!==child||(name&&basename(path).toLowerCase()!==name))returnundefined;returndirname(dirname(path));};construnDataFolder=holder(flagValue('--pack'),'packs')??holder(flagValue('--mtp'),'mtp','rt');/* A model the person pointed at their own GGUF folder (not <data>/models/<tag>): remembered, so a later load runs setup with that folder again instead of downloading the files (#3199). */constownGgufDir=typeofnativeFile==='string'&&basename(dirname(dirname(nativeFile)))!=='models'&&existsSync(nativeFile)?dirname(nativeFile):undefined;return{...entry,ownGgufDir,runDataFolder,contextMax:configContextLimit(cfg)??undefined,diskGb:entry.diskGb??entry.disk_gb,diskBytes:entry.diskBytes??entry.disk_bytes,sharedBytes:entry.sharedBytes??entry.shared_bytes,removableGb:entry.removableGb??entry.removable_gb,dataFolder,contextChosen:cfg.ui_context_chosen===true,visionPrepared:dsPictures(cfg)??(!!cfg.vision&&entry.picturesMissing!==true),/* The picture files are prepared when the run config has a vision section: the Models screen then loads straight away instead of running setup again (it used to read 'off' for every model, which sent every load of a picture-capable model through "Apply settings and load", and setup writes the arguments, the draft layer and the reader back each time). */images:(dsPictures(cfg)??(!!cfg.vision&&entry.picturesMissing!==true))?'yes':'off',draftLayer:cfg.mtp!==false,drafting:cfg.drafting??'auto',/* Picture reading is ON unless the person turned it off: no saved choice and not loaded is undefined, not false (owner rule: pictures on by default). */visionEnabled:cfg.ui_vision_enabled??(()=>{construnning=[...instances.values()].find(i=>i.model===entry.model&&i.status==='ready');returnrunning?!!running.vision:undefined;})(),visionDevice:cfg.ui_vision_device,resourceProfile:cfg.ui_resource_profile,lowRamMode:cfg.args?.includes('--resident-experts')?'resident':cfg.args?.includes('--mmap-experts')?'mmap':'auto'};})};}
/* The settings file Strata Control and the installer read (control-runtime.mjs sets STRATA_SETTINGS_FILE to it): the app's data folder is written there. */
constmodels=(list.models??[]).filter(m=>m.component_status!=='invalid'&&m.source_config).map(m=>({...(disk[m.id]??{}),model:m.id,config:basename(m.source_config),configPath:m.source_config,model_name:m.name,context:m.context_tokens??0,backend:m.backend,ready:(m.component_status==='present'||onlyPictures(m))&&!(disk[m.id]?.partsMissing),/* Only the picture file is gone (run config has the picture section, the file does not exist): the model is not "missing files". It loads, and the Load fetches the picture file again. */picturesMissing:m.component_status!=='present'&&onlyPictures(m),missing:missingNames(m.missing,disk[m.id]?.missingFiles),images:'off',port:undefined,last_used:null}));
constcores=os.cpus().length;
return{summary:engine.usable?`Strata Control is ready; engine ${engine.engine.version} (${engine.engine.platform}-${engine.engine.backend}).`:(engine.notice??'No engine for this PC was found.'),
if(refreshing)return!force&&snapshot.available?state():force?refreshing.then(()=>refresh(true)):refreshing;/* a forced read that meets one already running (started before an engine update finished) waits for it and reads again, so it never settles on the old answer */if(!force&&Date.now()-lastRead<45000)returnstate();
functionconfigure({repository}){if(packaged){if(instances.size||[...jobs.values()].some(j=>j.state==='running'))thrownewError('Unload models and finish operations before changing the installation.');constnext=realpathSync(String(repository));if(!statSync(next).isDirectory())thrownewError('Choose the folder that holds your Strata model configurations.');preferences={...preferences,installDir:next};packagedInstall=null;disconnect();writeFileSync(settingsPath,JSON.stringify(preferences,null,2),{mode:0o600});lastRead=0;returnrefresh(true);}if(instances.size||[...jobs.values()].some(j=>j.state==='running'))thrownewError('Unload models and finish operations before changing the installation.');constnext=realpathSync(String(repository));constold=preferences;preferences={...preferences,repo:next};try{repo();}catch(e){preferences=old;throwe;}disconnect();writeFileSync(settingsPath,JSON.stringify(preferences,null,2),{mode:0o600});lastRead=0;returnrefresh(true);}
functionsetDownloadDirectory(input,{insideJob=false}={}){if(!insideJob&&[...jobs.values()].some(j=>j.state==='running'))thrownewError('Finish the current operation before changing the folder.');if(typeofinput!=='string'||!input.trim()||/[\r\n\0]/.test(input))thrownewError('Choose a model folder.');letpath;try{path=realpathSync(input.trim());}catch{thrownewError('That folder does not exist. Check the path, or that the drive is connected.');}if(!statSync(path).isDirectory())thrownewError('Choose a folder, not a file.');/* A whole drive (D:\) is not filled with models: D:\Strata is made and used. */if(isDriveRoot(path)){constown=dataFolderFromPick(path).path;makeFolder(own);path=realpathSync(own);}try{accessSync(path,constants.R_OK|constants.W_OK);}catch{thrownewError('Strata cannot write in that folder. Choose a folder you can save files in.');}preferences={...preferences,downloadDirectory:path};/* the same choice for the installer and the Terminal: the Control settings file */if(packaged)rememberDataFolder(controlSettingsFile(),path,Date.now()/1000,true);writeFileSync(settingsPath,JSON.stringify(preferences,null,2),{mode:0o600});shareDataFolder();emit();returnstate();}
@@ -157,11 +168,11 @@ export function createNativeControl({directory,requests=()=>[],changed=()=>{},sp
/* A chosen drafter that is not on this computer yet is part of the download: its size is listed beside the model's (Control decides; nothing is fetched here). */
constchoice=args.drafting;if(packaged&&typeofchoice==='string'&&choice!=='auto'&&typeofcontrol.draftingPlan==='function'){try{constd=awaitcontrol.draftingPlan({mode:choice,family:args.family??null,dataDir:snapshot.installed?.data_folder??null});if(d.needed)return{...result,drafting:{needed:true,gb:d.gb,line:d.line}};}catch{/* the model plan still stands */}}
constchoice=args.drafting??(dflash2Served===true?'auto':undefined);if(packaged&&typeofchoice==='string'&&typeofcontrol.draftingPlan==='function'){try{constsize=snapshot.catalog?.families?.find(f=>f.family===args.family)?.sizes.find(s=>s.model===args.model),here=snapshot.installed?.models?.find(m=>m.model===size?.id);constd=awaitcontrol.draftingPlan({mode:choice,family:args.family??null,dataDir:here?.runDataFolder??snapshot.installed?.data_folder??null,...(unloadsRunning(args)?{unloading:true}:{})});if(d.needed||d.mtp_needed)return{...result,drafting:{needed:true,drafter:d.needed?'dflash2':'mtp',gb:d.needed?d.gb:d.mtp_gb,line:d.line}};}catch{/* the model plan still stands */}}
returnresult;}
/* The folder the person picked in the first-run chooser can be the one that holds their GGUF files (or a folder inside it). It is theirs, not a Strata data folder: packs are never made in it, and a Strata folder around it (<Strata>/models/<tag>) is used instead (#3507, #3424). */
functioninstallArgs(args,confirm){constallowed=['family','model','context','vision','kv','gpu','gpus','backend','low_ram','port','gguf_dir'];constsize=snapshot.catalog?.families?.find(f=>f.family===args.family)?.sizes.find(s=>s.model===args.model);constexisting=snapshot.installed?.models?.find(m=>m.model===size?.id);/* A model adopted from the person's own GGUF folder reports that folder's parent as its data folder: it is theirs, not Strata's, so a second preparation never uses it for the packs. */if(args.gguf_dir===undefined&&existing?.ownGgufDir)args={...args,gguf_dir:existing.ownGgufDir};constdata=nativeDataDirectory(ownFilesDataFolder(args.data_dir??(args.gguf_dir?undefined:existing?.dataFolder)??preferences.downloadDirectory??modelsDirectory?.(),args));if(data&&packaged&&!args.data_dir)rememberDataFolder(controlSettingsFile(),data);return{...(data?{data_dir:data}:{}),...Object.fromEntries(allowed.filter(k=>args[k]!==undefined).map(k=>[k,args[k]])),/* One card is "gpu", never "gpus" (the installer wants two or more there: "Gpus='0' is not valid", #3203). */...(/^\d{1,2}$/.test(String(args.gpus??''))?{gpu:Number(args.gpus),gpus:undefined}:{}),/* Picture reading is only downloaded when it was asked for (Read pictures); the default stays the smaller download. */...(packaged&&args.vision===undefined?{vision:'no'}:{}),confirm};}
functioninstallArgs(args,confirm){constallowed=['family','model','context','vision','kv','gpu','gpus','backend','low_ram','port','gguf_dir','drafter'];/* One drafter per run: when the choice means DFlash2 the installer does not fetch the MTP layer's ~5 GB (the drafter is fetched by the load); choosing the MTP later fetches the layer once. */if(args.drafter===undefined&&args.drafting!==undefined||args.drafter===undefined&&dflash2Served===true){constd=resolveDrafter(args.drafting,args.family,dflash2Served,dflash2Split);if(d==='dflash2')args={...args,drafter:'dflash2'};}constsize=snapshot.catalog?.families?.find(f=>f.family===args.family)?.sizes.find(s=>s.model===args.model);constexisting=snapshot.installed?.models?.find(m=>m.model===size?.id);/* A model adopted from the person's own GGUF folder reports that folder's parent as its data folder: it is theirs, not Strata's, so a second preparation never uses it for the packs. */if(args.gguf_dir===undefined&&existing?.ownGgufDir)args={...args,gguf_dir:existing.ownGgufDir};/* A model that is installed keeps working in the data folder its own run config names (where its pack and draft layer are), whether its files are under it or in the person's own folder: that is never changed or written to the settings from here. Only a folder taken from the app's models folder is the person's choice, and the one the installer is told about. */constown=args.data_dir===undefined?existing?.runDataFolder:undefined;constdata=own??nativeDataDirectory(ownFilesDataFolder(args.data_dir??(args.gguf_dir?undefined:existing?.dataFolder)??preferences.downloadDirectory??modelsDirectory?.(),args));if(data&&packaged&&!args.data_dir&&!own)rememberDataFolder(controlSettingsFile(),data);return{...(data?{data_dir:data}:{}),...Object.fromEntries(allowed.filter(k=>args[k]!==undefined).map(k=>[k,args[k]])),/* One card is "gpu", never "gpus" (the installer wants two or more there: "Gpus='0' is not valid", #3203). */...(/^\d{1,2}$/.test(String(args.gpus??''))?{gpu:Number(args.gpus),gpus:undefined}:{}),/* Picture reading is only downloaded when it was asked for (Read pictures); the default stays the smaller download. */...(packaged&&args.vision===undefined&&args.family!=='deepseek41'?{vision:'no'}:{}),confirm};}
functionbegin(kind,run,metadata={}){if([...jobs.values()].some(j=>j.state==='running'))thrownewError('Finish or cancel the current engine operation first.');constjob={id:randomUUID(),kind,state:'running',startedAt:newDate().toISOString(),summary:'Starting…',result:null,error:null,...metadata};for(const[id,old]ofjobs)if(jobs.size>=12&&old.state!=='running')jobs.delete(id);jobs.set(job.id,job);emit();voidrun(job).then(result=>{if(job.state==='running'){Object.assign(job,{state:'done',summary:result?.summary??'Complete',result,finishedAt:newDate().toISOString()});}}).catch(error=>{if(job.state==='running')Object.assign(job,{state:'error',error:error.message,summary:error.message,...(error.details?{details:error.details}:{}),finishedAt:newDate().toISOString()});}).finally(()=>{if(job.kind.startsWith('update')||job.kind==='rollback')disconnect();lastRead=0;emit();voidrefresh(true);});returnstate().jobs.find(j=>j.id===job.id);}
asyncfunctionwatchInstall(job,current){job.summary=current.step??'Resuming preparation status…';job.result={install:current};while(job.state==='running'){awaitnewPromise(r=>setTimeout(r,1500));constresult=awaittool('strata_install');job.summary=result.summary;job.result=result;emit();if(!result.install?.running){if(result.install?.exit_code!==0)thrownewError(result.install?.error??'Preparation did not finish. Retry to reuse completed files.');return{summary:'Model preparation complete. Load it from Models.'};}}}
@@ -195,7 +206,9 @@ export function createNativeControl({directory,requests=()=>[],changed=()=>{},sp
/* A context the person picked is kept with a marker (ui_context_chosen), so only an old saved 32K, which has no marker, is read as the old default. */
if(args.contextChosen===true){try{constid=(result.install.family??args.family??'qwen')==='qwen'?String(args.model).toLowerCase():`${args.family}-${String(args.model).toLowerCase()}`;rememberSetting(record(id).path,'ui_context_chosen',true);}catch{/* the install itself is done */}}
/* `visionUnavailable`: this engine has no picture reader, so the run goes without pictures but nothing is saved: the model's own setting stays what the person had (it used to save "off", and the model stayed off after the engine got the reader). */
constnoReader=args.visionUnavailable===true;
constvision=ownEngine(config)?undefined:runtimeVision(config,noReader?'no':args.vision);/* DeepSeek has its own picture tower: its picture setting is the --vision flag setup wrote */
/* The app's suggested cards only fill a gap: a model whose settings file already names its GPUs ("gpu": [1, 0], with "gpu_order": "as_given") keeps them (#3256). A card the person picked in the screen always wins. */
/* Roles chosen in the screen (Main / Helper / Off per card, gpu-roles.mjs): the card list is the roles' mirror - Helpers first, the Main card LAST, Off cards left out - with "gpu_roles" beside it. Every card Off is allowed: the server says plainly what the engine can do. */
/* The Qwen engine's --vision stands alone, so it goes (with a picture word after it, if an older file has one). The DeepSeek engine's is a pair, `--vision on|off`, that setup wrote and the engine reads: it stays whole (taking out only the name left `off` behind: `--eos 1 off`, "unknown option off", #4358 #4359 #4505 #4507). */
for(constargofcfg.args??[])if(arg.endsWith('.gguf')&&!existsSync(resolve(repo(),arg)))thrownewError('An engine argument names a missing GGUF. Restore the saved arguments or repair the files.');
/* The app's own choice of request slots (4, fewer where the card has no room) is part of the configuration, so a tune measures the engine a load will start: slots take VRAM from the expert cache. */
/* The agent asks for long answers (a thinking model can use most of the window): let the engine clamp the output to what is left of the context instead of refusing the request. */
/* One drafter per run. Control (strata_control/drafting.py) says which one this load runs and whether its file is on this computer: the plan of the saved model file with the choice the Load carries. */
/* The engines this Load is about to unload (the load below unloads every instance unless it adds one beside them): their graphics memory is given back before the new engine starts, so Control must not count it as busy memory when it asks whether the all-cards split engine fits (`--engines-unloading`; the Terminal does the same, Runner.card_probe). A model added beside the others (`multiple`) keeps their memory busy. */
/* The plan is for the cards this Load uses: the screen's own card choice goes over the ones the saved settings name (the all-cards split engine needs two or more). */
finally{if(temporary){try{rmSync(temporary,{force:true});}catch{/* best effort */}}}
}
/* The choice changed to the MTP and its layer is not here yet: it is fetched once, by the installer (finished steps are skipped), in this Load. Only the Models screen's own Load carries the choice; a restart or a tuning run never fetches. */
job.summary='Downloading the MTP drafter…';emit();
/* the model's own files folder (the run config's --native): without it the installer, asked for this model by name, would fetch the whole model again next to the layer (an app install adopted from the person's own GGUF folder has its files elsewhere) */constnative=(()=>{consta=(config.args??[]).map(String),i=a.indexOf('--native');returni>=0?(a[i+1]??''):'';})();try{awaitinstall(job,{family:fam.family,model:size.model,drafter:'mtp',vision:args.vision??(config.vision?'yes':'no'),...(native?{gguf_dir:dirname(native)}:{})});returntrue;}
catch{if(job.state==='running'){job.drafterNote='The MTP drafter could not be downloaded.';emit();}returnfalse;}/* one short line, not the installer's own text; Drafting below says what runs instead */
/* Only the Models screen's own Load passes the choice (its dialog has listed the download): a restart, a tuning run or any other load leaves `asked` undefined and never fetches; the model then starts with the drafter that is on this computer and says why. */
awaitcontrol.draftingFetch({configPath:probe,mode:cfg.drafting,signal:job.engineAbort.signal,onEvent:event=>{if(event.event==='progress'&&job.state==='running'){job.summary=event.step;job.details=event.total>1&&event.done<event.total?`${(event.done/1e9).toFixed(2)} of ${(event.total/1e9).toFixed(2)} GB`:undefined;emit();}}});
awaitcontrol.draftingFetch({configPath:probe,mode:cfg.drafting??'auto',...(noSplit?{noSplit:true}:{}),...(unloadsRunning(args)?{unloading:true}:{}),signal:job.engineAbort.signal,onEvent:event=>{if(event.event==='progress'&&job.state==='running'){job.summary=event.step;job.details=event.total>1&&event.done<event.total?`${(event.done/1e9).toFixed(2)} of ${(event.total/1e9).toFixed(2)} GB`:undefined;emit();}}});
if(job.state==='running'){job.summary='Loading the model…';job.details=undefined;emit();}
}catch(error){if(job.state==='running'){job.drafterNote=error.message;job.summary='The DFlash2 drafter could not be downloaded, so the model starts with the MTP. Loading the model…';emit();}}
}catch(error){if(job.state==='running'){job.drafterNote=error.message;job.summary=drafts.mtp_here===false?'The DFlash2 drafter could not be downloaded, so the model starts without a drafter. Loading the model…':'The DFlash2 drafter could not be downloaded, so the model starts with the MTP. Loading the model…';emit();}}
finally{try{rmSync(probe,{force:true});}catch{/* best effort */}}
}
/* One key of a model's settings file, set or removed (the Drafting choice): written like the Settings screen writes it, to be there for the next load. */
functionrememberSetting(configFile,key,value){try{constcurrent=readJson(configFile);if(value===null?!(keyincurrent):current[key]===value)return;constnext={...current};if(value===null)deletenext[key];elsenext[key]=value;consttemp=configFile+'.ui-tmp';writeFileSync(temp,JSON.stringify(next,null,2)+'\n',{mode:0o600});renameSync(temp,configFile);}catch{/* the load goes on with the choice for this start */}}
/* Which drafter runs, when it is not the one that was asked for (the engine's own words, "Drafting: MTP (DFlash2 didn't fit)"): under Details of the load. */
asyncfunctionnoteDrafting(job,instance,key){letnote='';try{constr=awaitfetcher(`http://127.0.0.1:${instance.port}/metrics`,{headers:{authorization:`Bearer ${key}`},signal:AbortSignal.timeout(2000)});if(r.ok)note=String((awaitr.json())?.engine?.drafting_note??'');}catch{/* a convenience */}constlines=[job.drafterNote,note].filter(Boolean);if(lines.length)job.details=[job.details,...lines].filter(Boolean).join('\n');}
asyncfunctionnoteDrafting(job,instance,key){letnote='',fellBack=false;try{constr=awaitfetcher(`http://127.0.0.1:${instance.port}/metrics`,{headers:{authorization:`Bearer ${key}`},signal:AbortSignal.timeout(2000)});if(r.ok){constengine=(awaitr.json())?.engine;note=String(engine?.drafting_note??'');fellBack=engine?.split_fallback===true;}}catch{/* a convenience */}/* the all-cards split engine: the line of what runs (the server starts the normal layer split by itself when the split engine would not come up) */constlines=[job.drafterNote,fellBack?SPLIT_FELL_BACK:instance.splitNote,note].filter(Boolean);if(lines.length)job.details=[job.details,...lines].filter(Boolean).join('\n');}
/* The card the picture reader will be chosen for: the cards the model runs on, the one with the most memory (setup's rule is the card's memory: the CPU reader under 24 GB). */
/* Control says the picture file is gone (missing) or, when pictures were asked for, this look at the disk says so; a load that asked for nothing never second-guesses Control. */
if(strict)throwObject.assign(newError(PLAIN_PICTURE_CODES.includes(code)?String(error.message):'The picture files could not be downloaded. Try again.'),{details:details?[details]:undefined});
if(externalRunning().length)thrownewError('A Strata server is already running outside this UI. Stop it before loading here, or connect its API in Settings → Models.');
if(args.multiple&&instances.size>=8)thrownewError('Unload an instance before adding another. Up to eight native instances can run here.');
/* Read pictures is on and the picture files are not here: the Load fetches those files (about 0.9 GB, once) and nothing else of the model, then goes on. No internet: the model loads without pictures and the page says so. */
/* One drafter per run (strata_control/drafting.py): switching to the MTP on a model set up for DFlash2 fetches the layer once, here; DFlash2 is fetched below. */
if(drafts?.mtp_needed===true&&(args.values?.drafting!==undefined||drafts.split===true&&(config.drafting==null||String(config.drafting).toLowerCase()==='auto'))){/* the Models screen's own Load carries the choice; a plain Auto load fetches the layer too when Control says the all-cards split engine will take the start (it runs the MTP and nothing else): the same as the Terminal */awaitinstallMtpLayer(job,args,config);if(job.state!=='running')return;awaitrefresh(true);config=checkedSettingsConfig(readFileSync(recordFile(args.model).path,'utf8'));drafts=awaitdraftPlan(args,configFile);
/* The draft layer for the split could not be had (no internet, disk): the load goes on as the 0.2.10 app did, the normal layer split with the plan DFlash2's again (the Models screen's own Load fetches that drafter below). One plain line says so. */
if(drafts?.split===true&&drafts.mtp_needed===true){noSplit=true;job.drafterNote=job.drafterNote??'The MTP drafter could not be downloaded.';job.summary='Loading the model…';emit();drafts=awaitdraftPlan(args,configFile,{noSplit:true});}}
const{cfg,resources,visionPreferences,parallelChoice,parallelNote:chosenNote,dsNote}=buildConfig(args,config,drafts?.choice==='dflash2'&&drafts.needed===true&&args.values?.drafting===undefined?'mtp':drafts?.choice??null);/* (a drafter that is not here and will not be fetched by this load: the MTP runs, and the request slots stay) */
if(matching?.status==='ready'&&matching.fingerprint===fingerprint&&(args.multiple||instances.size===1))return{summary:`${matching.label} is already ready.`,instanceId:matching.id};
if(!args.multiple){if(busy(requests().filter(r=>instances.has(r.instanceId))))thrownewError('Finish or cancel native requests before switching models.');for(constidof[...instances.keys()])awaitunload(id);}elseif(matching)awaitunload(matching.id);
/* Another model on the same graphics card: the engine takes the card's free memory, so a second one usually finds none. Loading goes on (recommend, never force); if it fails, the reason is said. */
constsharedSentence=sharedWith?`GPU ${instance.gpus.find(g=>sharedWith.gpus.includes(g))+1} is already used by ${sharedWith.label}, so this model did not fit there. Load it on another GPU, or unload ${sharedWith.label} first.`:null;
/* The split engine reads every expert into the cards before it answers, and a start that falls back to the layer split begins again: the 10 minutes the Terminal gives a launch (never above the ceiling). */
/* The all-cards split engine: where the plan says it takes this start, Control turns the run config into the split engine's (the step `strata launch` does itself: the rule, the engine package and the draft layer are checked on this computer) and says so in one line. Any "no" leaves the layer split config as it is. The second try after a failed start is never split. */
/* A Control whose wait ran out has NOT stopped the engine: `runtime start` answers "startup_wait_expired" (exit 8), or "runtime operation wait expired" when the owner was too busy to answer in time, while the engine is still loading (#5177 #4293). That is not a failure: Control is asked, and a start it still reports as starting (or ready) is kept and watched below instead of being stopped and marked failed. */
/* A settings, profile or run file an elevated run made can be closed to this user: repair the access in Strata's own folders and start again (this is not the memory try below). */
if(isAccess(error)&&typeofcontrol.repairAccess==='function'&&job.state==='running'){awaitcontrol.repairAccess();append(id,'Strata repaired access to its own files; starting again.');try{awaitstartWith(cfg);started=Promise.resolve();error=null;}catch(again){error=again;}}
append(id,`The first start stopped (${said}). Starting once more: ${detail}`);job.summary=words;job.details=[said,`Second try: ${detail}`];instance.notice=words;emit();
try{awaitstartWith(retryConfig);append(id,'Strata Control reports the engine is ready (second try).');}
catch(second){constsaid2=humanStartError(second);instance.status='error';instance.error=said2;/* the first stop and the second try stay in job.details; the pop-up shows the last stop once */emit();thrownewError(instance.error);}}}
deleteinstance.splitNote;
try{awaitstartWith(retryConfig,false);append(id,'Strata Control reports the engine is ready (second try).');}
catch(second){if(awaitstillLoading(second))adopted=true;else{constsaid2=humanStartError(second);instance.status='error';instance.error=said2;/* the first stop and the second try stay in job.details; the pop-up shows the last stop once */emit();thrownewError(instance.error);}}}}
proc.on('error',error=>{instance.status='error';instance.error=error.message;emit();});proc.on('exit',code=>{if(instances.has(id)&&instance.status!=='unloading'){instance.status='error';instance.error=`Engine stopped${code===null?'':` (exit ${code})`}. Open diagnostics or reload.`;emit();}});emit();
/* A start Control stopped waiting for is watched for as long as the start asked for (the server ends the engine itself when its own wait runs out, a minute after this one), and Control is asked now and then whether the engine is still there. */
try{constresponse=awaitfetcher(`http://127.0.0.1:${instance.port}/health`,{headers:{authorization:`Bearer ${key}`},signal:AbortSignal.timeout(2000)});if(job.state!=='running'){awaitunload(id);return;}if(response.ok){instance.status='ready';instance.phase='idle';awaitnoteDrafting(job,instance,key);if(resources||args.vision!==undefined||JSON.stringify(cfg[USER_KEY]??null)!==JSON.stringify(config[USER_KEY]??null))writeModelConfig(record(args.model).path,{...config,...(resources?{...resources,ui_resource_profile:args.resourceProfile}:{}),...visionPreferences,...(cfg[USER_KEY]?{[USER_KEY]:cfg[USER_KEY]}:{})});emit();ensurePoll();instance.parallelNote=parallelNote;return{summary:retried?`${instance.label} is ready, after a second try: ${retried.words}`:`${instance.label} is ready.${parallelNote?' '+parallelNote:''}`,instanceId:id};}}catch{}
try{constresponse=awaitfetcher(`http://127.0.0.1:${instance.port}/health`,{headers:{authorization:`Bearer ${key}`},signal:AbortSignal.timeout(2000)});if(job.state!=='running'){awaitunload(id);return;}if(response.ok){instance.status='ready';instance.phase='idle';awaitnoteDrafting(job,instance,key);if(resources||args.vision!==undefined||args.contextChosen===true||JSON.stringify(cfg[USER_KEY]??null)!==JSON.stringify(config[USER_KEY]??null))writeModelConfig(record(args.model).path,{...config,...(resources?{...resources,ui_resource_profile:args.resourceProfile}:{}),...visionPreferences,...(args.contextChosen===true?{ui_context_chosen:true}:{}),...(cfg[USER_KEY]?{[USER_KEY]:cfg[USER_KEY]}:{})});emit();ensurePoll();instance.parallelNote=parallelNote;return{summary:retried?`${instance.label} is ready, after a second try: ${retried.words}`:`${instance.label} is ready.${parallelNote?' '+parallelNote:''}${picturesNote?' '+picturesNote:''}`,instanceId:id};}}catch{}
/* The engine of a ready instance stopped answering. After a few looks in a row Control is asked: an owner that says the engine is stopped, failed or lost ends the "ready" the app was still showing (the model looked loaded for good while every request failed, #4973); an engine that is only busy stays as it is. */
asyncfunctionreconcileLost(instance){if(instance.misses<3||instance.status!=='ready'||!instance.profile||typeofcontrol.status!=='function')return;letst=null;try{st=awaitcontrol.status(instance.profile);}catch{return;}if(instance.status==='ready'&&['stopped','failed','stale'].includes(st?.state)){instance.status='error';instance.error='Engine stopped. Open diagnostics or reload.';}}
asyncfunctionbenchmark(job,args){constinstance=resolveInstance(args.instanceId);job.instanceId=instance.id;if(busy(requests().filter(r=>r.instanceId===instance.id)))thrownewError('Wait for this model to finish before benchmarking.');conststarted=Date.now();job.abort=newAbortController();constresponse=awaitfetcher(`http://127.0.0.1:${instance.port}/v1/chat/completions`,{method:'POST',headers:{'content-type':'application/json',authorization:`Bearer ${instance.key}`},signal:AbortSignal.any([job.abort.signal,AbortSignal.timeout(180000)]),body:JSON.stringify({model:instance.apiModelId,messages:[{role:'user',content:'Explain how a rainbow forms in clear, simple language.'}],temperature:0,reasoning_effort:'none',max_tokens:200,stream:false})});constbody=awaitresponse.json();if(!response.ok)thrownewError(body.error?.message??'Benchmark failed.');if(job.state!=='running')return;constresult={summary:'Measured benchmark complete.',at:newDate().toISOString(),model:instance.apiModelId,durationMs:Date.now()-started,tokensIn:body.usage?.prompt_tokens??null,tokensOut:body.usage?.completion_tokens??null,outputTps:body.timings?.predicted_per_second??null,promptTps:body.timings?.prompt_per_second??null,source:'engine',settings:'Greedy · thinking off · up to 200 output tokens'};writeFileSync(join(directory,'native-benchmark.json'),JSON.stringify(result,null,2),{mode:0o600});returnresult;}
functionresolveInstance(id){constaliases=[...instances.values()].filter(i=>i.aliases?.includes(id));if(aliases.length>1&&!instances.has(id)&&![...instances.values()].some(i=>i.apiModelId===id))thrownewError('This alias matches more than one instance. Choose its exact model ID.');consti=instances.get(id)??[...instances.values()].find(i=>i.apiModelId===id)??aliases[0]??(!id&&instances.size===1?[...instances.values()][0]:undefined);if(!i)thrownewError('Choose a loaded model ID.');if(i.status!=='ready')thrownewError('This model is not ready.');returni;}
updateLog(`${job.kind} started (app ${thisAppVersion()??'unknown'})`);
if(job.kind==='install-engine-file'){if(typeofargs.path!=='string'||!args.path)thrownewError('Choose the engine file first.');letdone;try{done=awaitcontrol.installEngineFile(args.path,{onEvent,signal:job.abort.signal});}finally{try{constown=resolve(directory,'incoming-engine')+sep;if(resolve(args.path).startsWith(own))rmSync(args.path,{force:true});}catch{/* the copy in the host's own folder; a leftover is harmless */}}return{summary:updateSummary(done)==='Everything is already up to date.'?(done.results?.[0]?.note??'Nothing was changed.'):updateSummary(done),result:done};}
if(job.kind==='rollback'){constdone=awaitcontrol.rollbackEngine();return{summary:'The previous engine is back. Your models and settings are unchanged.',result:done};}
functionupdateSummary(done){constr=(done.results??[]).filter(x=>x.status==='updated').map(x=>x.component==='engine'?'The engine is updated and ready.':'Strata is updated. It takes effect the next time you open the app.');returnr.length?r.join(' '):'Everything is already up to date.';}
if(other.length){gate.refusal='A model that was not started from this app is running, so nothing was changed. Stop it, then update the engine again.';gate.outside=other.map(label);job.abort.abort();return;}
job.step='getting-ready';job.models=models.map(label);job.summary=`Getting the new engine ready. ${job.models.join(' and ')} keeps running until it is ready.`;
}
elseif(e.phase==='stopping'){for(constmofmodels){consti=mine.get(m.profile);if(i)i.status='unloading';}job.step='stopping';job.models=models.map(label);job.summary=`Stopping ${job.models.join(' and ')}. Answers in progress finish first.`;}
elseif(e.phase==='stopped'){for(constmofmodels){consti=mine.get(m.profile);if(i&&instances.get(i.id)===i){instances.delete(i.id);stopped.push(i);if(defaultId===i.id)defaultId=instances.keys().next().value??null;}}job.step='stopped';job.models=models.map(label);job.summary=`${job.models.join(' and ')} stopped. Updating the engine…`;}
/* "Start again with the same settings": the stopped model's saved profile (the run file it was started with) is started again; the engine in use is whichever Control has primary now. */
asyncfunctionrestartStopped(job){
constoffer=stoppedForUpdate;if(!offer?.instances?.length)thrownewError('There is no stopped model to start again.');
instances.set(instance.id,instance);if(!defaultId||!instances.has(defaultId))defaultId=instance.id;job.summary=`Starting ${instance.label}. Loading can take a few minutes.`;emit();
letwait=960;try{const{config,entry}=record(instance.model);wait=startWaitFor(config,entry,instance.model);}catch{/* the usual wait */}
while(!ready&&Date.now()<deadline&&job.state==='running'){try{constresponse=awaitfetcher(`http://127.0.0.1:${instance.port}/health`,{headers:{authorization:`Bearer ${instance.key}`},signal:AbortSignal.timeout(2000)});ready=response.ok;}catch{/* not yet */}if(!ready)awaitnewPromise(resolve=>setTimeout(resolve,1200));}
if(!ready)thrownewError('Loading took more than five minutes. Check the engine log and free memory.');
functionupdateSummary(done){constr=(done.results??[]).filter(x=>x.status==='updated').map(x=>x.component==='engine'||x.component==='dsv41'?'The engine is updated and ready.':'Strata is updated. It takes effect the next time you open the app.');returnr.length?r.join(' '):'Everything is already up to date.';}
functionexecute(cmd,args,cwd){returnnewPromise((resolve,reject)=>{letoutput='';constprocess=spawnProcess(cmd,args,{cwd,stdio:['ignore','pipe','pipe']});process.stdout.on('data',c=>{output+=c;if(output.length>100000)process.kill();});process.stderr.on('data',c=>append('update',c.toString()));process.on('error',reject);process.on('exit',code=>code===0?resolve(output):reject(newError('Git could not update this installation. Open diagnostics.')));});}
/* Every write of a model's run config keeps the person's options: those they wrote into "args" or "user_args" since the file was last written are read from the file itself, go over what the caller built, and "args_written" is refreshed. */
functionoperation(kind,args={}){if(!['install','load','benchmark','tune','autotune','recheck','calibrate','update-engine','update-strata','rollback','install-engine-file','remove'].includes(kind))thrownewError('Choose a supported native operation.');if(!snapshot.available&&!(packaged&&COMPONENT_KINDS.includes(kind)))thrownewError(snapshot.error??'Connect the native Strata installation first.');if(kind==='install'||kind==='load'){constrefusal=engineRefusal(snapshot.catalog,args.model,snapshot.engineInfo);if(refusal)thrownewError(refusal);}returnbegin(kind,job=>kind==='install'?install(job,args):kind==='load'?loadEngine(job,args):kind==='benchmark'?benchmark(job,args):kind==='tune'?tune(job,args):kind==='autotune'||kind==='recheck'?autotune(job,args):kind==='remove'?removeModel(job,args):program(job,args),{guided:args.guided===true,model:args.model,...(args.family?{family:args.family}:{})});}
functionoperation(kind,args={}){if(!['install','load','pictures','benchmark','tune','autotune','recheck','calibrate','update-engine','update-strata','rollback','install-engine-file','remove','restart-models'].includes(kind))thrownewError('Choose a supported native operation.');if(kind==='restart-models'&&!packaged)thrownewError('Choose a supported native operation.');if(!snapshot.available&&!(packaged&&(COMPONENT_KINDS.includes(kind)||kind==='restart-models')))thrownewError(snapshot.error??'Connect the native Strata installation first.');if(kind==='install'||kind==='load'){constrefusal=engineRefusal(snapshot.catalog,args.model,snapshot.engineInfo);if(refusal)thrownewError(refusal);}returnbegin(kind,job=>kind==='install'?install(job,args):kind==='load'?loadEngine(job,args):kind==='pictures'?picturesOnly(job,args):kind==='benchmark'?benchmark(job,args):kind==='tune'?tune(job,args):kind==='autotune'||kind==='recheck'?autotune(job,args):kind==='remove'?removeModel(job,args):kind==='restart-models'?restartStopped(job):program(job,args),{guided:args.guided===true,model:args.model,...(args.family?{family:args.family}:{})});}
/* ---- The models folder card (0.2.2): what a picked folder means, and the three choices. The copying is Control's (strata models move). ---- */
asyncfunctionlocalPreview(path){if(!packaged)thrownewError('Checking local model files is part of the Strata app.');if(typeofpath!=='string'||!path.trim()||[...path].some(c=>c.charCodeAt(0)<32))thrownewError('Choose a folder.');returncontrol.importPlan(path.trim());}
/* A DeepSeek config on AMD written by setup 0.2.8 or earlier names ONE card and marks it "asked", though setup picked it itself (strata_control/gpu_pick.py): it must not stop the app's recommended cards from applying. A card setup named for itself is the first of the usable cards, largest memory first; another card number was the person's. Roles and a "gpus_by" marker are always honoured. */
letinstance;try{instance=control.resolveInstance(body.model);}catch(e){/* #5312: an Anthropic client sends a model name of its own (Claude Desktop: claude-5-5-sonnet) and cannot change it: with exactly one model loaded the route answers with it */if(path==='/v1/messages'&&body.model){try{instance=control.resolveInstance(undefined);}catch{}}if(!instance){res.writeHead(400,{'content-type':'application/json'});res.end(JSON.stringify({error:{message:e.message,type:'invalid_request_error',param:'model',code:null}}));return;}}
constabort=newAbortController(),started=Date.now();letsawDone=false;/* #4690: a client that hangs up after `data: [DONE]` (the OpenAI SDK does) has the whole answer: that is not a cancel. */res.on('close',()=>{if(!res.writableEnded&&!sawDone)abort.abort();});
constobserve=value=>{/* Anthropic's stream carries the prompt count in message_start (message.usage) and only the output count in message_delta (usage): both are read (#4678). */for(constusageof[value.message?.usage,value.usage??value.response?.usage])if(usage){tokensIn=usage.prompt_tokens??usage.input_tokens??tokensIn;tokensOut=usage.completion_tokens??usage.output_tokens??tokensOut;}if(value.timings){tps=value.timings.predicted_per_second??tps;promptTps=value.timings.prompt_per_second??promptTps;}};
if(!response.ok){constcontent=awaitresponse.text();serving.finish(entry.id,{durationMs:Date.now()-started,error:`Engine returned ${response.status}`});res.writeHead(response.status,{'content-type':response.headers.get('content-type')??'application/json'});res.end(content);return;}
/** Graphics memory of DFlash2 alone, with the MTP layer not loaded beside it (RESULTS-DF2MEM.md: 890 MiB; the MTP layer 1082 MiB). Fit, plan and slot estimates use the drafter that is chosen. */
exportconstDFLASH2_VRAM_GB=0.89;
/** The same with the converted pack (the selector's codebooks as Q4_0 rows, 715 MiB; strata_control/drafting.py DRAFTER_MIB "dflash2_selq4"): `capabilities.dflash2Selq4` says it is the one in use. */
// DFlash2 chosen by name with a number of requests (owner, 2026-11-03: the person's own choice wins) runs DFlash2 with that many requests, a drafter per request
// (strata_control/drafting.py blocked_by `named`). Expert mode's batch-mtp wish is still added here as in 0.2.10: the server knows the backend and the engine, takes
// --batch-mtp out when DFlash2 runs, and keeps it when the MTP runs instead (Metal, which has no per-request drafters).
// DFlash2 (named, or Auto where the engine has it) and nobody chose a number of requests: one at a time, the app does not fill in slots beside it. (A number the person chose is kept
// above: DFlash2 named runs it with that many requests; Auto keeps the MTP for several.)
/** The engine's own default reserve, and the one setup writes for pictures read on the GPU: neither is a choice of the person. */
constENGINE_DEFAULT_RESERVE_MIB=700;
@@ -75,28 +76,57 @@ export function resourceSettings(config,hardware,profile,model={},values={}){
/* A reserve the person set is theirs, 0 included: no floor and no share of the card (#3215 #3270). Without one the card gets the safety default (defaultReserves). */
// RAM: "leave 20% for my work" keeps the model's RAM within 80% of the PC. A model whose experts do not fit there (the
// setup rule: experts plus 10 GB) keeps only that many GiB of them in RAM and reads the rest from the model files
// (--resident-budget-gib, which implies --mmap-experts). Full performance takes the budget out again.
// RAM: "leave 20% for my work". An engine that knows the setting (RAM_KEEP_MIN_ENGINE and newer) is asked to keep W GiB of the RAM free, W = ramKeepGib(RAM), as the env STRATA_RAM_KEEP_GIB: it decides from
// the RAM that is free at its start, not from the PC's total, using the full RAM copy of the experts (the arena) only if W GiB stay free after it and otherwise keeping the hottest experts in a RAM copy
// that leaves max(W, its own safety headroom) free. An env variable and not the flag --ram-keep-gib, so that an engine without the setting never meets an argument it does not know. The engine in use
// is the one the Engine tab selected (its version, model.engineVersion, or the folder named in the run config's exe); an older engine, or one whose version cannot be read, gets the rule of 0.2.9
// unchanged, which every engine understands: a RAM budget of 80% of the PC (--resident-budget-gib, which implies --mmap-experts) when the model's experts do not fit there (the setup rule: experts plus
// 10 GB), and a STRATA_RESIDENT_HEADROOM_GIB of 20% of the RAM. Full performance (and `strata serve`) pass nothing: the whole system, the arena whenever it fits; either rule is taken out again.
// The person's own RAM choice wins: a RAM mode or --ram-keep-gib in their args, and STRATA_RAM_KEEP_GIB or STRATA_RESIDENT_HEADROOM_GIB of theirs in the env (the engine reads those first).
/** The first engine version that reads STRATA_RAM_KEEP_GIB /--ram-keep-gib(thesettingwasaddedin0.1.56,theengineofthe0.2.9update;thereleased0.2.9engineis0.1.55).Anengineolderthanthis,oronewhoseversioncannotberead,getsthe
/** True when the engine with this version string knows the setting. Unknown or unreadable: false (the 0.2.9 rule works on every engine, an ignored env variable would keep no room). */
/** The version in the folder a run config's exe names (.../engine/0.1.55-windows-x86_64-cuda-cuda13/strata.exe), or null (the exe "component:strata-engine" means the active engine, whose version the caller gives). */
// The engine refused an oversized prompt chunk at the start (an APU whose pool the expert cache fills; the person or the environment named the chunk): the same chunk would fail again, and a memory setting is not the cause.
if(/prompt chunk does not fit/i.test(text))returnnull;
// A start that died for a reason that is not memory (a missing or bad DLL, a crash, a blocked file) is not helped by a memory setting, and must not be called a memory failure.
if(/0xC0000135|0xC000007B|0xC0000142|0xC000001D|0xC0000005|0xC0000409|0xC0000022|error while loading shared libraries|cannot open shared object file|\bENOENT\b|exit code 127|exit code 139|illegal instruction/i.test(text))returnnull;
constranOut=/out of memory|memory guard|\boom\b|\bkilled\b|exit code 137|cannot allocate|not enough memory|bad_alloc|insufficient memory/i.test(text),first=ranOut?'The first start ran out of memory':'The first start did not finish';
{id:'aliases',label:'API model aliases',group:'Serving',type:'array',hint:'Comma-separated names clients may use for this model.'},
{id:'idle_unload_s',label:'Unload after idle seconds',group:'Serving',type:'number',min:0,max:86400,hint:'Zero keeps the model loaded.'},
{id:'lazy_load',label:'Load on first request',group:'Serving',type:'boolean'},
{id:'mtp',label:'Draft layer (MTP)',group:'Serving',type:'boolean',hint:'The draft layer guesses several words ahead, which makes answers faster, and costs about 1 GB of graphics memory and some RAM. Turn it off if the model stops while it starts, or the card is small: the expert cache gets that memory back and answers are slower. Default is on.'},
{id:'drafting',label:'Drafting',group:'Serving',type:'enum',choices:['auto','mtp','dflash2','off'],hint:'Which drafter guesses words ahead to make answers faster. Auto (recommended) lets Strata pick from what it measured on cards like yours; today that is the MTP. DFlash2 is for Qwen3.8-Flash-Next only and downloads a 1.2 GB drafter once. If DFlash2 cannot run, the MTP is used and Details says why. Off turns the draft layer off.'},
{id:'batchMtp',label:'MTP per request',group:'Serving',type:'boolean',hint:'Each concurrent request uses the draft layer. Left alone, Strata turns it on for one graphics card with 24 GB or more and several requests at once, and off otherwise; choose on or off here to decide yourself.'},
{id:'mtp',notForModel:'deepseek',label:'Draft layer (MTP)',group:'Serving',type:'boolean',hint:'The draft layer guesses several words ahead, which makes answers faster, and costs about 1 GB of graphics memory and some RAM. Turn it off if the model stops while it starts, or the card is small: the expert cache gets that memory back and answers are slower. Default is on.'},
{id:'drafting',notForModel:'deepseek',label:'Drafting',group:'Serving',type:'enum',choices:['auto','mtp','dflash2','off'],hint:'Which drafter guesses words ahead to make answers faster, one at a time. Auto (recommended) is DFlash2 for Qwen3.8-Flash-Next where the engine has it, the MTP otherwise; on two or more identical graphics cards that the all-cards split engine takes, it is that engine with the MTP draft layer. DFlash2 downloads a 1.2 GB drafter once, the MTP its draft layer once. With Auto, several requests at once use the MTP. If you choose DFlash2 yourself, it runs with the number of requests you set. If DFlash2 cannot run, the MTP is used alone and Details says why. Off turns the drafter off.'},
{id:'batchMtp',notForModel:'deepseek',label:'MTP per request',group:'Serving',type:'boolean',hint:'Each concurrent request uses the draft layer. Left alone, Strata turns it on for one graphics card with 24 GB or more and several requests at once, and off otherwise; choose on or off here to decide yourself.'},
{id:'ds_experts',label:'Speed (DeepSeek)',group:'Serving',type:'enum',choices:['5','6'],forModel:'deepseek',hint:'Faster uses 5 of 6 experts per word: a small quality change, usually not noticeable. Full quality uses all 6. Left alone, the engine picks Faster on a PC whose graphics cards total 16 GB or less.'},
{id:'parallel',label:'Concurrent requests',group:'Serving',type:'number',min:1,max:8,hint:'More slots need more memory and can slow each response.'},
/** Settings the app itself applies when it starts a model (they are not read from the engine's source, so the engine's text never says it supports them). */
/** The families that have a Drafting choice and the model names the DFlash2 drafter was made for (strata_control/drafting.py SUPPORTED_FAMILIES, _SUPPORTED_NAME): Qwen3.8-Flash-Next, Gyro and Unsloth's quantizations of the same weights; not Swift, Coder or NVFP4. */
/** The drafter a choice means (one per run; strata_control/drafting.py choice_of): Auto is DFlash2 where the engine has it (`served`, Control's word) and the model is one it was made for, else the MTP; off and mtp are what they say. `model` is a run config's model name or a family. */
/** `split` (Control's plan: the all-cards split engine will take this start) makes Auto the MTP, the draft layer that engine runs; a DFlash2 the person chose by name stays. */
/** The engine arguments without the draft layer: --mtp <folder or count>, --spec-min-p <p> (its confidence threshold) and --batch-mtp. --spec stays: prompt lookup still uses its windows. Same rule as strata_control/draft_layer.py. */
@@ -8,3 +8,17 @@ export function runtimeVision(config,requested){
if(mode==='cpu'&&config.vision.cpu_exe){const{cpu_exe,...rest}=config.vision;return{...rest,exe:cpu_exe,gpu:false};}// AMD: the CPU reader program, not the HIP one with the GPU hidden
if(code==='EADDRINUSE')return`Port ${port} is already in use. Strata may already be running; close it or the other program, or start Strata with another port (STRATA_PORT).`;
if(code==='EACCES'&&platform!=='win32')return`This system does not let Strata use port ${port}. On Linux and macOS, ports below 1024 need administrator rights. Start Strata with another port (STRATA_PORT), for example 8801.`;
if(code==='EACCES')return`Windows or a firewall does not let Strata use port ${port}. Hyper-V, WSL and Docker keep ranges of ports for themselves (see "netsh int ipv4 show excludedportrange protocol=tcp"). Start Strata with another port (STRATA_PORT), for example 8801.`;
if(code==='EADDRNOTAVAIL')return`${host} is not an address of this computer, so Strata cannot listen on it. Check STRATA_BIND.`;
return`Strata cannot listen on ${host}:${port} (${error?.message??code}).`;
// The owner too, so a file an elevated run made is not left owned by Administrators (needs the right to; best effort).
try{execFileSync(system32('icacls.exe'),[path,'/setowner',`*${currentUserSid()}`],{stdio:'ignore',timeout:15000,windowsHide:true});}catch{/* the explicit entry above already opens it to the user */}
endTreeThenKill(child);// Windows: taskkill /T. Linux and macOS: the job leads its own process group (startDetached), so the group is ended (#4529 #4570)
}
/** How big a folder is, stopping early once it is clearly too big to copy for a trial. */
/** Plain words for a switch or a delete in Settings > Engine: { message, details }. Control's own sentence for "the engine in use" is already plain and is kept. */
letmessage=action==='delete'?"The engine wasn't deleted. Nothing else was changed.":"The engine wasn't switched. Nothing was changed.";
if(exit===7||/is serving|model is running/i.test(raw))message='A model is running, so nothing was changed. Unload the model in Models, then try again.';
elseif(/engine in use/i.test(raw))message='This is the engine in use. Switch to another one first, then delete it.';
elseif(/running program is using|something is using it/i.test(raw))message='A program is still using this engine, so it was not deleted. Unload your models and try again.';
elseif(/not installed|already been deleted/i.test(raw))message='That engine is already gone. The list has been refreshed.';
elseif(/another update is already running/i.test(raw))message='An update is running. Wait for it to finish, then try again.';
elseif(exit===5||/can no longer be used|does not fit|cannot run/i.test(raw))message="This engine can't run on this computer, so nothing was changed.";
return{message,details:raw||undefined};
}
/** An Error carrying the plain message and, behind it, the technical reason. */
if(engine.status==='available')Object.assign(engineView,{status:'available',message:`A new engine is available (${mib(engine.download_bytes)}).`});
if(engine.status==='available'&&engine.installed_locally)Object.assign(engineView,{status:'available',message:'A newer engine is already on this computer. Update switches to it; nothing is downloaded.'});
elseif(engine.status==='available')Object.assign(engineView,{status:'available',message:`A new engine is available (${mib(engine.download_bytes)}).`});
elseif(engine.status==='install')Object.assign(engineView,{status:'available',message:`This PC needs an engine to run models. Press Update engine to download it (${mib(engine.download_bytes)}).`});
elseif(engine.status==='current'||engine.status==='failed-before'||engine.status==='skipped')Object.assign(engineView,{status:'current',message:'The engine is up to date.',details:engine.note});
elseif(engine.status==='wrong-make')Object.assign(engineView,{status:'unavailable',message:engine.note??'Nothing was changed: the update was for another make of graphics card.',details:(engine.reasons??[]).join('\n')||undefined});
@@ -136,7 +136,7 @@ export function CommandPalette({ open }: {
<inputref={inputRef}className="st-input st-input--bare"style={{flex: 1}}placeholder={tr("Search chats, screens or actions…")}aria-label={tr("Search commands")}aria-activedescendant={flat[cursor]?`command-${flat[cursor].id}`:undefined}value={query}onChange={(e)=>setQuery(e.target.value)}onKeyDown={onKeyDown}/>
<kbdclassName="kbd">{tr("esc")}</kbd>
</div>
<divstyle={{overflowY:'auto',padding: 8}}>
<divclassName="palette__list">
{tr(flat.length===0?(<divclassName="small muted"style={{padding: 18}}>{tr("Nothing matches “{0}”. Try > for commands, # for conversations, or @ for workspaces.",[query])}</div>):(grouped.map(([group,items])=>(<divkey={group}style={{marginBottom: 6}}>
conststatus=phase==='unavailable'?'Status unavailable':phase==='error'?'Error':phase==='loading'?'Loading':!current&&!nativeCurrent?'No model loaded':phase==='reading'?'Reading prompt':phase==='thinking'?'Thinking':phase==='generating'?'Generating':'Ready';
// The model is busy with a chat that is not the one in front of the person: the chip says which one (a title, or how many).
@@ -92,7 +100,7 @@ export function Sidebar({ containerRef }: {
<divclassName={cx('ws__row',activeWorkspace===w.id&&'is-active')}><buttonclassName="ws__name"aria-expanded={expanded}onClick={()=>{setActiveWorkspace(w.id);setFolders(f=>({...f,[w.id]:!expanded}));}}><Iconname="chevron"size={14}className={cx('ws__chev',expanded&&'is-open')}/><Iconname="folder"size={17}/><span>{w.name}</span></button><IconBtnname="pencil"title={tr("Rename {0}",[w.name])}onClick={()=>{setWsName(w.name);setNavOpen(false);setRenameWsId(w.id);}}/>{tr(w.id!=='root'?<IconBtnname="x"title={tr("Remove {0} from sidebar",[w.name])}onClick={()=>{setNavOpen(false);setRemoveWorkspaceId(w.id);}}/>:null)}</div>
{tr(expanded?<divclassName="navsessions">
{tr(shown.map(s=><divkey={s.id}>
<divclassName="ws__session row"><aclassName="navsession"href={href('chat',s.id)}aria-current={activeSession===s.id&&location.route==='chat'?'page':undefined}onClick={()=>selectSession(s.id)}onContextMenu={e=>{e.preventDefault();openMenu(s.id,e.currentTarget);}}title={sessionTitle(s)}><spanclassName="navsession__title">{sessionTitle(s)}</span>{tr(s.pinned?<spanaria-label={tr("Pinned")}><Iconname="pin"size={12}/></span>:<spanclassName="navsession__time">{tr(relTime(s.updatedAt))}</span>)}{tr(isUnread(s,activeSession)?<spanclassName="navsession__unread"aria-label={tr("Unread")}/>:null)}{tr(s.archived?<spanclassName="xs muted">{tr("Archived")}</span>:null)}</a><buttonclassName="st-btn st-btn--icon session-overflow"aria-label={tr("Actions for {0}",[sessionTitle(s)])}onClick={e=>openMenu(s.id,e.currentTarget)}><Iconname="ellipsis"size={16}/></button></div>
<divclassName="ws__session row"><aclassName="navsession"href={href('chat',s.id)}data-working={working.has(s.id)||undefined}aria-current={activeSession===s.id&&location.route==='chat'?'page':undefined}onClick={()=>selectSession(s.id)}onContextMenu={e=>{e.preventDefault();openMenu(s.id,e.currentTarget);}}title={sessionTitle(s)}>{tr(working.has(s.id)?<spanclassName="navsession__working"role="img"aria-label={tr("Working")}title={tr("Working")}><svgviewBox="0 0 16 16"aria-hidden="true"focusable="false"><circleclassName="navsession__track"cx="8"cy="8"r="6"/><circleclassName="navsession__arc"cx="8"cy="8"r="6"pathLength="100"/></svg></span>:null)}<spanclassName="navsession__title">{sessionTitle(s)}</span>{tr(s.pinned?<spanaria-label={tr("Pinned")}><Iconname="pin"size={12}/></span>:<spanclassName="navsession__time">{relTimeShort(s.updatedAt)}</span>)}{tr(!working.has(s.id)&&isUnread(s,activeSession)?<spanclassName="navsession__unread"aria-label={tr("Unread")}/>:null)}{tr(s.archived?<spanclassName="xs muted">{tr("Archived")}</span>:null)}</a><buttonclassName="st-btn st-btn--icon session-overflow"aria-label={tr("Actions for {0}",[sessionTitle(s)])}onClick={e=>openMenu(s.id,e.currentTarget)}><Iconname="ellipsis"size={16}/></button></div>
{tr(!q&&rows.length>shown.length?<buttonclassName="ws-view small"onClick={()=>{setNavOpen(false);setRecentOpen(true);}}>{tr("View all chats")}</button>:null)}
@@ -102,7 +110,7 @@ export function Sidebar({ containerRef }: {
<divclassName="toolcard__quick-actions"><spanclassName="xs muted">{tr("Needs your approval")}</span><Btnsize="sm"onClick={()=>resolveTool(messageId,t.id,false)}>{tr("Deny")}</Btn><Btnvariant="primary"size="sm"icon="check"onClick={()=>resolveTool(messageId,t.id,true)}>{tr("Approve")}</Btn></div>
{tr(current.kind==='website'?<><iframekey={`${current.id}-${revision}`}title={current.title}src={current.content?undefined:url??undefined}srcDoc={current.content}sandbox="allow-scripts"referrerPolicy="no-referrer"/></>:current.kind==='pdf'?url?<Suspensefallback={<pclassName="muted preview-placeholder">{tr("Opening PDF viewer\u2026")}</p>}><PdfPreviewkey={`${current.id}-${revision}`}url={url}/></Suspense>:<p>{tr("No PDF to display.")}</p>:current.kind==='image'?<divclassName="image-preview"data-size={imageSize==='1'?'actual':'fit'}>{tr(url?<imgkey={`${current.id}-${revision}`}src={url}alt={current.title}onError={()=>setError('Couldn’t load this image.')}/>:null)}{tr(error?<prole="alert">{tr(error)}</p>:null)}</div>:loading?<pclassName="muted preview-placeholder">{tr("Loading document\u2026")}</p>:error?<prole="alert"className="inline-error preview-placeholder">{tr(error)}</p>:current.kind==='code'?<SyntaxCodetext={text}filename={artifactFilename(current)}lineNumbers/>:current.kind==='table'?<TablePreviewkey={current.id}text={text}/>:mode==='source'?<SyntaxCodetext={text}language="markdown"lineNumbers/>:<articleclassName="document-markdown"><Markdowntext={text}/></article>)}
{tr(current.kind==='website'?<><iframekey={`${current.id}-${revision}-${version}`}title={current.title}src={current.content?undefined:url??undefined}srcDoc={current.content}sandbox="allow-scripts"referrerPolicy="no-referrer"/></>:current.kind==='pdf'?url?<Suspensefallback={<pclassName="muted preview-placeholder">{tr("Opening PDF viewer\u2026")}</p>}><PdfPreviewkey={`${current.id}-${revision}-${version}`}url={url}/></Suspense>:<p>{tr("No PDF to display.")}</p>:current.kind==='image'?<divclassName="image-preview"data-size={imageSize==='1'?'actual':'fit'}>{tr(url?<imgkey={`${current.id}-${revision}-${version}`}src={url}alt={current.title}onError={()=>setError('Couldn’t load this image.')}/>:null)}{tr(error?<prole="alert">{tr(error)}</p>:null)}</div>:loading?<pclassName="muted preview-placeholder">{tr("Loading document\u2026")}</p>:error?<prole="alert"className="inline-error preview-placeholder">{tr(error)}</p>:current.kind==='code'?<SyntaxCodetext={text}filename={artifactFilename(current)}lineNumbers/>:current.kind==='table'?<TablePreviewkey={current.id}text={text}/>:mode==='source'?<SyntaxCodetext={text}language="markdown"lineNumbers/>:<articleclassName="document-markdown"><PreviewMarkdowntext={text}/></article>)}
<divclassName="chat-toolbar__conversation"><h1title={title}>{title}</h1><ChatModelPicker/><buttonref={agentsRef}className="chat-agents"title={tr(count?`${count} subagents running · View overview`:'View subagent overview')}aria-label={tr(count?`${count} subagents running — show overview`:'Subagents — show overview')}aria-haspopup="dialog"aria-expanded={agents}data-working={count>0||undefined}onClick={()=>setAgents(v=>!v)}><Iconname="experts"size={17}/>{tr(count>0?<spanclassName="agent-live-dot"/>:null)}<spanclassName="chat-agents__label">{tr("Subagents")}</span>{tr(session?.agents?.length?<spanclassName="chat-agents__count">{tr(count||session.agents.length)}</span>:null)}<Iconname="chevron"className={agents?'is-open':''}size={13}/></button><spanclassName="spacer"/><buttonref={menuRef}className="st-btn st-btn--icon"title={tr("Chat options")}aria-label={tr("Chat options")}aria-haspopup="menu"aria-expanded={menu}onClick={()=>setMenu(v=>!v)}><Iconname="ellipsis"/></button>{tr(!selected&&(items.length||extraTabs.length)?<IconBtnname="panel"title={tr("Open previews")}onClick={()=>onSelect(extraTabs.at(-1)?.id??items[0].id)}/>:null)}</div>
{tr(previews?<divclassName="chat-toolbar__previews"><ArtifactTabsextraTabs={extraTabs}onCloseTab={onCloseTab}items={items}selected={selected}onSelect={onSelect}/><divclassName="preview-window-actions"><divid="preview-file-actions"className="preview-file-actions"/><IconBtnname={expanded?'contract':'expand'}title={tr(expanded?'Exit full screen preview':'Full screen preview')}onClick={onExpand}/><IconBtnname="panel"title={tr("Collapse preview sidebar")}onClick={onClose}/></div></div>:null)}
{tr(agents?<Popoveranchor={agentsRef.current}title={tr("Subagents")}detail={session?.agents?.length?<>{tr(count)}{tr(" running \u00B7 ")}{tr(session.agents.filter(a=>a.status==='completed').length)}{tr(" completed \u00B7 ")}{tr(session.agents.length)}{tr(" total")}</>:undefined}width={460}onClose={()=>setAgents(false)}>{tr(session?.agents?.length?<>{tr(session.agents.map(a=><SubagentRowkey={a.id}agent={a}now={now}/>))}</>:<divclassName="subagents-empty"><Iconname="experts"size={24}/><strong>{tr("No subagents recorded")}</strong><pclassName="small muted">{tr("Subagents that run in this conversation appear here, with their progress, token usage and elapsed time. Completed runs stay visible.")}</p></div>)}</Popover>:null)}
{tr(menu?<FloatingMenuanchor={menuRef.current}title={tr("Chat options")}onClose={()=>setMenu(false)}><buttonrole="menuitem"className="menu__item"disabled={!!remote}title={tr(remote?"API models use their provider’s response settings.":undefined)}onClick={()=>{setMenu(false);setSettings(true);}}><Iconname="sliders"/>{tr("Response settings")}</button><buttonrole="menuitem"className="menu__item"disabled={!session}onClick={()=>{if(session)
{tr(active?<sectionclassName="tunnel-stage tunnel-stage--status"key={tunnel?.status}aria-live="polite"><spanclassName="tunnel-stage__symbol"><Iconname={tunnel?.status==='connected'?'check':'clock'}size={26}/></span><h3className="h3">{tr(tunnel?.status==='connected'?'You’re connected':tunnel?.status==='stopping'?'Stopping the tunnel…':'Connecting to Cloudflare…')}</h3>{tr(tunnel?.url?<><pclassName="small muted">{tr("Use this address and your Strata API key in your other app.")}</p><divclassName="install-command"><code>{tunnel.url}</code><IconBtnname="copy"title={tr("Copy tunnel URL")}onClick={()=>voidnavigator.clipboard.writeText(tunnel.url!)}/></div><pclassName="xs muted">{tr(tunnel.mode==='quick'?'Temporary address · turn off streaming in your client.':`Your Cloudflare route must point to ${tunnel.originUrl}.`)}</p></>:<pclassName="small muted">{tr("This usually takes a few seconds. You can close this window while it connects.")}</p>)}</section>:<sectionclassName="tunnel-stage"key={step}aria-labelledby="tunnel-step-title">
{tr(active?<sectionclassName="tunnel-stage tunnel-stage--status"key={tunnel?.status}aria-live="polite"><spanclassName="tunnel-stage__symbol"><Iconname={tunnel?.status==='connected'?'check':'clock'}size={26}/></span><h3className="h3">{tr(tunnel?.status==='connected'?'You’re connected':tunnel?.status==='stopping'?'Stopping the tunnel…':'Connecting to Cloudflare…')}</h3>{tr(tunnel?.url?<><pclassName="small muted">{tr("Use this address and your Strata API key in your other app.")}</p><divclassName="install-command"><code>{tunnel.url}</code><CopyBtntext={tunnel.url!}label="Tunnel URL"title="Copy tunnel URL"/></div><pclassName="xs muted">{tr(tunnel.mode==='quick'?'Temporary address · turn off streaming in your client.':`Your Cloudflare route must point to ${tunnel.originUrl}.`)}</p></>:<pclassName="small muted">{tr("This usually takes a few seconds. You can close this window while it connects.")}</p>)}</section>:<sectionclassName="tunnel-stage"key={step}aria-labelledby="tunnel-step-title">
{tr(step===0?<><pclassName="small muted">{tr("Give your local API an HTTPS address that another app can reach.")}</p><divclassName="tunnel-choices"role="radiogroup"aria-label={tr("Cloudflare connection type")}>{tr([{value:'quick'asconst,title:'Free temporary URL',detail:'No account or domain needed. Good for a quick connection.',icon:'bolt'asconst},{value:'domain'asconst,title:'My own domain',detail:'A stable address on a domain you manage with Cloudflare.',icon:'link'asconst}].map(choice=><buttonkey={choice.value}role="radio"aria-checked={mode===choice.value}tabIndex={mode===choice.value?0:-1}onKeyDown={e=>{if(['ArrowRight','ArrowLeft','ArrowUp','ArrowDown'].includes(e.key)){
{tr(step===1?<divclassName="tunnel-automatic"data-ready={installed||undefined}aria-live="polite"aria-busy={pending}><spanclassName="tunnel-stage__symbol"><Iconname={installed?'check':error?'warning':'refresh'}size={28}/></span><pclassName="small muted">{tr(installed?'Your Cloudflare connector is installed and ready.':error?'Automatic setup needs another try.':'Strata downloads and sets up the Cloudflare connector for you.')}</p>{tr(installed?<>{tr(tunnel?.connector?<pclassName="xs muted">{tr("Using {0}",[tunnel.connector.summary])}</p>:null)}{tr(other?<pclassName="xs muted"role="status">{tr(other.others.length===1&&!other.own.length?"cloudflared is already running for another tunnel (PID {0}).":other.own.length?"Strata’s tunnel for this address is already running (PID {0}).":"cloudflared is already running for other tunnels (PIDs {0}).",[(other.own.length?other.own : other.others).join(", ")])}</p>:null)}</>:error?<BtnonClick={()=>setRetry(v=>v+1)}>{tr("Retry setup")}</Btn>:<spanclassName="small muted"role="status">{tr("Setting up\u2026")}</span>)}</div>:null)}
{tr(step===2?<><pclassName="small muted">{tr(keyReady?"Only apps with your Strata API key can use this connection.":"Without an API key, anyone who finds the link can use your model.")}</p>{tr(keyReady?<divclassName="tunnel-check"><Iconname="shield"/><div><strong>{tr("API key ready")}</strong><span>{tr("Use your existing key in the app you connect.")}</span></div></div>:<divclassName="tunnel-key-step"><Iconname="shield"size={30}/><pclassName="small">{tr("Create a key to protect your API. You\u2019ll return here when it\u2019s ready. You can also continue without one, but anyone who finds the link can then use your model.")}</p><BtnonClick={onCreateKey}>{tr("Create an API key (recommended)")}</Btn></div>)}</>:null)}
{tr(mode==='domain'&&step===3?<><pclassName="small muted">{tr("Create a tunnel in your ")}<ahref="https://dash.cloudflare.com/"target="_blank"rel="noreferrer">{tr("Cloudflare dashboard \u2197")}</a>{tr(". Copy the token from its connector setup.")}</p><labelclassName="input-field"><span>{tr("Tunnel token")}</span><inputtype="password"className="st-input"autoComplete="off"spellCheck={false}value={token}onChange={e=>setToken(e.target.value)}placeholder={tr("Paste only the token after --token")}/></label><labelclassName="input-field"><span>{tr("Your hostname")}</span><inputclassName="st-input"autoComplete="off"spellCheck={false}value={hostname}onChange={e=>setHostname(e.target.value)}placeholder={tr("ai.example.com")}/></label><div><pclassName="small muted">{tr("Add a published application route for this hostname with this service URL:")}</p><divclassName="install-command"><code>{tunnel?.originUrl??'Loading…'}</code><IconBtnname="copy"title={tr("Copy tunnel service URL")}disabled={!tunnel?.originUrl}onClick={()=>voidnavigator.clipboard.writeText(tunnel!.originUrl)}/></div></div><pclassName="xs muted">{tr("The token is used for this connection only and is not saved.")}</p></>:null)}
{tr(mode==='domain'&&step===3?<><pclassName="small muted">{tr("Create a tunnel in your ")}<ahref="https://dash.cloudflare.com/"target="_blank"rel="noreferrer">{tr("Cloudflare dashboard \u2197")}</a>{tr(". Copy the token from its connector setup.")}</p><labelclassName="input-field"><span>{tr("Tunnel token")}</span><inputtype="password"className="st-input"autoComplete="off"spellCheck={false}value={token}onChange={e=>setToken(e.target.value)}placeholder={tr("Paste only the token after --token")}/></label><labelclassName="input-field"><span>{tr("Your hostname")}</span><inputclassName="st-input"autoComplete="off"spellCheck={false}value={hostname}onChange={e=>setHostname(e.target.value)}placeholder={tr("ai.example.com")}/></label><div><pclassName="small muted">{tr("Add a published application route for this hostname with this service URL:")}</p><divclassName="install-command"><code>{tunnel?.originUrl??'Loading…'}</code><CopyBtntext={tunnel?.originUrl??''}label="Tunnel service URL"title="Copy tunnel service URL"disabled={!tunnel?.originUrl}/></div></div><pclassName="xs muted">{tr("The token is used for this connection only and is not saved.")}</p></>:null)}
{tr(step===last?<><pclassName="small muted">{tr(mode==='quick'?'Cloudflare will create your address when you connect.':'Start the connector for the domain you configured.')}</p><dlclassName="tunnel-review"><div><dt>{tr("Address")}</dt><dd>{tr(mode==='quick'?'Free temporary URL':hostname)}</dd></div><div><dt>{tr("Connector")}</dt><dd><Iconname="check"size={14}/>{tr(tunnel?.connector?.version?"Installed · cloudflared {0}":"Installed",tunnel?.connector?.version?[tunnel.connector.version]:undefined)}</dd></div><div><dt>{tr("Access")}</dt><dd><Iconname="shield"size={14}/>{tr(keyReady?"API key required":"No API key: anyone with the link can use it")}</dd></div></dl><pclassName="small muted">{tr(mode==='quick'?'The address changes each time. Turn off streaming in your client; replies arrive all at once.':'Use this hostname and your Strata API key in your client. Streaming is supported.')}</p><pclassName="xs muted">{tr("Only the inference API is shared. Your computer and Strata must stay on.")}</p></>:null)}
</section>)}
{tr(error||tunnel?.error?<ErrorNoticemessage={error||tunnel?.error||'Connection failed'}next={tr("Check your internet connection, then retry.")}/>:null)}
<buttonrole="menuitem"className="menu__item"onClick={()=>{setMenu(false);voidshowOf(file,sessionId,onPreview,file.kind);}}><Iconname="panel"/>{tr("Open in sidebar")}</button>
<buttonrole="menuitem"className="menu__item"onClick={()=>voidrun(()=>fileAction.open(sessionId,file.path,'folder'),'The folder was not opened')}><Iconname="folder"/>{tr("Show in folder")}</button>
<buttonrole="menuitem"className="menu__item"onClick={()=>voidrun(async()=>{awaitnavigator.clipboard.writeText(awaitfileAction.fullPath(sessionId,file.path));pushToast('info',tr('Path copied'));},'The path was not copied')}><Iconname="copy"/>{tr("Copy path")}</button>
<buttonrole="menuitem"className="menu__item"onClick={()=>voidrun(async()=>{awaitfileAction.attach(sessionId,file.path);pushToast('info',tr('Attached to your next message'),file.name);},'The file was not attached')}><Iconname="attach"/>{tr("Attach as context")}</button>
Loaded 100 of 1056 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.