Compare commits

...
169 Commits
Author SHA1 Message Date
Niko1221 c62d5e3a15 DeepSeek plan: the NVIDIA cards it counts are the ones setup runs (a card of 8 GB or less starts Off beside a bigger one), and the plan says which card is not used 2026-10-31 03:23:01 +01:00
Niko1221 7aa9a584aa Setup: say which card is not used when a card under 8 GB sits beside a bigger NVIDIA card (a line only, no change to the cards used) 2026-10-31 03:10:24 +01:00
Niko1221 987ebd442f Launch: list the Arc Pro B65 (0xe222) and the other discrete Battlemage/Alchemist ids, so two cards are both named for a launch that names none 2026-10-31 03:10:24 +01:00
Niko1221 e4031fc4d0 AMD cards fix, review round: setup's pinned one-card pick is still written (with gpus_by), only DeepSeek on AMD is judged when a copy adopts an earlier card, the launcher uses the plan's card rule and remembers a named card, Windows uses setup's amd_rank and no Linux arch list, message names strata launch --gpu 2026-10-31 03:05:41 +01:00
Niko1221 dec8f1366c Tests for the AMD card rules (4 cards, APU, mixed architectures, Windows, --gpu, --gpus, old configs, Intel, NVIDIA unchanged); stale-stamp test stubs setup.gpus 2026-10-31 02:56:19 +01:00
Niko1221 6f7f763ce5 DeepSeek on several AMD cards uses every card when none was named: setup's own pick is no longer 'asked'; old configs of that pick move to every card at the start; the launcher and the app stop keeping it 2026-10-31 02:56:19 +01:00
Niko1221 f93ec8a924 DeepSeek card rule shared by the plan, the options and setup: every card that counts, one AMD card on Windows, only cards the engine package has code for, an APU beside a discrete card left out 2026-10-31 02:56:19 +01:00
Niko1221 5322f1c2bd NOTICE for libstrix (asf0, MIT): the gfx1151 fused-expert barrier follows its barrier() 2026-10-31 01:21:21 +01:00
Niko1221 4cf0d8cd38 Merge f-039-mac-dsu-rel (33bbbca44) 2026-10-30 22:59:51 +01:00
Niko1221 33bbbca442 Offer UD-IQ1_M on a Mac too: not run there, said plainly in its plan (the Mac gate stays as an empty mechanism) 2026-10-30 22:59:35 +01:00
Niko1221 38f56619cc Both Unsloth DeepSeek sets are offered on a DGX Spark: both passed a real Spark run (AARCH64_NOT_YET is empty); tests keep the mechanism 2026-10-30 22:50:34 +01:00
Niko1221 1a26492064 Merge f-039-mac-dsu-rel (6a7d75057) 2026-10-30 22:46:09 +01:00
Niko1221 bee469c462 serve: the qualified GDN speedups are on from the server (Linux CUDA: GDN_TC + GDN_CONVL2=2; Linux HIP gfx12 only: GDN_CHUNKED), Qwen3.8 only, explicit env wins 2026-10-30 22:46:09 +01:00
Niko1221 6a7d750578 UD-IQ1_S on a Mac: the measured line from the M5 Max run (alone: 15-18 tok/s, 3K prompt 230-310, 14K 520) 2026-10-30 22:44:19 +01:00
Niko1221 417765b8d8 tests: the Spark gate still holds both Unsloth sets 2026-10-30 22:36:49 +01:00
Niko1221 5c25767fcd Offer Unsloth UD-IQ1_S on a Mac (run on an M5 Max); UD-IQ1_M stays off until it has run 2026-10-30 22:36:26 +01:00
Niko1221 ea5dbffa7f Metal: Q6_K multi-column dense matvec (ds4_q6k_mvn) 2026-10-30 22:36:13 +01:00
Niko1221 a3a1103c13 dsu_parity: --bench and the ds4_mm pipelines 2026-10-30 22:36:13 +01:00
Niko1221 ba3d28b7e3 Q6_K matvec: packed loads via uchar4 2026-10-30 22:36:12 +01:00
Niko1221 0dba618d34 Metal Q6_K dense matvec: unaligned block loads (210-byte blocks), found by dsu_parity; test fix 2026-10-30 22:36:12 +01:00
Niko1221 5ff7da4a70 Metal tests for the Unsloth set kernels: IQ1_S matvec and tile loader, Q6_K dequant and matvec, reference checked against ggml 2026-10-30 22:36:12 +01:00
Niko1221 1b6c67122a Metal DeepSeek engine: IQ1_S expert kernels (decode GEMV, window, prompt tile) and a Q6_K dense dequant for the Unsloth sets 2026-10-30 22:36:12 +01:00
Niko1221 7851a8dab6 Merge f-038-ds-unsloth (final 4148c5125) 2026-10-30 20:26:56 +01:00
Niko1221 4148c51257 launch: ask dsv41_fit.not_here directly (the launch tests stub the setup object) 2026-10-30 20:25:25 +01:00
Niko1221 bd3c5cd4cb Keep Unsloth's 1-bit DeepSeek sets off a DGX Spark until a Spark run passes (AARCH64_NOT_YET, one rule beside the Mac one) 2026-10-30 20:23:59 +01:00
Niko1221 99866a47a2 Merge f-029-urgent (bc694233b) 2026-10-30 20:06:15 +01:00
Niko1221 73e0a3b6ed Merge f-038-ds-unsloth (final 9144da7b1) 2026-10-30 20:00:26 +01:00
Niko1221 9144da7b1a tests: the launch list's download sizes include the two Unsloth sets 2026-10-30 19:59:31 +01:00
Niko1221 a833fb46f3 Merge f-038-ds-unsloth (macOS gate 32806f82a) 2026-10-30 19:58:24 +01:00
Niko1221 32806f82aa Keep Unsloth's 1-bit DeepSeek sets off the Mac until the Metal engine has an IQ1_S kernel (lists, --model, --model-path, app check, setup) 2026-10-30 19:57:24 +01:00
Niko1221 bc694233bf Hardware list: AMD cards are counted from the kernel's KFD files when there is no setup module and no rocm-smi (ROCm from a Python venv), instead of not at all 2026-10-30 19:45:23 +01:00
Niko1221 dd3f9ec82f tests: the catalog download sizes cover the two Unsloth DeepSeek sets (241.0 and 258.4 GB) 2026-10-30 19:35:11 +01:00
Niko1221 47cbb76690 Merge f-038-ds-unsloth (current head) 2026-10-30 19:28:13 +01:00
Niko1221 ab083a693f Chat: streamed text is applied about ten times a second instead of once per token, so a long answer no longer saturates the page (main-thread time 15.7 s to 9.0 s, peak heap 114 MB to 62-75 MB for a 40,000-token answer; Mio's Out of Memory report) 2026-10-30 19:24:02 +01:00
Niko1221 ad4755c249 Say plainly that the 1-bit UD-IQ1 sets are weaker on long or careful work 2026-10-30 19:23:07 +01:00
Niko1221 b60fbb54f7 serve: the parallel stack default is Linux CUDA only (HIP and Windows get nothing: no measurement outside the replica config) 2026-10-30 19:22:21 +01:00
Niko1221 8f13b8c4ad Server: one plain line at start when a graphics card in use is of an architecture the compiled engine was not built for (#4662) 2026-10-30 19:18:45 +01:00
Niko1221 0b1808f072 Update: a failed test start of the new engine names the kept log file to report (#4692) 2026-10-30 19:18:01 +01:00
Niko1221 cfa7f737cc Setup: a compiled engine gets code for every usable NVIDIA card in the PC, not only the card setup picked (a TITAN RTX + RTX 3080 build for sm_75 only failed every long prompt with unspecified launch failure) (#4662) 2026-10-30 19:18:01 +01:00
Niko1221 b3de587e03 Merge f-036-strix-u (7f6bc3308): the Windows gfx1151 default set holds both STRATA_SELECT_WMMA and STRATA_PF_F16 (seven switches); test counts updated 2026-10-30 19:17:27 +01:00
Niko1221 7f6bc33081 gfx1151 on Windows: the prompt experts run on the F16 matrix-core kernels by default (STRATA_PF_F16)
Owner rule: a consistent Strix Halo speed gain goes on by default. Windows gfx1151, engine 0.1.54 builds, interleaved, idle box
(ab_f16def, 2 rounds, no environment on either side): IQ3_S prompts 4K 1,180 -> 1,279, 8K 1,289 -> 1,398, 32K 1,299 -> 1,404 tok/s
(x1.08); UD-IQ4_XS 8K 1,092 -> 1,223, 32K 1,135 -> 1,248 (x1.10-1.12). Decode over 200 tokens unchanged (58.6 -> 58.2, 48.8 -> 48.5),
first greedy ids identical, 38 of 38 runs exit 0. The rounding differs from the int8 kernels; KL is information only.
STRATA_PF_F16=0 turns it off. arch_defaults_test expects six switches on Windows gfx1151.
2026-10-30 19:15:54 +01:00
Niko1221 1ead709790 Control: the time-skip check and tune engine reads also tolerate non-UTF-8 output (#4675) 2026-10-30 19:12:42 +01:00
Niko1221 dd63f88fdd Qwen3.6 engine: the warm-up reads only cells that exist and starts its small probes in a fresh sequence, so a prompt chunk equal to the context no longer writes past the KV cache (#4708) 2026-10-30 19:12:21 +01:00
Niko1221 2a9ccfe3ea serve: the parallel stack default only where it was run (Linux CUDA nine switches, Linux HIP seven, nothing on Windows or macOS) 2026-10-30 19:08:30 +01:00
Niko1221 dd471d3140 serve: the exact parallel stack is the default with batch slots (explicit env still wins)
With --parallel/--batch >= 2 on a CUDA or HIP Qwen engine the server sets CKPT_POOL=1, CKPT_POOL_READY=4, SLOT_CKPT_MOVE, SLOT_COPY_DIRECT, SLOT_COPY_BATCHSYNC, BATCH_GDN_GROUPS, BATCH_ATTN_ROWS, BATCH_SELF_COMMIT and QSA_SLOTS unless the environment or the config's env already names them (=0 turns one off). DeepSeek, SYCL, Metal and Vulkan engines are untouched.
2026-10-30 19:08:30 +01:00
Niko1221 4a75fa2b62 API gateway: a client that hangs up right after data: [DONE] no longer turns a finished request into "Request cancelled." (#4690) 2026-10-30 19:07:42 +01:00
Niko1221 3282c61343 Server: a tool the conversation already called counts as declared, so a call to it comes back as a tool call, not raw text (#4663) 2026-10-30 19:06:50 +01:00
Niko1221 209dcadb3f Parser: a fence is closed only by one as long as it opened with, and an indented code block is a quote, so calls shown in them are not run (#4702 #4703) 2026-10-30 19:06:11 +01:00
Niko1221 e66a445892 Server: a picture reader written into an older engine folder follows the engine in use after an update (#4658) 2026-10-30 19:03:21 +01:00
Niko1221 68718c33fa Monitor: the drive-temperature reader (every physical drive, from the first second) runs only while the Monitor is open, not at app start (#4661) 2026-10-30 19:02:16 +01:00
Niko1221 ea52e55cd7 Chat: only the streaming message renders again on each token, so a long chat no longer redoes its whole history per token (Out of Memory report from Mio; heap and main-thread time measured) 2026-10-30 18:58:34 +01:00
Niko1221 08e8770c6e Prefill (layer-major): an expert with no mapped blob is copied by its source instead of ending the request (#4695) 2026-10-30 18:46:16 +01:00
Niko1221 7146d6983f Setup, Control and Terminal: a child's non-UTF-8 output (GBK adapter names) no longer kills Step 1 or launch (#4675) 2026-10-30 18:44:41 +01:00
Niko1221 01ab6b51bb Merge f-036-strix-ds (957038a1c) 2026-10-30 18:43:54 +01:00
Niko1221 957038a1cd dsv41 (Windows HIP, integrated GPU): the shards' views are dropped after the load by default (DS4_NO_VIEW=0 turns it off, =1 forces it elsewhere)
Unbuffered reads of a file that has a view in the process run at half the drive's rate on Windows (probe on Strix Halo: 6.5 GB/s with no view, 3.2 with one). Default on for an integrated GPU, where the CPU lane's misses read through the store's slots. Decode 9.1 -> 9.4-9.7 tok/s (code, drafter off, 2 interleaved pairs); the 32K prompt is unchanged; no failed reads in any run.
2026-10-30 18:43:29 +01:00
Niko1221 d340dc8dd1 apps/host test: planning a whole-drive models folder no longer deletes <drive>\Strata (#4689) 2026-10-30 18:43:11 +01:00
Niko1221 ffaa6ad78c tests: the app preview and launch list know the two Unsloth sets 2026-10-30 18:41:24 +01:00
Niko1221 4006305476 tests: the lockfile versions are 0.2.9 2026-10-30 18:40:48 +01:00
Niko1221 97e6f1fe19 Version 0.2.9: Terminal and app 0.2.9, engine and Control 0.1.55 (every Qwen and DeepSeek package, Metal and SYCL included) 2026-10-30 18:40:46 +01:00
Niko1221 33bd74182b Merge f-038-ds-unsloth 2026-10-30 18:40:13 +01:00
Niko1221 4d12701181 Merge f-0281-ds 2026-10-30 18:40:13 +01:00
Niko1221 aed382885e Merge f-036-strix-ds 2026-10-30 18:40:12 +01:00
Niko1221 274257630b Merge f-036-strix-u 2026-10-30 18:40:12 +01:00
Niko1221 d96873fc56 Merge f-036-strix-q 2026-10-30 18:40:11 +01:00
Niko1221 86bed1c92f Merge f-037-arc (5eaee81a4) 2026-10-30 18:40:11 +01:00
Niko1221 203df526f1 serve/test_admit_order: the engine stand-in sets the arrivals deque the coalescing code reads 2026-10-30 18:39:58 +01:00
Niko1221 b6fc410ee5 Metal window.cpp: qualify NG_HC_DIM / NG_HIST in finish_rows (the --batch rows code did not compile in the host build) 2026-10-30 18:39:58 +01:00
Niko1221 0a76d62e68 pinned.cu: the shared-arena tmpfs check is Linux-only (sys/vfs.h and statfs f_type), so the Metal host build compiles again 2026-10-30 18:39:58 +01:00
Niko1221 c0e245c8be Merge f-029-par-metal (766b54afb) 2026-10-30 18:39:27 +01:00
Niko1221 16e08b50cd Merge f-029-par-amd (4772f0cd9) 2026-10-30 18:39:27 +01:00
Niko1221 1fb3e9a326 Merge f-029-par-harness 2026-10-30 18:37:54 +01:00
Niko1221 1f8f2fa8ad Merge f-029-par-big 2026-10-30 18:37:53 +01:00
Niko1221 3a76d5eaf6 Merge f-029-gdn-mx 2026-10-30 18:37:53 +01:00
Niko1221 01ef876562 Merge f-029-gdn-amd 2026-10-30 18:37:53 +01:00
Niko1221 4746bfb165 Merge f-029-gdn-cuda 2026-10-30 18:37:52 +01:00
Niko1221 0b1fbf4012 gfx1151 on Windows: STRATA_SELECT_WMMA joins the default prompt set (x1.008 at 8K, x1.037 at 32K, x1.077 at 64K; about 10 of 2,051 selected cells flip at the cutoff for queries past ~5K tokens) 2026-10-30 18:22:35 +01:00
Niko1221 6923421a61 Accept Unsloth's DeepSeek UD-IQ1_S and UD-IQ1_M sets (pins, models, app file check, fit numbers; opt-in names) 2026-10-30 18:05:31 +01:00
Niko1221 09dc5c52b3 gfx11 gr_upmix: 256 tokens a block (16 waves) halves the w_up staging per token; prefill timing splits the QSA projection stage 2026-10-30 17:55:46 +01:00
Niko1221 5eaee81a43 [SYCL] kprof: profiling-tag sections time stretches without events of their own (the oneMKL GEMMs) 2026-10-30 16:33:01 +01:00
Niko1221 d936db6547 gfx1151: a Q8_0 down expert layer takes the F16 matrix-core prompt kernel by default
UD-IQ4_XS and UD-Q4_K_XL keep five layers with Q8_0 down experts; without STRATA_PF_F16 they left the fused path for MMQ.
On those layers only the F16 kernel now runs (STRATA_PF_Q8D_F16=0 turns it off); every other format keeps its kernel.
UD-IQ4_XS on Windows gfx1151, interleaved x3: 4K prompt 1,071 -> 1,120 tok/s, 8K 1,136 -> 1,180, same output ids;
IQ3_S unchanged (1,190 -> 1,186, 1,286 -> 1,284).
2026-10-30 16:15:54 +01:00
Niko1221 ac3e0e17b5 GDN gfx12: the chunked path from 64 tokens, as on gfx11 (owner: every gain counts; final gate at 64: +1.7% to +6.1%, KL 0.0025/0.038, 55/61 tasks, soak clean) 2026-10-30 16:05:14 +01:00
Niko1221 b3bc477512 diagnostics: STRATA_QSA_STAT (overlap of consecutive queries' selections), hip_pa_bench target and PA_BENCH_REF (compare two variants of the prompt attention on a recorded selection) 2026-10-30 15:41:53 +01:00
Niko1221 db729163ce Revert "setup: a Strix Halo's BIOS carve-out holds expert slots of the Unsloth RAM-budget plans"
This reverts commit 94fc01c6fd.
2026-10-30 15:41:41 +01:00
Niko1221 19b5f873f2 dsv41 (Windows): DS4_NO_VIEW=1 drops every view of the shards and the load's buffered handles after the load, reads go unbuffered
A file with a view (or a buffered handle with cached reads) in the process halves unbuffered read speed on Windows (probe: 6.5 GB/s with no view, 3.2 with a view of the whole file or of 1 GB of it, 3.3 with a buffered handle held open, 6.7 on a copy while the original is mapped). The CPU lane reads misses through the store's slots, so the views are not needed after the load: Pack::drop_views() unmaps them, reopens never-read fallback handles and Pack::read_at goes unbuffered. StoreHold re-reads an expert that has no slot instead of falling back to the view.
Also: DS4_IO_STATS (demand read queue/read/copy times), DS4_IO_PRIO, DS4_READ_SPLIT (default 1, off; deeper queues did not help). Decode 8.9 -> 9.6 tok/s (code, drafter off).
2026-10-30 14:56:43 +01:00
Niko1221 0122eceb4c [SYCL] --batch-groups: 'auto' is understood, and two Arc B-series cards with 8 or more slots default to 2 pipelined groups (INFO batch_groups); STRATA_BATCH_GROUPS_AUTO=0 / --batch-groups 1 turn it off 2026-10-30 13:56:52 +01:00
Niko1221 8401c21f9d [SYCL] prompt attention on the int8 DPAS (ESIMD, Battlemage): hi/lo int8 Q and P, 2D block loads for the four-cell selection blocks; default on for Battlemage, STRATA_PROMPT_ATTN_ESIMD=0 off 2026-10-30 13:22:33 +01:00
Niko1221 9c2121cf5c dsv41: the CPU lane's service thread spins through a decode token (DS4_LANE_SPIN_MS, 250) and naps on a high-resolution timer; DS4_PROF_SYNC decode stage profile; DS4_LANE_SKIP timing oracle
The service thread spun 2 ms after its last answer; a Strix Halo decode layer takes ~2.8 ms, so it slept (Windows sleep_for(20 us) wakes after ~1 ms+) before almost every request.
Decode 8.2 -> 9.3 tok/s on pack 2.25 (2 interleaved pairs). DS4_PROF in decode and in the prompt hangs the HIP driver on Windows; DS4_PROF_SYNC drains the stream at each decode stage mark instead.
2026-10-30 12:26:18 +01:00
Niko1221 1cfffa2ede gfx1151 fused experts: one barrier per stage (explicit LDS wait, after libstrix's barrier(), asf0/libstrix MIT) and 3 blocks per WGP for IQ2_S/IQ2_XS gate/up on gfx11 2026-10-30 12:14:41 +01:00
Niko1221 a83252a7b7 gfx1151: the VGPR cap of the occupancy expert kernels applies on clang 24 as on clang 23 (IQ3_S/IQ4_NL layer 79.7 -> 69.9 ms at 16K tokens, parity unchanged) 2026-10-30 11:40:16 +01:00
Niko1221 d8056fbe00 [SYCL] STRATA_KPROF=1: per-kernel GPU timer (event-profiled queues, kp() launch wrapper, decode/prompt tables); tools/arc A/B harness 2026-10-30 11:27:59 +01:00
Niko1221 6fb110e557 Opt-in research switches: STRATA_PAR_COUNTERS, STRATA_PIPED_CONCURRENT_MIN, multi-prompt layer-major read (Prefill::run_multi, BCO co-reads, server STRATA_COALESCE_PREFILL), STRATA_CRASH_BT 2026-10-30 11:08:53 +01:00
Niko1221 28c4543e48 tools: parallelism load harness (par_load, par_analyze, par_run, sweep, recon), E00 capability probe, batch_test --co/--seq/--pad and the phase-1/2 variant lists 2026-10-30 11:08:53 +01:00
Niko1221 e1e7e32680 prefill timing: STRATA_PREFILL_TIMING also splits the hyper-connection read into down, up, inject and mix 2026-10-30 11:03:01 +01:00
Niko1221 94fc01c6fd setup: a Strix Halo's BIOS carve-out holds expert slots of the Unsloth RAM-budget plans
uma_expert_cache counted only the RAM the OS sees, so UD-IQ4_XS (55 GiB of experts) and UD-Q4_K_XL (72 GiB) on a PC with a
64 GB carve-out and 64 GB seen by Windows got no cache slots beside the 40 GiB budget and read the rest from the SSD.
The carve-out (less 14 GB for the dense weights and buffers) now adds its slots. Tests added.
2026-10-30 11:00:42 +01:00
Niko1221 b0ba459514 Merge f-029-par-sycl (SYCL batch windows, GDN/attn rows kernels) 2026-10-30 10:56:29 +01:00
Niko1221 2fef430202 Merge f-034-cand (0.2.9 candidate) into f-036-strix 2026-10-30 10:52:31 +01:00
Niko1221 7a9245b6c0 [SYCL] batch window: port the one-launch-for-all-rows kernels (GDN groups, block scores rows, decode attention rows; opt-in STRATA_BATCH_GDN_GROUPS / STRATA_BATCH_ATTN_ROWS) and the batch-window stage profile; no speed gain on 2x Arc B65 2026-10-30 08:25:37 +01:00
Niko1221 368bcb7a26 serve/test_admit_order: the engine stand-in sets the arrivals deque the coalescing code reads 2026-10-30 07:32:39 +01:00
Niko1221 5ad0135c00 Metal window.cpp: qualify NG_HC_DIM / NG_HIST in finish_rows (the --batch rows code did not compile in the host build) 2026-10-30 07:32:39 +01:00
Niko1221 cdb5470c70 pinned.cu: the shared-arena tmpfs check is Linux-only (sys/vfs.h and statfs f_type), so the Metal host build compiles again 2026-10-30 07:32:39 +01:00
Niko1221 87970c8ae0 Merge f-029-par-metal (766b54afb) 2026-10-30 07:32:39 +01:00
Niko1221 777d11f8a3 Merge f-029-par-amd (4772f0cd9) 2026-10-30 07:32:39 +01:00
Niko1221 dcc79c5230 Merge f-029-par-harness (9b5965c3c) 2026-10-30 07:32:32 +01:00
Niko1221 db0606b4f8 Merge f-029-par-big (714438ec1) 2026-10-30 07:32:31 +01:00
Niko1221 4bfd66686e Merge f-029-gdn-mx (348457223) 2026-10-30 07:32:31 +01:00
Niko1221 5c00af8a1f Merge f-029-gdn-amd (f0a34fa9b) 2026-10-30 07:32:31 +01:00
Niko1221 78859bbcb1 Merge f-029-gdn-cuda (51567de1a) 2026-10-30 07:32:30 +01:00
Niko1221 9b5965c3c1 z_ctx 2026-10-30 07:32:03 +01:00
Niko1221 836dd2d86e sweep: server flags column; stacked-candidate variant lists 2026-10-30 07:21:57 +01:00
Niko1221 6d7255f205 z_pool 2026-10-30 07:20:46 +01:00
Niko1221 97bbd76c92 tools/par/overhead.sh (counters overhead A/B) 2026-10-30 07:20:14 +01:00
Niko1221 36e798f4c5 Opt-in research switches: STRATA_PAR_COUNTERS, STRATA_PIPED_CONCURRENT_MIN, multi-prompt layer-major read (Prefill::run_multi, BCO co-reads, server STRATA_COALESCE_PREFILL), STRATA_CRASH_BT 2026-10-30 07:19:55 +01:00
Niko1221 1510e6d6d2 tools: parallelism load harness (par_load, par_analyze, par_run, sweep), E00 capability probe and the phase-1/2 variant lists 2026-10-30 07:19:55 +01:00
Niko1221 4772f0cd9a co-read adaptive: look 10 ms for the burst's other arrivals before deciding 2026-10-30 06:21:21 +01:00
Niko1221 7aafac422c shared arena on Windows: a named pagefile-backed section (CreateFileMapping/MapViewOfFile) with the same header and load state word, pid election on both OSes, server option enabled, pinned_shared_win_test 2026-10-30 06:02:30 +01:00
Niko1221 62b042e843 co-read: adaptive - the leader waits and the read is shared only when the engine has seen at least 3 requests arrive together (STRATA_COALESCE_MIN_REQS, STRATA_COALESCE_BURST_MS) 2026-10-30 05:58:22 +01:00
Niko1221 9d8337844e replicas: the memfd arena warns when the RAM will not hold all the engines 2026-10-30 05:24:08 +01:00
Niko1221 714438ec19 GDN TC: two value tiles per head on cards with >= 96 SMs (STRATA_GDN_TC_TILES), harness --tiles 2026-10-30 05:12:40 +01:00
Niko1221 d3af2d42ad GDN: single-pass conv+SiLU+L2 (STRATA_GDN_CONVL2=2), output-only reduced-precision experiment mode (STRATA_GDN_TC_NP=2) 2026-10-30 05:12:40 +01:00
Niko1221 1a865fb3db GDN TC: fused output norm for the split scan, NP experiment variants (STRATA_GDN_TC_NP), STRATA_GDN_TC_FUSE 2026-10-30 05:12:40 +01:00
Niko1221 a0e7f5160d GDN harness: tc any_sms bypass for multi-wave tests, output hash 2026-10-30 05:12:40 +01:00
Niko1221 dc149ef003 GDN: production-linked parity/bench harness, dispatch probe, FP64 fixtures, contract doc; opt-in STRATA_GDN_TC on 99-KB cards (split v/T/P buffering) 2026-10-30 05:12:40 +01:00
Niko1221 9f1d0ad041 replicas: a replica that fails to start beside a living one is started again on the same shared arena before the sharing is given up 2026-10-30 05:07:50 +01:00
Niko1221 1064ff7783 STRATA_COALESCE_WAIT_MS 2026-10-30 04:52:38 +01:00
Niko1221 301a09f100 STRATA_PREFILL_LM_FORCE (research reference for the multi-prompt read) 2026-10-30 04:52:38 +01:00
Niko1221 4f88a2fa8b multi-prompt read: relaxed stage-chunk rule, sub-chunks at the smallest stage chunk; gated identity variants 2026-10-30 04:52:37 +01:00
Niko1221 1f05fdc283 [Engine] CUDA hyper-connection read (v3): the transposed warp sums also for 5..7-token launches 2026-10-30 04:52:35 +01:00
Niko1221 a8fba1f5b7 multi-prompt PLE: tables, constants and weights from the main session, the history of the prompt's own 2026-10-30 04:52:32 +01:00
Niko1221 2f0d6a1c15 replica profile (opt-in): setup --replica-profile picks one engine per card + shared expert arena + the host stack when the RAM and VRAM check passes 2026-10-30 04:51:14 +01:00
Niko1221 42aee7a349 [Engine] CUDA hyper-connection read (v3), 8-token launches: the eight warp sums in one transposed butterfly (same additions, same bits; decode c=8 +2%) 2026-10-30 04:47:30 +01:00
Niko1221 3782008944 shared arena: memfd backing (not limited by the /dev/shm size), one engine reads the experts and the others skip the read, RAM check before the pages are taken 2026-10-30 04:46:51 +01:00
Niko1221 05cd12e7a2 [Engine] batch window: STRATA_BATCH_GDN_GROUPS now also works with STRATA_BATCH_SELF_COMMIT (the grouped GDN launch keeps its one-row groups itself) 2026-10-30 04:19:00 +01:00
Niko1221 0c94851835 shared arena: the header is written before any engine reserves pages, an importer waits for a header still being written, and every engine reserves the tmpfs pages 2026-10-30 04:13:24 +01:00
Niko1221 21f950b4b4 [Engine] CUDA hyper-connection read (v3): exact 5..8-token down kernels with two rows per warp (same dots, same bits; decode c=8 +5%) 2026-10-30 03:56:23 +01:00
Niko1221 3ccfd5aed3 replicas: opt-in shared expert arena (one /dev/shm file for all replicas, stale-file sweep, private fallback); the engine reserves a tmpfs arena up front 2026-10-30 03:50:58 +01:00
Niko1221 51567de1a6 GDN TC: two value tiles per head on cards with >= 96 SMs (STRATA_GDN_TC_TILES), harness --tiles 2026-10-30 03:46:18 +01:00
Niko1221 f0a34fa9bf kGdnW12MinT comment: the 8K figure 2026-10-30 03:46:05 +01:00
Niko1221 84aa2d7d57 GDN gfx12: chunked path only from calls of 12000 tokens (measured crossover for the +5% bar); p2006/KL scripts 2026-10-30 03:45:42 +01:00
Niko1221 1d9b4c48e9 prefill: STRATA_LM_HIP=1 opts the layer-major (and multi-prompt) read in on HIP builds 2026-10-30 03:21:45 +01:00
Niko1221 2072510a47 [Engine] --batch on one GPU: opt-in concurrent prompt read beside the decoding slots (STRATA_BATCH_CONCURRENT_READ) 2026-10-30 03:10:51 +01:00
Niko1221 0ea3e455fd GDN: single-pass conv+SiLU+L2 (STRATA_GDN_CONVL2=2), output-only reduced-precision experiment mode (STRATA_GDN_TC_NP=2) 2026-10-30 03:05:51 +01:00
Niko1221 a9f154e82b run_multi: layer-major even for a short total 2026-10-30 03:02:51 +01:00
Niko1221 766b54afb1 [Engine] Metal: --batch N / "parallel": N runs, and the slot rows share one window
- the pdl capability queries answer false instead of aborting the engine (#3981, #2715)
- each batch slot keeps its own running state in the Metal window (GDN state and conv history, QSA K/V, indexer tail and pooled keys,
  PLE history), side by side in bigger buffers; the slot copy, the checkpoints and session_zero follow the session they are for
- a batch window holds one token of each active slot: the GDN step, the QSA kernels and the PLE conv take a per-row slot, cell and
  history parity (function constants, off for every other caller), so the dense and expert weights are read once for all rows
- STRATA_METAL_BATCH_ROWS=0 runs the slots one window each instead (diagnosis)
2026-10-30 02:58:46 +01:00
Niko1221 faa1be268c co-read: server side (STRATA_COALESCE_PREFILL), par_load --keep-text, cmp_texts 2026-10-30 02:53:32 +01:00
Niko1221 47a1047750 WIP multi-prompt layer-major prefill (Prefill::run_multi) and BCO co-reads in the batch admission 2026-10-30 02:53:25 +01:00
Niko1221 ee05114846 GDN TC: fused output norm for the split scan, NP experiment variants (STRATA_GDN_TC_NP), STRATA_GDN_TC_FUSE 2026-10-30 02:12:18 +01:00
Niko1221 d8c94b45c8 [Engine] CUDA prompt path: opt-in FP16 cache of dequantized dense weights (STRATA_PF_W16_CACHE_MIB) 2026-10-30 02:11:45 +01:00
Niko1221 236adb3987 GDN gfx12: conv + SiLU + q/k norm written as FP16 and read by the chunked kernels (one entry, STRATA_GDN_FUSE=0 off); ablation hooks removed 2026-10-30 01:51:05 +01:00
Niko1221 3484572236 Metal: tensor-op chunk-state probe, layer bench with before/after, A/B driver 2026-10-30 01:50:15 +01:00
Niko1221 e0cc4e859e GDN harness: tc any_sms bypass for multi-wave tests, output hash 2026-10-30 01:47:46 +01:00
Niko1221 cea3057e2c Metal: GDN chunked v2 (measured, slower), M5 GEMM tiles + fused GDN input projections (bit-identical), layer bench 2026-10-30 01:42:07 +01:00
Niko1221 5e9995d296 GDN gfx12: LDS-only barrier in the chunked scan; conv + SiLU + q/k norm folded into the chunked kernels (opt-in STRATA_GDN_CHUNKED=1, STRATA_GDN_FUSE=0 off), bench check mode 2026-10-30 01:33:37 +01:00
Niko1221 095464152c Metal: GDN prompt scan chosen per chip (M4 scanr_4_8, M5 Max scanr_4_4 from T 4096), bit-identical 2026-10-30 01:24:05 +01:00
Niko1221 2cf3fd181f Metal: gdn_scan_f (scan + gdn_post fused, bit-identical, slower: kept as a measured negative), bench 2026-10-30 01:22:28 +01:00
Niko1221 06f212aefe GDN: production-linked parity/bench harness, dispatch probe, FP64 fixtures, contract doc; opt-in STRATA_GDN_TC on 99-KB cards (split v/T/P buffering) 2026-10-30 01:12:19 +01:00
Niko1221 4d9d9cefd3 GDN: the chunked WMMA recurrence on gfx12 (opt-in STRATA_GDN_CHUNKED=1), gfx12 fragment layout 2026-10-30 00:59:13 +01:00
Niko1221 f34ab1c8f5 GDN gfx11 WMMA: compile the device pass for gfx1103 too (the runtime gate lists it; the pass trapped there) 2026-10-30 00:55:55 +01:00
Niko1221 280885fda0 Metal: GDN prompt scan benchmark (sequential vs chunked), extra gdn_scanr TB instantiations, opt-in STRATA_METAL_GDN_SCAN 2026-10-30 00:49:57 +01:00
Niko1221 ae2657a19d gdn_stage_bench: fix auto deduction 2026-10-30 00:42:26 +01:00
Niko1221 d4f15adf87 tests/hip: gdn_stage_bench, the GDN prompt stage (gates, conv, recurrence) per piece with output and state hashes 2026-10-30 00:39:06 +01:00
Niko1221 dbdec0bb9e Control 0.1.55 and Terminal 0.2.8.1 (Control-only release: the app stays 0.2.8, the engine packages 0.1.54) 2026-10-30 00:32:32 +01:00
Niko1221 d7cb41ba87 DeepSeek local GGUF: accept both layouts of the IST-DAS sets, the pinned four-shard one and ISTA-DASLab's six-shard one (owner decision, option B)
The download default stays on the four-shard pin cb02422c; nothing is fetched under the new names. The six-shard files (Hub commit a419d74c,
27 October: five shards a set, the Engram as the sixth) and the four-shard files with the first shard ISTA re-published on 26 October (what the
Hub's last four-shard commit 653a9874 serves; #4237 was refused by the app because only the pinned size of shard 1 was known) are taken from
files already here, by size and SHA-256.

- setup.py: DSV41_ISTA6_FILES (15 files), shards6 on the three sets, dsv41_layout_of / dsv41_parts / dsv41_pack_layout; install_dsv41 picks the
  layout of a --gguf-dir folder (both whole: the pinned one), packs the five files with the Engram as --engram, and builds the pack again when it
  was made from the other layout's files. The converted Engram (same name and size, another hash) is accepted beside the pinned one.
- strata_control/dsv41_sets.py, models.py: the app's check and `strata models import` know the six-shard names, the re-published shard 1, and
  that a folder with both whole layouts of one set is that set once.
- strata_cli/launch: --model-path and the guided screen name what is missing in the layout that is there.
- tools/dsv41/pack_gguf.py: --engram as the sixth shard of a six-shard split; the pinned release's Engram serves the six-shard shards too.
- Tests for each; docs/DEEPSEEK41.md.

Builds on the cherry-picks of #4348 (integer tensors in the GGUF reader) and #4632 (StackArc3's six-shard work, option A), which this
commit reworks so the four-shard pin and files keep working unchanged.

Related: #4237, #4256, #4632.
2026-10-30 00:32:32 +01:00
StackArc3 c4e975cc7a Accept ISTA-DASLab's six-shard DeepSeek V4.1 Flash GGUF layout
On 27 October 2026 ISTA-DASLab replaced the four-shard IST-DAS builds on
the Hub's main with six-shard builds (llama.cpp b11870+ layout), and
--model-path with those files failed with "...-00001-of-00004.gguf is
missing". The v0.2.7.1 pin still serves the old files.

- Pin a419d74c: five shards per build named of-00006 and the shared
  Engram as the sixth. The Engram was converted in place (same name and
  size, new hash).
- pack_gguf.py counts an --engram file as the sixth shard only if it is
  one file, not already among the shards, number count-1 of the same
  split, and holds the single tensor per_layer_token_embd.weight.
- An Engram whose finish mark names an old hash is fetched again in the
  same run. One whose mark has no hash is hashed: a match is kept and
  marked, a mismatch is fetched again. A user's own old one is refused
  with the reason.
- A pack from an earlier run is rebuilt unless it names this run's files.
- Six-shard only; four-shard files are ignored and stay on disk. The
  first-release name tables are now empty. No engine code changes.

Related: #4237, #4256.
2026-10-30 00:16:56 +01:00
lu123bing c15a90b28c gguf reader: size the integer tensors, so a complete DeepSeek first shard is accepted and a cut one refused
BLOCK_GEOMETRY had no row for I8/I16/I32/I64, which GGML_TYPES names and which IST-DASLab's DeepSeek-V4.1
first shard uses for the Engram hash parameters (tools/dsv41/pack_gguf.py). The preflight in front of setup
and the app reads "no geometry" as "unsupported file" (strata_control/gguf_preflight.py:165), so a complete
file was refused as "cut short or another version" or as "a format Strata does not read" (#4256).
setup.check_shards sized such a tensor at 0 bytes (setup.py:3324), so a shard cut inside one passed as whole.

The rows are the ones tools/dsv41/ista_intake.py:112 already uses for these types.
2026-10-30 00:08:56 +01:00
Niko1221 b2dbd9420d dsv41 serve: the cache settle runs in the idle between requests (WP-8's option 1)
The prompt-side settle is 800 ms because it sits inside the prompt's own time, and for a run with no next request
(the CLI, a one-shot tool call) that is the right price.  In a server it is not: an agent's turn ends, it runs its
tool for a second or ten, and that time is free for the swap rounds the *next* answer needs.

Measured why it matters (job d18-attrib, 2026-10-28): the same candidate engine with the settle at 6,000 ms
decodes a 256-token code answer at 10.92 / 11.14 tok/s, at 800 ms at 9.82 / 9.62 - 12% paid for the prompt.
The idle settle is meant to give that back without returning the 6 s to the prompt.

How: Model::settle_idle(cancel) runs cache_settle's swap rounds on its own thread, without the RAM store preload,
for at most DS4_SETTLE_IDLE_MS (6,000) and stopping between rounds when cancel is set.  The serve loop starts it
at the top of each iteration (nothing in flight), and cancels and joins it the moment a line arrives, before any
request work touches the cache.  DS4_SETTLE_IDLE=0 turns it off; the prompt-side budget is unchanged.

Not measured yet: the benefit needs a serve-path harness (two GEN requests with an idle between them).  On the CLI
the thread is never started, so no CLI number moves.
2026-10-28 17:00:25 +01:00
Niko1221 c697e859e9 dsv41: the matrix-core prompt attention (DS4_FAST_ATTN) on by default for gfx1151 on Windows
The switch was on for gfx12 only, and ds4_kernels.cu said why: "gfx11 and gfx1151 (Strix Halo) are untested
and keep the scalar kernels".  Measured on Strix Halo 2026-10-28 (2.00 bpw, 32K prompt, the synchronised
chunk profile): the sparse-attention stage is 43.3% of the prompt's GPU time and runs at 2.3 TFLOPS
effective; with DS4_FAST_ATTN=1 the stage falls from 38,340 ms to 6,976 ms and the prompt from 101.6 s to
70.8 s - +43.6%, 322.5 -> 463.0 tok/s - with the same greedy first token (27408,).

The plan's D-P3a had closed this on +1.6 / -0.2 / +1.2 / -2.8% end to end, which is what 4K-8K shows; at
32K the term is five times its 4K size and the switch is the whole difference.

fast_attn_default() takes the gfx1151-on-Windows flag as its own argument (the existing three-argument calls
in the test are updated), ds4_kernels.cu decides it from the device name once, and experts_rule_test covers
the new cell: gfx1151 Windows on, 0.2.6 arithmetic off, DS4_FAST_ATTN=0 off, =1 on anywhere.
2026-10-28 14:04:56 +01:00
Niko1221 2ea46ded73 dsv41: the post-prompt cache settle's budget 6,000 -> 800 ms on every platform
The settle sits inside the prompt's own time: prompt: N tokens in X ms includes it, so a 6 s budget was
6 s of every DeepSeek first token.  Measured on Strix Halo 2026-10-28 (p4k fixture, three interleaved rounds
each, --dspark off): 6,000 ms -> 153.1 tok/s, 800 ms -> 190.0 (+24.1%), 0 -> 197.4 (+29.0%); the decode of
the 256-token answer that follows pays 2.9% at 800 ms and 3.8% at 0.  800 ms keeps 1,412-1,436 swaps against
the 6,000's 987-1,046, so it is still moving the hot experts in, it just stops paying for the last 5 s.

An agent turn's answer is 50-300 tokens (where the swaps buy nothing) and its first token is the 800 ms, so
this is the plan's WP-8 option (2) without the server-loop work of the idle settle (option 1, still the better
answer and not done here).

DS4_SETTLE_MS still overrides it (0 = off, any value in ms).
2026-10-28 14:04:52 +01:00
Niko1221 20bba22af7 gfx1151 on Windows: a 16,384-token prompt chunk, and the docs for the two new prompt defaults
- src/program/generate.cpp: the gfx1151 Windows prompt chunk goes from 8,192 to 16,384.  The prompt reads every expert once per
  chunk, so a bigger chunk reads them fewer times: 32K 1,250.6-1,260.0 -> 1,286.3-1,307.9 tok/s, 64K 1,188.4-1,191.0 ->
  1,234.3-1,238.2 (+3.0% / +3.9%, interleaved pairs); at 4K and 8K the chunk is the prompt and nothing changes.  The buffers'
  reserve grows to ~10.9 GiB and the cache keeps all 24,576 slots (84.7 GiB free at a 64K context).  STRATA_GFX1151_CHUNK=0
  keeps 6,144; auto:32768 is another ~2% at 64K and stays opt-in.
- docs/STRIX_HALO_WINDOWS.md section 4: what 0.2.8 turns on for gfx1151 on Windows (the five-switch matrix-core set and the
  prompt keeping its own buffers), the measured prompt table with the opt-outs, the chunk.
- docs/STRIX_HALO.md section 5: the Windows gfx1151 exception to 'the set is opt-in on an integrated Radeon'.

Numbers: Qwen3.8-Flash-Next IQ3_S, engine 0.1.52, prompts 4K to 64K, x1.59 to x1.72 over 0.2.7; first-token KL 0.0014 mean
(bar 0.0228); the own-buffer path is bit-exact (KL 0.00000 against the lending engine with the same switches).
2026-10-28 11:15:18 +01:00
Niko1221 1493781e91 gfx1151 on Windows: the matrix-core prompt set by default, and the prompt keeps its own buffers
Two prompt defaults for Strix Halo (gfx1151) on Windows, measured on the strix box with the
0.1.52 engine and Qwen3.8-Flash-Next IQ3_S (ids prompts of 4,095 / 8,190 / 11,999 tokens,
interleaved pairs, one engine at a time):

1. The matrix-core prompt set (HIP_WMMA, PF_GEMM, PA_FAST, HC_UPMIX, PF_FUSED) is on by
   default, as it has been on the Radeon 780M since 021-minipc: 4K 688.8 -> 1,192.8 tok/s,
   8K 787.0 -> 1,283.5, 12K 750.4 -> 1,256.5 (x1.63 to x1.72).  SELECT_WMMA stays out
   (x1.003 at 8K on the f-024-strix-pp branch); PF_F16 was measured and adds nothing on
   this pack, so the bit-changing surface is the five switches the branch gated.
   First-token KL against the 0.2.7 default 0.00210 / 0.00039 / 0.00214 at 4K / 8K / 12K,
   top-1 the same, noise floor 0.00000 (the accepted HC_Q8 bar is 0.0228 mean).

2. The prompt path keeps its own buffers instead of lending expert-cache slots.  A lent
   slot's expert is re-read from the host arena during every chunk at ~0.5 GB/s here (the
   arena is unregistered in UMA mode): 823 slots = 2.75 s of a 3.3 s 4K prompt in the
   issuer thread.  With own buffers every expert stays resident (24,576 slots, 'experts
   streamed 0') and the prompt is 8.5% (4K) to 8.9% (8K) faster.  STRATA_PREFILL_BORROW=1
   keeps 0.2.7's lending, STRATA_GFX1151_DEFAULTS=0 the whole block; --prefill auto keeps
   the 8,192-token chunk the gfx1151 rule chose instead of falling back to 2,048.

arch_defaults_test: the Windows gfx1151 expectations and the new opt-outs.
2026-10-28 11:15:18 +01:00
283 changed files with 12764 additions and 1157 deletions

No files matched your search

+1 -1
View File
@@ -66,7 +66,7 @@ body:
attributes:
label: Strata version
description: "In the app: Settings > About. In the Terminal: `strata --version`. From source: the About tab of the web page, or `engine` in `GET /v1/status`, or the git commit."
placeholder: "Strata 0.2.8 · engine 0.1.54"
placeholder: "Strata 0.2.9 · engine 0.1.55"
validations:
required: true
- type: input
+2 -2
View File
@@ -1,6 +1,6 @@
# AGENTS.md
Version 0.2.8, engine 0.1.54. The public model families are DeepSeekV4.1-Flash-1017,
Version 0.2.9, engine 0.1.55. 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
@@ -15,7 +15,7 @@ Strata has three parts:
- **The app** (`apps/desktop`, the Tauri shell; `apps/host`, the Node host and the web UI). It is for people who do
not want a terminal. One installer per system ([latest release](https://github.com/Niko1221/Strata/releases/latest)):
`Strata-0.2.8-windows-x64-setup.exe`, `Strata-0.2.8-linux-x86_64.AppImage`, `Strata-0.2.8-macos-arm64.dmg`.
`Strata-0.2.9-windows-x64-setup.exe`, `Strata-0.2.9-linux-x86_64.AppImage`, `Strata-0.2.9-macos-arm64.dmg`.
The Windows and Linux installers carry no engine: the app downloads the engine for the graphics card on first
start, or installs a file the user already has. The Mac dmg keeps its small Metal engine inside.
- **The Terminal** (`strata.py`, `strata_cli/`, `strata_control/`): the `strata` command. It sets up a model, serves
+29 -1
View File
@@ -8,7 +8,7 @@
# below refuses anything older: silently building for a pre-Ampere arch would produce a binary that runs
# and is wrong about its own performance, which is the worst of both outcomes.
cmake_minimum_required(VERSION 3.24)
project(strata VERSION 0.1.54 LANGUAGES CXX)
project(strata VERSION 0.1.55 LANGUAGES CXX)
# EXPERIMENTAL: the Intel Arc engine, the SYCL port in sycl/ (PR #423; icpx + oneMKL, docs/INTEL_ARC.md). It is its
# own project that takes the unported sources from src/; with this on, nothing below is configured.
option(STRATA_ENABLE_SYCL "EXPERIMENTAL: build the Intel Arc (SYCL) engine from sycl/ - docs/INTEL_ARC.md" OFF)
@@ -1076,6 +1076,10 @@ if(STRATA_ENABLE_CUDA OR STRATA_ENABLE_HIP)
add_test(NAME hip_gdn_chunked_off COMMAND hip_gdn_chunked --off)
add_test(NAME hip_gdn_chunked_default COMMAND hip_gdn_chunked --default)
list(APPEND _strata_hip_tests hip_gdn_chunked hip_gdn_chunked_off hip_gdn_chunked_default)
# GDN prompt stage bench (gates + conv + recurrence, per-piece times and output/state hashes at 4K/16K/32K); a measurement
# tool, run by hand with the STRATA_GDN_* knobs under test - no add_test
add_executable(hip_gdn_stage_bench tests/hip/gdn_stage_bench.cpp)
target_link_libraries(hip_gdn_stage_bench PRIVATE strata_prefill strata_hip_runtime)
# #313: the RDNA3 / RDNA3.5 WMMA dense GEMMs (opt-in STRATA_WMMA_GEMM=1) against a reference
add_executable(hip_prefill_wmma_gemm_parity tests/hip/prefill_wmma_gemm_parity.cpp)
target_include_directories(hip_prefill_wmma_gemm_parity PRIVATE "${CMAKE_CURRENT_SOURCE_DIR}/src/prefill")
@@ -1098,6 +1102,9 @@ if(STRATA_ENABLE_CUDA OR STRATA_ENABLE_HIP)
add_test(NAME hip_router_fast COMMAND hip_router_fast)
list(APPEND _strata_hip_tests hip_router_fast)
# S6: the RDNA4 matrix-core prompt attention (STRATA_HIP_WMMA) against the FP32 kernel and FP64; skips off gfx12
# the prompt attention on a recorded selection (STRATA_QSA_DUMP), for A/B of kernel variants (STRATA_QSA_UNION=1)
add_executable(hip_pa_bench tests/cuda/pa_bench.cpp)
target_link_libraries(hip_pa_bench PRIVATE strata_kernels strata_hip_runtime)
add_executable(hip_prompt_attn_wmma src/kernels/qsa_prompt_attn_parity.cpp)
target_link_libraries(hip_prompt_attn_wmma PRIVATE strata_kernels strata_hip_runtime)
add_test(NAME hip_prompt_attn_wmma COMMAND hip_prompt_attn_wmma 32768 2048 3)
@@ -1278,6 +1285,20 @@ if(STRATA_ENABLE_CUDA OR STRATA_ENABLE_HIP)
# "not on this card" for the tc variant
add_test(NAME gdn_rec_parity_selftest_frag COMMAND gdn_rec_parity --selftest-frag)
set_tests_properties(gdn_rec_parity_selftest_frag PROPERTIES SKIP_RETURN_CODE 77)
# the production-linked GDN parity / benchmark harness (no kernel copies: it calls gdn_gates, gdn_conv and
# gdn_recurrence of strata_prefill through the GdnProbe of include/strata/prefill/gdn_probe.hpp, against an FP64
# oracle from tests/gdn/gdn_fixture.hpp). Usage: docs/GDN_CONTRACT.md. The smoke test is small and CPU-cheap.
execute_process(COMMAND git -C ${CMAKE_CURRENT_SOURCE_DIR} rev-parse --short=9 HEAD OUTPUT_VARIABLE _gdn_sha
OUTPUT_STRIP_TRAILING_WHITESPACE ERROR_QUIET)
if(NOT _gdn_sha)
set(_gdn_sha unknown)
endif()
add_executable(gdn_harness src/prefill/gdn_harness.cu)
target_include_directories(gdn_harness PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/tests/gdn)
target_compile_definitions(gdn_harness PRIVATE GDN_HARNESS_SHA="${_gdn_sha}")
target_link_libraries(gdn_harness PRIVATE strata_prefill)
add_test(NAME gdn_harness_smoke COMMAND gdn_harness --T 400 --chunk 160 --cuts 37 --paths auto,keyhead,pipe --ref-heads 0,17)
set_tests_properties(gdn_harness_smoke PROPERTIES TIMEOUT 300)
# the prompt path's BF16 products against cuBLAS's own BF16 GEMM, within fp32 rounding (sm_7x runs them on the
# FP16 tensor cores; GPU, synthetic, no model; --bench adds timings)
add_executable(gemm_bf16_parity src/prefill/gemm_bf16_parity.cu)
@@ -1638,6 +1659,13 @@ if(STRATA_ENABLE_CUDA OR STRATA_ENABLE_HIP)
target_link_libraries(pinned_shared_test PRIVATE strata_core ${_strata_gpu_runtime_target})
add_test(NAME pinned_shared_test COMMAND pinned_shared_test)
endif()
if(WIN32) # the replicas' shared expert arena as a named section (src/core/pinned.cu): two arenas, a child process, the last handle
add_executable(pinned_shared_win_test src/core/pinned_shared_win_test.cpp)
target_include_directories(pinned_shared_win_test PRIVATE include)
target_link_libraries(pinned_shared_win_test PRIVATE strata_core ${_strata_gpu_runtime_target})
add_test(NAME pinned_shared_win_test COMMAND pinned_shared_win_test)
set_tests_properties(pinned_shared_win_test PROPERTIES SKIP_RETURN_CODE 77 TIMEOUT 180)
endif()
# Plan v0.3 P2: the n-gram table read straight from the SSD. The selftest needs only the CPU and a temp file.
add_executable(ple_reader_test src/ngram/ple_reader_test.cpp)
+1 -1
View File
@@ -95,7 +95,7 @@ Unsicher, was passt? [Wähle ein Modell für deinen Computer](docs/MODELS.md#pic
Installiere die App, wähle ein Modell und leg mit dem Chat los. Es gibt einen Installer pro System. Beim ersten Start lädt die App die Engine für deine Grafikkarte herunter. [Hilfe zum Installer](docs/INSTALL.md#the-app).
<sub>Dateien: `Strata-0.2.8-windows-x64-setup.exe` · `Strata-0.2.8-linux-x86_64.AppImage` · `Strata-0.2.8-macos-arm64.dmg`</sub>
<sub>Dateien: `Strata-0.2.9-windows-x64-setup.exe` · `Strata-0.2.9-linux-x86_64.AppImage` · `Strata-0.2.9-macos-arm64.dmg`</sub>
<p align="center">
<img src="docs/media/app-chat.png" width="900" alt="Strata-Desktop-App mit Chat und Live-Codevorschau nebeneinander">
+1 -1
View File
@@ -95,7 +95,7 @@ Medido con el conjunto de 2.00 bpw en Strata 0.2.5. «Salida» es la velocidad d
Instala la aplicación, elige un modelo y empieza a chatear. Hay un instalador por sistema. En el primer arranque, la aplicación descarga el motor para tu tarjeta gráfica. [Ayuda con el instalador](docs/INSTALL.md#the-app).
<sub>Archivos: `Strata-0.2.8-windows-x64-setup.exe` · `Strata-0.2.8-linux-x86_64.AppImage` · `Strata-0.2.8-macos-arm64.dmg`</sub>
<sub>Archivos: `Strata-0.2.9-windows-x64-setup.exe` · `Strata-0.2.9-linux-x86_64.AppImage` · `Strata-0.2.9-macos-arm64.dmg`</sub>
<p align="center">
<img src="docs/media/app-chat.png" width="900" alt="Aplicación de escritorio de Strata con un chat y una vista previa del código en vivo, una junto a otra">
+1 -1
View File
@@ -95,7 +95,7 @@ Vous ne savez pas ce qui tient ? [Choisir un modèle pour votre ordinateur](docs
Installez l'application, choisissez un modèle et commencez à discuter. Il y a un seul installateur par système. Au premier lancement, l'application télécharge le moteur de votre carte graphique. [Aide pour l'installateur](docs/INSTALL.md#the-app).
<sub>Fichiers: `Strata-0.2.8-windows-x64-setup.exe` · `Strata-0.2.8-linux-x86_64.AppImage` · `Strata-0.2.8-macos-arm64.dmg`</sub>
<sub>Fichiers: `Strata-0.2.9-windows-x64-setup.exe` · `Strata-0.2.9-linux-x86_64.AppImage` · `Strata-0.2.9-macos-arm64.dmg`</sub>
<p align="center">
<img src="docs/media/app-chat.png" width="900" alt="Application de bureau Strata avec une discussion et un aperçu du code en direct côte à côte">
+1 -1
View File
@@ -95,7 +95,7 @@ Strata 0.2.5 で 2.00 bpw のセットを使って測定しました。「出力
アプリをインストールし、モデルを選んで、チャットを始めましょう。インストーラーはシステムごとに1つです。初回起動時に、アプリがお使いのグラフィックスカード用のエンジンをダウンロードします。[インストーラーのヘルプ](docs/INSTALL.md#the-app)。
<sub>ファイル: `Strata-0.2.8-windows-x64-setup.exe` · `Strata-0.2.8-linux-x86_64.AppImage` · `Strata-0.2.8-macos-arm64.dmg`</sub>
<sub>ファイル: `Strata-0.2.9-windows-x64-setup.exe` · `Strata-0.2.9-linux-x86_64.AppImage` · `Strata-0.2.9-macos-arm64.dmg`</sub>
<p align="center">
<img src="docs/media/app-chat.png" width="900" alt="チャットとライブのコードプレビューを並べて表示した Strata デスクトップアプリ">
+1 -1
View File
@@ -99,7 +99,7 @@ Not sure what fits? [Choose a model for your computer](docs/MODELS.md#pick-by-me
Install the app, choose a model, and start chatting. There is one installer for each system. On first start the app
downloads the engine for your graphics card. [Help with the installer](docs/INSTALL.md#the-app).
<sub>Files: `Strata-0.2.8-windows-x64-setup.exe` · `Strata-0.2.8-linux-x86_64.AppImage` · `Strata-0.2.8-macos-arm64.dmg`</sub>
<sub>Files: `Strata-0.2.9-windows-x64-setup.exe` · `Strata-0.2.9-linux-x86_64.AppImage` · `Strata-0.2.9-macos-arm64.dmg`</sub>
<p align="center">
<img src="docs/media/app-chat.png" width="900" alt="Strata desktop app with a chat and live code preview side by side">
+1 -1
View File
@@ -99,7 +99,7 @@ Nie wiesz, co się zmieści? [Wybierz model do swojego komputera](docs/MODELS.md
Zainstaluj aplikację, wybierz model i zacznij rozmawiać. Dla każdego systemu jest jeden instalator. Przy pierwszym
uruchomieniu aplikacja pobiera silnik dla twojej karty graficznej. [Pomoc z instalatorem](docs/INSTALL.md#the-app).
<sub>Pliki: `Strata-0.2.8-windows-x64-setup.exe` · `Strata-0.2.8-linux-x86_64.AppImage` · `Strata-0.2.8-macos-arm64.dmg`</sub>
<sub>Pliki: `Strata-0.2.9-windows-x64-setup.exe` · `Strata-0.2.9-linux-x86_64.AppImage` · `Strata-0.2.9-macos-arm64.dmg`</sub>
<p align="center">
<img src="docs/media/app-chat.png" width="900" alt="Aplikacja Strata na komputer: czat i podgląd kodu na żywo obok siebie">
+1 -1
View File
@@ -95,7 +95,7 @@ Não sabe o que cabe? [Escolha um modelo para o seu computador](docs/MODELS.md#p
Instale o aplicativo, escolha um modelo e comece a conversar. Há um instalador por sistema. Na primeira vez que abre, o aplicativo baixa o motor da sua placa de vídeo. [Ajuda com o instalador](docs/INSTALL.md#the-app).
<sub>Arquivos: `Strata-0.2.8-windows-x64-setup.exe` · `Strata-0.2.8-linux-x86_64.AppImage` · `Strata-0.2.8-macos-arm64.dmg`</sub>
<sub>Arquivos: `Strata-0.2.9-windows-x64-setup.exe` · `Strata-0.2.9-linux-x86_64.AppImage` · `Strata-0.2.9-macos-arm64.dmg`</sub>
<p align="center">
<img src="docs/media/app-chat.png" width="900" alt="Aplicativo de desktop do Strata com um chat e uma prévia do código ao vivo lado a lado">
+1 -1
View File
@@ -95,7 +95,7 @@ strata launch --family deepseek41 --model ist-das-2.00bpw --plan
安装应用,选择模型,就可以开始聊天。每个系统只有一个安装程序。首次启动时,应用会下载适合你显卡的引擎。[安装程序帮助](docs/INSTALL.md#the-app)。
<sub>文件: `Strata-0.2.8-windows-x64-setup.exe` · `Strata-0.2.8-linux-x86_64.AppImage` · `Strata-0.2.8-macos-arm64.dmg`</sub>
<sub>文件: `Strata-0.2.9-windows-x64-setup.exe` · `Strata-0.2.9-linux-x86_64.AppImage` · `Strata-0.2.9-macos-arm64.dmg`</sub>
<p align="center">
<img src="docs/media/app-chat.png" width="900" alt="Strata 桌面应用,聊天和实时代码预览并排显示">
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "strata-desktop",
"productName": "Strata",
"version": "0.2.8",
"version": "0.2.9",
"private": true,
"description": "Strata Desktop - local AI, tuned to one model.",
"scripts": {
+1 -1
View File
@@ -3625,7 +3625,7 @@ checksum = "ff7589a1859b09232522bb9edf076ca7fcf14ded96685d3fa629db73023ffbb7"
[[package]]
name = "strata-desktop"
version = "0.2.8"
version = "0.2.9"
dependencies = [
"gtk",
"serde",
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "strata-desktop"
version = "0.2.8"
version = "0.2.9"
description = "Strata Desktop — local AI, tuned to one model."
edition = "2021"
rust-version = "1.77"
+2 -2
View File
@@ -1,12 +1,12 @@
{
"name": "strata",
"version": "0.2.8",
"version": "0.2.9",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "strata",
"version": "0.2.8",
"version": "0.2.9",
"dependencies": {
"html-to-image": "^1.11.13",
"katex": "^0.19.0",
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "strata",
"private": true,
"version": "0.2.8",
"version": "0.2.9",
"type": "module",
"description": "Strata \u2014 local AI, tuned to one model. Web shell + engine host.",
"scripts": {
+26
View File
@@ -0,0 +1,26 @@
// Throwaway: serves the demo host unchanged, except POST /api/chat/stream, which streams a long scripted reply (NDJSON frames like the real host).
import http from 'node:http';
const UP = Number(process.env.UP ?? 18791), PORT = Number(process.env.PORT ?? 18790);
const TOK = Number(process.env.REPLY_TOKENS ?? 12000), REASON = Number(process.env.REASON_TOKENS ?? 3000), RATE = Number(process.env.RATE ?? 400);
const sleep = ms => new Promise(r => setTimeout(r, ms));
const para = i => `### Section ${i}\n\nThis is paragraph ${i} with **bold**, *italic*, \`inline code\` and a link [x](https://example.com/${i}). The measured value is about ${i} units and the results agree with the earlier runs, which is what we expected from the model.
More prose follows here so that a paragraph looks like real writing and not like a stress pattern.\n\n- item one of ${i}\n- item two of ${i}\n\n\`\`\`python\ndef f_${i}(a, b):\n total = a + b * ${i}\n return total\n\`\`\`\n\n| a | b |\n|---|---|\n| ${i} | ${i * 2} |\n\n`;
http.createServer(async (req, res) => {
if (req.method === 'POST' && req.url.startsWith('/api/chat/stream')) {
let raw = ''; for await (const c of req) raw += c;
res.writeHead(200, { 'content-type': 'application/x-ndjson' });
const w = o => res.write(JSON.stringify(o) + '\n');
w({ type: 'start', model: 'Mock', contextMax: 131072, requestId: 'r' + Date.now() });
let sent = 0, i = 0;
const per = Math.max(1, Math.round(RATE / 50));
while (sent < REASON) { w({ type: 'reasoning', text: 'thinking about step ' + (i++) + ' of the problem. '.repeat(1) }); sent += 9; if (sent % per < 9) await sleep(20); }
sent = 0; i = 0;
while (sent < TOK) { const p = para(i++); for (let k = 0; k < p.length; k += 24) { w({ type: 'delta', text: p.slice(k, k + 24) }); sent += 6; if ((sent / 6) % Math.max(1, Math.round(per / 2)) === 0) await sleep(10); } w({ type: 'stats', tokensOut: sent, tps: 90 }); }
w({ type: 'done', tokensIn: 100, tokensOut: sent, tps: 90, durationMs: 1000, usageSource: 'engine' });
return res.end();
}
const up = http.request({ host: '127.0.0.1', port: UP, path: req.url, method: req.method, headers: req.headers }, r => { res.writeHead(r.statusCode, r.headers); r.pipe(res); });
up.on('error', () => { res.writeHead(502); res.end(); });
req.pipe(up);
}).listen(PORT, '127.0.0.1', () => console.log('proxy on', PORT));
+57
View File
@@ -0,0 +1,57 @@
// Measures the chat page's heap, DOM size and main-thread time per streamed reply. Needs a throwaway host (STRATA_REAL_ENGINE=0, port 18791), scripts/chat-growth-proxy.mjs (port 18790) and Chrome.
import { chromium } from 'playwright-core';
import { deflateSync } from 'node:zlib';
import { randomBytes } from 'node:crypto';
import { execSync } from 'node:child_process';
const BASE = process.env.BASE ?? 'http://127.0.0.1:18790';
const TURNS = Number(process.env.TURNS ?? 6), PICS = Number(process.env.PICS ?? 3), MON = process.env.MON !== '0';
const crc = (() => { const t = new Int32Array(256).map((_, n) => { let c = n; for (let k = 0; k < 8; k++) c = c & 1 ? 0xedb88320 ^ (c >>> 1) : c >>> 1; return c; }); return b => { let c = -1; for (const x of b) c = t[(c ^ x) & 255] ^ (c >>> 8); return (c ^ -1) >>> 0; }; })();
function png(w, h) {
const chunk = (type, data) => { const len = Buffer.alloc(4); len.writeUInt32BE(data.length); const body = Buffer.concat([Buffer.from(type), data]); const c = Buffer.alloc(4); c.writeUInt32BE(crc(body)); return Buffer.concat([len, body, c]); };
const raw = Buffer.alloc((w * 3 + 1) * h); for (let y = 0; y < h; y++) { raw[y * (w * 3 + 1)] = 0; randomBytes(w * 3).copy(raw, y * (w * 3 + 1) + 1); }
const ihdr = Buffer.alloc(13); ihdr.writeUInt32BE(w, 0); ihdr.writeUInt32BE(h, 4); ihdr[8] = 8; ihdr[9] = 2;
return Buffer.concat([Buffer.from([137, 80, 78, 71, 13, 10, 26, 10]), chunk('IHDR', ihdr), chunk('IDAT', deflateSync(raw, { level: 1 })), chunk('IEND', Buffer.alloc(0))]);
}
const browser = await chromium.launch({ executablePath: 'C:/Program Files/Google/Chrome/Application/chrome.exe', headless: true, args: ['--enable-precise-memory-info', '--js-flags=--expose-gc'] });
const ctx = await browser.newContext({ viewport: { width: 1400, height: 900 } });
const page = await ctx.newPage();
const cdp = await ctx.newCDPSession(page);
await cdp.send('Performance.enable');
await fetch(BASE + '/api/model-providers', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ kind: 'custom', provider: null, name: 'Mock model', baseUrl: 'http://127.0.0.1:18898/v1', protocol: 'openai-chat', apiKey: '', models: [{ id: 'mock-model', name: 'Mock model' }] }) });
await page.goto(BASE + '/'); await page.waitForTimeout(1500);
const skip = page.getByText('Go straight to the app'); await skip.waitFor({ timeout: 10000 }).catch(() => {}); if (await skip.count()) { await skip.click(); await page.waitForTimeout(800); }
async function metrics(tag) {
await cdp.send('HeapProfiler.collectGarbage').catch(() => {});
const m = Object.fromEntries((await cdp.send('Performance.getMetrics')).metrics.map(x => [x.name, x.value]));
const dom = await cdp.send('Memory.getDOMCounters').catch(() => ({}));
const sess = await page.evaluate(() => { try { return (localStorage.getItem('strata.sessions.v1') ?? '').length; } catch { return -1; } });
console.log(`${tag.padEnd(22)} heap=${(m.JSHeapUsedSize / 1048576).toFixed(0)}MB total=${(m.JSHeapTotalSize / 1048576).toFixed(0)}MB nodes=${dom.nodes ?? m.Nodes} listeners=${dom.jsEventListeners ?? '-'} docs=${m.Documents} ls=${(sess / 1e6).toFixed(2)}M`);
return m.JSHeapUsedSize;
}
await page.locator('.chat-model-picker').first().click(); await page.locator('.model-picker-api').first().click(); await page.waitForTimeout(500);
await metrics('start');
const ta = page.locator('.composer textarea');
for (let t = 1; t <= TURNS; t++) {
if (t <= PICS) {
const b64 = png(900, 900).toString('base64');
await page.evaluate(b64 => { const bytes = Uint8Array.from(atob(b64), c => c.charCodeAt(0)); const d = new DataTransfer(); d.items.add(new File([bytes], 'image.png', { type: 'image/png' })); document.querySelector('.composer textarea').dispatchEvent(new ClipboardEvent('paste', { clipboardData: d, bubbles: true, cancelable: true })); }, b64);
await page.waitForTimeout(600);
}
await ta.fill('turn ' + t + ' write a long answer');
await page.getByRole('button', { name: 'Send' }).click();
const t0 = Date.now();
let peak = 0; const cpu0 = (await cdp.send('Performance.getMetrics')).metrics.find(x => x.name === 'TaskDuration').value;
for (;;) {
await page.waitForTimeout(2000);
const m = Object.fromEntries((await cdp.send('Performance.getMetrics')).metrics.map(x => [x.name, x.value]));
peak = Math.max(peak, m.JSHeapUsedSize);
const busy = await page.getByRole('button', { name: /Stop/ }).count();
if (!busy || Date.now() - t0 > 240000) break;
}
const cpu1 = (await cdp.send('Performance.getMetrics')).metrics.find(x => x.name === 'TaskDuration').value;
console.log(` turn ${t}: ${(Date.now() - t0) / 1000}s peak-heap=${(peak / 1048576).toFixed(0)}MB main-thread-busy=${(cpu1 - cpu0).toFixed(1)}s`);
await metrics('after turn ' + t);
}
if (MON) { await page.getByText('Monitor', { exact: true }).first().click().catch(() => console.log('no monitor link')); await page.waitForTimeout(25000); await metrics('monitor 25s'); }
try { console.log(execSync('powershell -NoProfile -c "(Get-Process chrome | Measure-Object WorkingSet64 -Sum).Sum/1MB"').toString().trim(), 'MB chrome working set total'); } catch {}
await browser.close();
+5 -2
View File
@@ -4,7 +4,7 @@ import { detect, cachedHardware } from './hardware.mjs';
import { readGpuTelemetry, gpuMemoryTotals } from './device-probes.mjs';
import { createSystemSampler } from './system-telemetry.mjs';
import { sensorPace, WATCH_MS } from './sensor-pace.mjs';
import { readTemperatures } from './temperature-telemetry.mjs';
import { readTemperatures, stopTemperatures } from './temperature-telemetry.mjs';
import { addEnergy, energyWh } from './energy-meter.mjs';
import { state as servingState } from './serving.mjs';
import { originalDashboard } from './monitor-dashboard.mjs';
@@ -127,11 +127,14 @@ function tick() {
if(!sensorsPending&&Date.now()-sensorsAt>=pace.intervalMs){
sensorsPending=true;sensorsAt=Date.now();
const hardware=cachedHardware()??hwCache;
void Promise.all([readGpuTelemetry(hardware?.gpus??[],{transfer:pace.transfer}),sampleSystem(hardware?.processors??[]),(process.env.STRATA_REAL_ENGINE==='1'?readTemperatures():Promise.resolve(temperatures)).catch(()=>({cpuTempC:null,ssdTempC:null}))])
void Promise.all([readGpuTelemetry(hardware?.gpus??[],{transfer:pace.transfer}),sampleSystem(hardware?.processors??[]),(process.env.STRATA_REAL_ENGINE==='1'&&Date.now()<watchedUntil?readTemperatures():Promise.resolve(temperatures)).catch(()=>({cpuTempC:null,ssdTempC:null}))])
.then(([readings,system,temps])=>{state.hardware.gpus=readings;systemReading=system;temperatures=temps;})
.catch(()=>{state.hardware.gpus=[];systemReading=null;})
.finally(()=>{sensorsPending=false;});
}
/* #4661: the drive-temperature reader asks every physical drive for its temperature. Only while the Monitor is looked at, never from the first second of the app
(on some PCs the storage queries stall the whole system); it ends a couple of minutes after the Monitor was last seen. */
if(Date.now()>watchedUntil+120000)stopTemperatures();
addEnergy(hardwareSnapshot().powerW,Date.now());
const hw = state.hardware;
const busy = state.phase === 'generating' || state.phase === 'reading' || state.phase === 'thinking';
+11 -1
View File
@@ -278,7 +278,7 @@ return {summary:result.summary??'Deleted',removed:result.plan?.gb??null};}
const resources=args.resourceProfile&&!ownEngine(config)?resourceSettings({...base0,[USER_KEY]:mine,vision},snapshot.hardware,args.resourceProfile,modelArena(args.model),values0):null;
const cfg={...config,...visionPreferences,...(resources?{...resources,ui_resource_profile:args.resourceProfile}:{}),host:'127.0.0.1',open_browser:false,...validateNativeSettings(args.values??{}),vision};
/* 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. */
const namedInFile=(Array.isArray(config.gpu)?config.gpu.length>0:config.gpu!==undefined&&config.gpu!==null&&config.gpu!=='')||!!config.gpu_roles;
const namedInFile=((Array.isArray(config.gpu)?config.gpu.length>0:config.gpu!==undefined&&config.gpu!==null&&config.gpu!=='')||!!config.gpu_roles)&&!setupPickedOneCard(config,snapshot.hardware);
/* 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. */
if(args.gpuRoles!==undefined){const make=snapshot.hardware?.engine_vendor,cards=(snapshot.hardware?.gpus??[]).filter(g=>!make||!g.vendor||g.vendor===make),named=parseRoles(args.gpuRoles);if(!named)throw new Error('Choose Main, Helper or Off for each graphics card, with one Main.');const known=new Set(cards.map(g=>g.index));if(Object.keys(named).some(k=>!known.has(Number(k))))throw new Error('Choose detected graphics cards.');const roles=Object.fromEntries(cards.map(g=>[g.index,named[g.index]??'off'])),m=mirrorRoles(roles,cards);for(const k of ['gpu','gpu_order','gpu_none','gpu_roles','gpu_cards','hip_ordinal'])delete cfg[k];Object.assign(cfg,m);const count=Array.isArray(m.gpu)?m.gpu.length:m.gpu_none?0:1;cfg.layer_split=count>1?validateNativeSettings({layer_split:args.layerSplit??'auto'}).layer_split:'auto';if(cfg.layer_split!=='auto'&&cfg.layer_split.split(',').length!==count-1)throw new Error('Choose one layer split point between each pair of GPUs.');}
else if(args.gpus!==undefined&&!(args.gpusFromRecommendation&&namedInFile)){const detected=new Set((snapshot.hardware?.gpus??[]).map(g=>g.index));if(!Array.isArray(args.gpus)||!args.gpus.length||new Set(args.gpus).size!==args.gpus.length||args.gpus.some(g=>!Number.isInteger(g)||!detected.has(g)))throw new Error('Choose distinct detected GPUs. CPU-only text inference is not supported by this native adapter.');cfg.gpu=args.gpus.length===1?args.gpus[0]:args.gpus;for(const k of ['gpu_roles','gpu_cards','gpu_none'])delete cfg[k];delete cfg.hip_ordinal;cfg.layer_split=args.gpus.length>1?validateNativeSettings({layer_split:args.layerSplit??'auto'}).layer_split:'auto';if(cfg.layer_split!=='auto'&&cfg.layer_split.split(',').length!==args.gpus.length-1)throw new Error('Choose one layer split point between each pair of GPUs.');}
@@ -593,3 +593,13 @@ return {summary:result.summary??'Deleted',removed:result.plan?.gb??null};}
async function localPreview(path){if(!packaged)throw new Error('Checking local model files is part of the Strata app.');if(typeof path!=='string'||!path.trim()||[...path].some(c=>c.charCodeAt(0)<32))throw new Error('Choose a folder.');return control.importPlan(path.trim());}
return {state:publicState,tuneStatus,tuneEffective,tuneOff,tunePresets:{list:()=>autotuner.list(),action:(kind,id,extra)=>autotuner.action(kind,id,extra),undoFor:async args=>autotuner.undoFor(tuneConfig(args).path)},refresh,localPreview,configure,setDownloadDirectory,planModelsFolder,startModelsFolder,interruptedMove,finishInterruptedMove,leaveInterruptedMove,plan,settings,save,settingsFile,openSettingsFile,operation,cancel,unload,resolveInstance,poll,logs:()=>logs.slice(),benchmarkReport:()=>readJson(join(directory,'native-benchmark.json'),null),setDefault(id){defaultId=resolveInstance(id).id;emit();return publicState();},hasLive:()=>[...instances.values()].some(i=>i.status==='ready'),readyInstances:()=>[...instances.values()].filter(i=>i.status==='ready'),defaultInstanceId:()=>defaultId,close};
}
/* 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. */
export function setupPickedOneCard(config,hardware){
if(!config||config.arch!=='deepseek41'||config.backend!=='hip'||config.gpus_by||config.gpu_roles||!config.gpus_asked)return false;
const one=Array.isArray(config.gpu)?(config.gpu.length===1?config.gpu[0]:null):config.gpu;if(!Number.isInteger(Number(one))||one===null||one==='')return false;
const cards=(hardware?.gpus??[]).filter(g=>(g.vendor==='amd')&&g.usable!==false),shares=g=>!!(g.uma||g.integrated||g.unified_gb),vram=g=>Number(g.vram_gb)||0;
const real=cards.some(g=>!shares(g)&&vram(g)>=4),keep=cards.filter(g=>!(shares(g)&&real)&&vram(g)>=8);
if(process.platform==='win32'||keep.length<2)return false;
const first=[...keep].sort((a,b)=>Math.round(vram(b))-Math.round(vram(a))||a.index-b.index)[0];
return Number(one)===first.index;
}
+2 -2
View File
@@ -2,7 +2,7 @@
import { once } from 'node:events';
export async function nativeGateway({req,res,path,body,control,serving,fetcher=fetch}){
let instance;try{instance=control.resolveInstance(body.model);}catch(e){res.writeHead(400,{'content-type':'application/json'});res.end(JSON.stringify({error:{message:e.message,type:'invalid_request_error',param:'model',code:null}}));return;}
const abort=new AbortController(),started=Date.now();res.on('close',()=>{if(!res.writableEnded)abort.abort();});
const abort=new AbortController(),started=Date.now();let sawDone=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();});
const entry=serving.open({client:req.headers['user-agent']?.slice(0,100)??'API client',endpoint:path.slice(3),model:instance.apiModelId,instanceId:instance.id,gpuIds:instance.gpus.map(String),cancel:()=>abort.abort()});
let tokensIn=null,tokensOut=null,tps=null,promptTps=null;
const observe=value=>{const 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;}};
@@ -11,7 +11,7 @@ export async function nativeGateway({req,res,path,body,control,serving,fetcher=f
if(!response.ok){const content=await response.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;}
serving.mark(entry.id,{state:'generating'});
res.writeHead(response.status,{'content-type':response.headers.get('content-type')??'application/json','cache-control':'no-cache','x-strata-request-id':entry.id,'x-accel-buffering':'no'});
if(body.stream===true){let buffer='';const decoder=new TextDecoder();for await(const chunk of response.body){buffer+=decoder.decode(chunk,{stream:true});let end;while((end=buffer.indexOf('\n'))>=0){const line=buffer.slice(0,end);buffer=buffer.slice(end+1);if(line.startsWith('data:'))try{observe(JSON.parse(line.slice(5)));}catch{}}if(!res.write(chunk))await once(res,'drain',{signal:abort.signal});}}
if(body.stream===true){let buffer='';const decoder=new TextDecoder();for await(const chunk of response.body){buffer+=decoder.decode(chunk,{stream:true});let end;while((end=buffer.indexOf('\n'))>=0){const line=buffer.slice(0,end);buffer=buffer.slice(end+1);if(line.startsWith('data:')){if(line.slice(5).trim()==='[DONE]')sawDone=true;else try{observe(JSON.parse(line.slice(5)));}catch{}}}if(!res.destroyed&&!res.write(chunk))await once(res,'drain',{signal:abort.signal});}}
else{const text=await response.text();try{observe(JSON.parse(text));}catch{}res.write(text);}
serving.mark(entry.id,{tokensIn,tokensOut,tps,promptTps,usageSource:tokensIn===null||tokensOut===null?'unavailable':'reported'});serving.finish(entry.id,{tokensOut,durationMs:Date.now()-started});res.end();
}catch(e){serving.finish(entry.id,{tokensOut,durationMs:Date.now()-started,error:abort.signal.aborted?'Request cancelled.':e.message});if(!res.headersSent){res.writeHead(502,{'content-type':'application/json'});res.end(JSON.stringify({error:{message:e.message,type:'server_error'}}));}else res.end();}
@@ -122,3 +122,8 @@ export async function readTemperatures({ platform = process.platform, now = Date
}
return { cpuTempC: null, ssdTempC: null };
}
/** Ends the Windows drive-temperature loop (the Monitor has not been open for a while); the next read starts it again. */
export function stopTemperatures() {
if (windowsReader) { windowsReader.stop(); windowsReader = null; }
}
+1 -1
View File
@@ -96,7 +96,7 @@ export function SettingsUpdates() {
</>;
}
/** "Strata 0.2.8 · Engine 0.1.54": the app's own version first, the engine's beside it (About). */
/** "Strata 0.2.9 · Engine 0.1.55": the app's own version first, the engine's beside it (About). */
export function AboutVersions() {
const native = useNative();
const app = appVersionLabel(useStore(s => s.setup?.app.version), native?.version);
+23 -2
View File
@@ -1775,6 +1775,23 @@ export async function sendMessage(text: string, options: { fromLoop?: boolean; g
const started = performance.now();
const imageSlots=photoHistorySlots(prior,pictures.length);
/* Text and reasoning arrive token by token (90 a second or more). Drawing the whole answer again for each one made the page do more work for every
word the longer the answer grew (measured: 15.7 s of main-thread time for 24 s of a 40,000-token answer), until the page stopped answering
and ran out of memory. The pieces are gathered and applied together about ten times a second; any other frame, the end and a failure
apply what is waiting first, so the order the person sees is the order it was written. */
const waiting: ['reasoning' | 'delta', string][] = [];
let waitingTimer: ReturnType<typeof setTimeout> | null = null;
const applyReasoning = (text: string) => patchAssistant((m) => ({ ...m, reasoning: (m.reasoning ?? '') + text, reasoningStartedAt: m.reasoningStartedAt ?? Date.now() }));
const applyDelta = (text: string) => patchAssistant((m) => { const content=tidyAnswer(m.content+text); if (m.reasoningStartedAt && m.reasoningMs === undefined) m = { ...m, reasoningMs: Date.now() - m.reasoningStartedAt }; const count=Math.max(1,Math.ceil((content.length+(m.reasoning?.length??0))/4)); return {...m,content,...(m.blocks?{blocks:appendText(m.blocks,text)}:{}),tokens:m.usageSource==='estimated'&&m.tps?m.tokens:count,inputTokens:m.inputTokens??Math.ceil((body.length+prior.reduce((n,p)=>n+p.content.length,0))/4),usageSource:'estimated'}; });
const flushWaiting = () => {
if (waitingTimer !== null) { clearTimeout(waitingTimer); waitingTimer = null; }
for (const [kind, text] of waiting.splice(0)) if (kind === 'delta') applyDelta(text); else applyReasoning(text);
};
const queueText = (kind: 'reasoning' | 'delta', text: string) => {
const last = waiting[waiting.length - 1];
if (last && last[0] === kind) last[1] += text; else waiting.push([kind, text]);
waitingTimer ??= setTimeout(flushWaiting, 90);
};
try {
await streamPost(
planStart ? '/api/plan/start-goal' : '/api/chat/stream',
@@ -1801,6 +1818,8 @@ export async function sendMessage(text: string, options: { fromLoop?: boolean; g
// A line from the app itself carries `key` (the English sentence): shown in the person's language.
if (frame.type === 'delta' && typeof frame.key === 'string') frame = { ...frame, text: tr(frame.key, Array.isArray(frame.values) ? frame.values : undefined) };
const type = frame.type as string;
if (type === 'delta' || type === 'reasoning') { queueText(type, String(frame.text ?? '')); return; }
flushWaiting();
if (type === 'start' && frame.agent === true) { const turn = liveTurns.get(sessionId); if (turn) turn.agent = true; }
if (type === 'queued') {
patchAssistant(m => ({ ...m, requestId: String(frame.requestId) }));
@@ -1837,7 +1856,7 @@ export async function sendMessage(text: string, options: { fromLoop?: boolean; g
} else if (type === 'plan-step') {
/* the whole plan follows in a plan frame; the live status already says which step is running */
} else if (type === 'reasoning') {
patchAssistant((m) => ({ ...m, reasoning: (m.reasoning ?? '') + String(frame.text ?? ''), reasoningStartedAt: m.reasoningStartedAt ?? Date.now() }));
/* applied through queueText above */
} else if (type === 'notice') {
// The host says why this chat is not using the agent. One toast per reason per page load.
const code = String(frame.code ?? 'notice');
@@ -1852,7 +1871,7 @@ export async function sendMessage(text: string, options: { fromLoop?: boolean; g
} else if (type === 'agent') {
patchSession(sessionId, s => ({ ...s, agents: updateAgent(s.agents ?? [], frame.agent) }));
} else if (type === 'delta') {
patchAssistant((m) => { const content=tidyAnswer(m.content+String(frame.text??'')); if (m.reasoningStartedAt && m.reasoningMs === undefined) m = { ...m, reasoningMs: Date.now() - m.reasoningStartedAt }; const count=Math.max(1,Math.ceil((content.length+(m.reasoning?.length??0))/4)); return {...m,content,...(m.blocks?{blocks:appendText(m.blocks,String(frame.text??''))}:{}),tokens:m.usageSource==='estimated'&&m.tps?m.tokens:count,inputTokens:m.inputTokens??Math.ceil((body.length+prior.reduce((n,p)=>n+p.content.length,0))/4),usageSource:'estimated'}; });
/* applied through queueText above */
} else if (type === 'deliverable') {
const file = readDeliverable(frame);
if (file) patchAssistant(m => ({ ...m, deliverables: mergeDeliverable(m.deliverables, file) }));
@@ -1914,6 +1933,7 @@ export async function sendMessage(text: string, options: { fromLoop?: boolean; g
turnAbort.signal,
);
} catch (err) {
flushWaiting();
if ((err as Error).name !== 'AbortError') {
patchAssistant((m) => ({
...m,
@@ -1925,6 +1945,7 @@ export async function sendMessage(text: string, options: { fromLoop?: boolean; g
patchAssistant((m) => ({ ...m, streaming: false }));
}
} finally {
flushWaiting();
// A cancelled or failed turn is still worth keeping.
flushSessions();
clearStatus(sessionId);
+14 -3
View File
@@ -33,7 +33,7 @@ import { useLiveStatus } from '../lib/live-status';
import { askQuestion, askSub, askIcon } from '../lib/ask';
import { artifactFromUrl, type PreviewArtifact } from '../lib/artifacts';
/** Chat — talk to the one loaded model. */
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { Markdown } from '../components/Markdown';
import { Btn, Icon, IconBtn, ErrorNotice, type IconName } from '../components/ui';
import { composerCap, scrollBehavior } from '../lib/responsive';
@@ -188,6 +188,16 @@ export function ChatScreen() {
if (item)
openPreview(item);
}
/* The message list is memoised (#4640 family: Out of Memory in a long chat): a streamed token changes one message, so the others must not
re-render. These handlers keep one identity for the life of the screen and always call the newest functions above. */
const latestHandlers = useRef({ editPrompt, openPreview, openTimeSkip, openLink });
latestHandlers.current = { editPrompt, openPreview, openTimeSkip, openLink };
const messageHandlers = useMemo(() => ({
onEditPrompt: (text: string) => latestHandlers.current.editPrompt(text),
onPreview: (a: PreviewArtifact) => latestHandlers.current.openPreview(a),
onOpenTimeSkip: (id: string) => latestHandlers.current.openTimeSkip(id),
onOpenLink: (url: string) => latestHandlers.current.openLink(url),
}), []);
const scroller = useRef<HTMLDivElement>(null);
const focusMode = useStore((s) => s.focus);
/** Per-conversation scroll offsets, so returning to one restores the view. */
@@ -445,7 +455,7 @@ export function ChatScreen() {
<div className="chat__inner">
{tr(session.messages.length === 0 ? (session.statsAvailable === false ? <p className="small muted">{tr("Editing from an earlier message. Update the prompt below.")}</p> :
<UsageDashboard loaded={!!loaded}/>) : (session.messages.map((m, i) => (<Message key={m.id} message={m} isLast={i === session.messages.length - 1} onEditPrompt={editPrompt} onPreview={openPreview} onOpenTimeSkip={openTimeSkip} timeSkipDone={!!allTimeSkipRuns.find(r => r.id === m.timeSkipId)?.completedAt} timeSkipReal={!!allTimeSkipRuns.find(r => r.id === m.timeSkipId)?.job} timeSkipRun={allTimeSkipRuns.find(r => r.id === m.timeSkipId)} onOpenLink={openLink}/>))))}
<UsageDashboard loaded={!!loaded}/>) : (session.messages.map((m, i) => (<Message key={m.id} message={m} isLast={i === session.messages.length - 1} onEditPrompt={messageHandlers.onEditPrompt} onPreview={messageHandlers.onPreview} onOpenTimeSkip={messageHandlers.onOpenTimeSkip} timeSkipDone={!!allTimeSkipRuns.find(r => r.id === m.timeSkipId)?.completedAt} timeSkipReal={!!allTimeSkipRuns.find(r => r.id === m.timeSkipId)?.job} timeSkipRun={allTimeSkipRuns.find(r => r.id === m.timeSkipId)} onOpenLink={messageHandlers.onOpenLink}/>))))}
<QueuedMessages chatId={session.id}/>
</div>
</div>
@@ -611,7 +621,8 @@ function Composer({ draft, setDraft, loaded, streaming, onSubmit, onKey, session
</div>) : null)}
</div>);
}
function Message({ message: m, isLast, onEditPrompt, onPreview, onOpenLink, onOpenTimeSkip, timeSkipDone, timeSkipReal, timeSkipRun, }: {
const Message = memo(MessageView);
function MessageView({ message: m, isLast, onEditPrompt, onPreview, onOpenLink, onOpenTimeSkip, timeSkipDone, timeSkipReal, timeSkipRun, }: {
message: ChatMessage;
isLast: boolean;
onEditPrompt: (text: string) => void;
+1 -1
View File
@@ -37,7 +37,7 @@ test('streaming: a tag that arrives in pieces is removed once it is complete', (
test('the store applies it to the answer, the ordered pieces and the Markdown export', async () => {
const store = await readFile(new URL('../src/lib/store.ts', import.meta.url), 'utf8');
assert.match(store, /tidyAnswer\(m\.content\+String\(frame\.text/);
assert.match(store, /tidyAnswer\(m\.content\+text\)/); // applyDelta, which the queued pieces go through
assert.match(store, /m\.role === 'assistant' \? tidyAnswer\(m\.content\) : m\.content\)/);
assert.match(await readFile(new URL('../src/lib/deliverables.ts', import.meta.url), 'utf8'), /tidyAnswer\(last\.text \+ text\)/);
});
+33
View File
@@ -0,0 +1,33 @@
/** A streamed token changes one message. The others must not render again, or a long chat does work (and makes garbage) for every
* token in proportion to its whole history: measured 0.7 s of main-thread time for a 10-turn reply on a new chat and 1.9 s after ten
* replies, peak heap 95 MB instead of 50 MB (scripts/chat-growth.mjs). Source guards: the DOM is not available in this suite. */
import test from 'node:test';
import assert from 'node:assert/strict';
import { readFileSync } from 'node:fs';
import { resolve } from 'node:path';
const chat = readFileSync(resolve(import.meta.dirname, '../src/screens/Chat.tsx'), 'utf8');
test('the message row is a memoised component', () => {
assert.match(chat, /import \{[^}]*\bmemo\b[^}]*\} from 'react'/);
assert.match(chat, /const Message = memo\(MessageView\)/);
});
test('the list passes handlers whose identity never changes', () => {
assert.match(chat, /const messageHandlers = useMemo\(\(\) => \(\{[\s\S]*?\}\), \[\]\)/);
for (const name of ['onEditPrompt', 'onPreview', 'onOpenTimeSkip', 'onOpenLink'])
assert.match(chat, new RegExp(`${name}=\{messageHandlers\.${name}\}`), name);
assert.doesNotMatch(chat, /onEditPrompt=\{editPrompt\}/);
});
const store = readFileSync(resolve(import.meta.dirname, '../src/lib/store.ts'), 'utf8');
test('streamed text and reasoning are applied in groups, and everything else applies what is waiting first', () => {
assert.match(store, /const queueText = \(kind: 'reasoning' \| 'delta', text: string\)/);
assert.match(store, /waitingTimer \?\?= setTimeout\(flushWaiting, 90\)/);
// the order of frames the person sees is the order written: a frame of any other kind flushes first
assert.match(store, /if \(type === 'delta' \|\| type === 'reasoning'\) \{ queueText\(type, String\(frame\.text \?\? ''\)\); return; \}\s*flushWaiting\(\);/);
// a failure or the end of the turn never leaves text behind
assert.match(store, /\} catch \(err\) \{\s*flushWaiting\(\);/);
assert.match(store, /\} finally \{\s*flushWaiting\(\);/);
});
+3 -1
View File
@@ -71,11 +71,13 @@ test('the plan says what the picked folder means and keeps the person\'s spellin
test('a whole drive is planned as <drive>\\Strata, shown as <drive>\\Strata\\Models', async t => {
const f = fixture(t);
const root = f.root.slice(0, f.root.indexOf(process.platform === 'win32' ? '\\' : '/') + 1) || '/';
const before = existsSync(join(root, 'Strata'));
const plan = await f.native.planModelsFolder(root);
assert.equal(existsSync(join(root, 'Strata')), before, 'planning must not create or remove anything');
assert.equal(plan.wholeDrive, true);
assert.equal(plan.folder, join(root, 'Strata'));
assert.equal(plan.shown, join(root, 'Strata', 'Models'));
rmSync(join(root, 'Strata'), { recursive: true, force: true });
// Planning must not touch the disk. This test never creates <drive>\Strata, so it never deletes it either (#4689).
});
test('the card is blocked while a model runs or a job runs: a running model must be stopped on purpose', async t => {
+25
View File
@@ -0,0 +1,25 @@
/** The app must not keep the one card a DeepSeek config of setup 0.2.8 and earlier names after setup's own pick (strata_control/gpu_pick.py). */
import test from 'node:test';
import assert from 'node:assert/strict';
import { setupPickedOneCard } from '../server/native-control.mjs';
const amd = (index, extra = {}) => ({ index, vendor: 'amd', vram_gb: 32, usable: true, ...extra });
const four = { gpus: [0, 1, 2, 3].map(i => amd(i)) };
const old = { arch: 'deepseek41', backend: 'hip', gpu: 0, gpus_asked: true };
const linux = process.platform !== 'win32';
test('setup\'s own one-card pick is not a choice (Linux, several cards)', { skip: !linux }, () => {
assert.equal(setupPickedOneCard(old, four), true);
});
test('another card, a marker, roles, a list or another model are the person\'s', () => {
assert.equal(setupPickedOneCard({ ...old, gpu: 2 }, four), false);
assert.equal(setupPickedOneCard({ ...old, gpus_by: 'person' }, four), false);
assert.equal(setupPickedOneCard({ ...old, gpu_roles: { 0: 'main' } }, four), false);
assert.equal(setupPickedOneCard({ ...old, gpu: [0, 1] }, four), false);
assert.equal(setupPickedOneCard({ ...old, arch: 'qwen4exp' }, four), false);
assert.equal(setupPickedOneCard({ ...old, backend: 'cuda' }, four), false);
});
test('one usable card, or an APU beside one discrete card, leaves nothing to widen', () => {
assert.equal(setupPickedOneCard(old, { gpus: [amd(0)] }), false);
assert.equal(setupPickedOneCard({ ...old, gpu: 1 }, { gpus: [amd(0, { uma: true, vram_gb: 96 }), amd(1)] }), false);
});
+19
View File
@@ -27,3 +27,22 @@ test('native gateway preserves all three protocols and routes exact IDs, usage,
result=await call('/v1/responses',{model:'native-a',input:'slow',stream:true});const read=result.body.getReader();await read.read();await read.cancel();for(let n=0;n<30&&!cancelled;n++)await new Promise(r=>setTimeout(r,20));assert.equal(cancelled,true);
}finally{await close(gateway);await close(engine);}
});
test('a client that hangs up right after data: [DONE] has its answer: the request is done, not "Request cancelled." (#4690)',{timeout:30000},async()=>{
const net=await import('node:net');const records=[];let closeDelay=0;
const engine=http.createServer(async(req,res)=>{for await(const _ of req);res.writeHead(200,{'content-type':'text/event-stream',connection:'close'});
res.write('data: {"choices":[{"delta":{"content":"Hi"}}]}\n\n');res.write('data: {"choices":[],"usage":{"prompt_tokens":12,"completion_tokens":3}}\n\n');res.write('data: [DONE]\n\n');
await new Promise(r=>setTimeout(r,closeDelay));res.end();});
const enginePort=await listen(engine);
const serving={open:value=>{const record={id:records.length,state:'queued',tokensIn:null,tokensOut:null,...value};records.push(record);return record;},mark:(id,value)=>Object.assign(records[id],value),finish:(id,{tokensOut,error})=>Object.assign(records[id],{state:error?'error':'done',tokensOut,error})};
const control={resolveInstance:()=>({id:'x',apiModelId:'x',port:enginePort,key:'k',gpus:[0]})};
const gateway=http.createServer(async(req,res)=>{let text='';for await(const chunk of req)text+=chunk;await nativeGateway({req,res,path:req.url,body:JSON.parse(text),control,serving});});
const gatewayPort=await listen(gateway);
const call=()=>new Promise(resolve=>{const body=JSON.stringify({model:'x',stream:true,messages:[]});
const socket=net.connect(gatewayPort,'127.0.0.1',()=>socket.write(`POST /v1/chat/completions HTTP/1.1\r\nHost: x\r\nConnection: close\r\nContent-Type: application/json\r\nContent-Length: ${Buffer.byteLength(body)}\r\n\r\n${body}`));
let text='';socket.on('data',data=>{text+=data;if(text.includes('data: [DONE]'))socket.destroy();});socket.on('close',()=>resolve(text));});
try{
for(const delay of [0,5,30])for(let run=0;run<5;run++){closeDelay=delay;const before=records.length;assert.match(await call(),/\[DONE\]/);
for(let n=0;n<100&&!['done','error'].includes(records[before]?.state);n++)await new Promise(r=>setTimeout(r,20));
const last=records[before];assert.equal(last.state,'done',`delay ${delay} ms: ${last.error}`);assert.equal(last.tokensIn,12);assert.equal(last.tokensOut,3);}
}finally{await close(gateway);await close(engine);}
});
+1 -1
View File
@@ -57,5 +57,5 @@ test('Cargo.toml, tauri.conf.json and the desktop package agree on the app versi
const cargo = /^version\s*=\s*"([^"]+)"/m.exec(await readFile(new URL('src-tauri/Cargo.toml', root), 'utf8'))[1];
const pkg = JSON.parse(await readFile(new URL('package.json', root), 'utf8')).version;
assert.equal(cargo, pkg);
assert.equal(cargo, '0.2.8');
assert.equal(cargo, '0.2.9');
});
+11
View File
@@ -47,3 +47,14 @@ test('the monitor uses the pace, the Monitor screen asks while it is open, and /
const screen = await readFile(new URL('../src/screens/Monitor.tsx', import.meta.url), 'utf8');
assert.match(screen, /api\.get\('\/api\/monitor'\)/);
});
test('the drive-temperature reader starts only while the Monitor is watched and ends after it is gone (#4661)', async () => {
const monitor = await readFile(new URL('../server/monitor.mjs', import.meta.url), 'utf8');
assert.match(monitor, /STRATA_REAL_ENGINE==='1'&&Date\.now\(\)<watchedUntil\?readTemperatures\(\)/);
assert.match(monitor, /stopTemperatures\(\)/);
const { readTemperatures, stopTemperatures } = await import('../server/temperature-telemetry.mjs');
assert.equal(typeof stopTemperatures, 'function');
// nothing is started by importing or by stopping: on a PC that never opens the Monitor no PowerShell and no disk query exists
stopTemperatures();
assert.deepEqual(await readTemperatures({ platform: 'darwin' }), { cpuTempC: null, ssdTempC: null });
});
+2 -2
View File
@@ -10,8 +10,8 @@ holds the technical context behind the short [README](../README.md). Use [AI_SET
- Strata runs supported language models locally, with a desktop app, a terminal app, and an inference engine.
- The engine uses the GPU, system RAM and, for applicable models and memory modes, SSD storage.
See [how it works](HOW_IT_WORKS.md) and [engine details](DETAILS.md).
- There is one app installer per system (`Strata-0.2.8-windows-x64-setup.exe`, `Strata-0.2.8-linux-x86_64.AppImage`,
`Strata-0.2.8-macos-arm64.dmg`). The Windows and Linux installers carry no engine: the app downloads the engine for the
- There is one app installer per system (`Strata-0.2.9-windows-x64-setup.exe`, `Strata-0.2.9-linux-x86_64.AppImage`,
`Strata-0.2.9-macos-arm64.dmg`). The Windows and Linux installers carry no engine: the app downloads the engine for the
graphics card on first start, or installs a file the user already has. The Mac dmg keeps its Metal engine inside.
The Terminal's `strata launch` downloads the appropriate engine and model.
See [installation](INSTALL.md) and [Terminal launch](TERMINAL_LAUNCH.md).
+2 -2
View File
@@ -4,8 +4,8 @@ Use this guide when a user asks an AI agent to install Strata (version 0.2.5). T
one; do not start from a source checkout.
- **The Terminal and `strata launch`** (below). This is the route for an agent that has a shell.
- **The desktop app.** One installer per system: `Strata-0.2.8-windows-x64-setup.exe`,
`Strata-0.2.8-linux-x86_64.AppImage`, `Strata-0.2.8-macos-arm64.dmg`, from the
- **The desktop app.** One installer per system: `Strata-0.2.9-windows-x64-setup.exe`,
`Strata-0.2.9-linux-x86_64.AppImage`, `Strata-0.2.9-macos-arm64.dmg`, from the
[latest release](https://github.com/Niko1221/Strata/releases/latest). The Windows and Linux installers carry no
engine: on first start the app shows the engine for the user's graphics card and downloads it, or installs a file the
user already has ("I already have the file"). Details: [INSTALL.md](INSTALL.md).
+2 -2
View File
@@ -49,7 +49,7 @@ carries it but it is untested. Details: [MODELS.md](MODELS.md#deepseek-v41-flash
## Ready-made packages
The Terminal one-liner and `strata update` pick the package whose GPU code covers the card. The app installer
(`Strata-0.2.8-windows-x64-setup.exe`, `Strata-0.2.8-linux-x86_64.AppImage`) has no engine inside: on its first start the app
(`Strata-0.2.9-windows-x64-setup.exe`, `Strata-0.2.9-linux-x86_64.AppImage`) has no engine inside: on its first start the app
downloads the matching package for the card. You can also press **Update engine** in Settings (or run `strata update`).
| Package (variant) | OS | GPU code inside | ROCm |
@@ -131,7 +131,7 @@ reused. The Monitor shows the card's load, VRAM, temperature and power from Linu
## Windows
Install the [app](INSTALL.md) (`Strata-0.2.8-windows-x64-setup.exe` from the
Install the [app](INSTALL.md) (`Strata-0.2.9-windows-x64-setup.exe` from the
[release page](https://github.com/Niko1221/Strata/releases/latest); it downloads the engine for the card), or
install the [Terminal](INSTALL.md#the-terminal) and run `strata launch`. You need Windows 10 or 11 (64-bit), a supported card
and a current AMD driver. No ROCm or HIP SDK, no compiler, no admin rights. `START-HERE.bat --backend hip` is a
+9
View File
@@ -39,6 +39,15 @@ perplexity +7.3% (code +3.2%, agent work +2.2%). IQ1_M: perplexity +21% (KL 0.21
| IQ4_NL: `DeepSeek-V4.1-Flash-Engram-IQ4_NL.gguf` | 111 GB | the 4-bit Engram lookup table the model reads from RAM or SSD. Not a model to pick: all three sets use this one file, and it is already in their download sizes |
| DSpark drafter (Q8_0) | 8 GB | the small draft model (Unsloth) |
**Two layouts of the same sets.** Setup downloads the four-shard files (`...-0000N-of-00004.gguf`, the Hugging Face revision `cb02422c`). On 27 October 2026
ISTA-DASLab re-split the three sets: five shards each (`...-0000N-of-00006.gguf`; shard 1 is 5.8 MB of metadata and has no tensors) and the Engram file as the
sixth shard of the split. Strata takes either from files you already have (`--gguf-dir`, `--model-path`, "Use a local GGUF" in the app, `strata models import`),
checks their size and SHA-256, and packs them. The same goes for the four-shard files whose first shard ISTA-DASLab re-published on 26 October with a larger
chat template. What differs in the six-shard files: the two Engram tables are one tensor (the same bytes), the Engram gate is stored already folded (q times k),
the attention output matrix is one 2-D tensor instead of a 3-D one, and there is a routing bias for picture tokens. The packs are the same size and the engine
reads both. The Engram has the same name and size in both layouts but another hash; either one works with either layout, and setup downloads the pinned one
if you have none. The download default is still the four-shard files.
**On a Ryzen AI Max "Strix Halo" (Windows)** the graphics chip and the CPU share one pool of memory, so the engine counts it once: the
128 GB box (a 64 GB BIOS carve-out plus the 64 GB Windows sees) keeps about 109 GB of the 2.00 set's experts in that pool (the dense
weights and 12 GB for Windows and the desktop aside) and reads the other 23 GB from the SSD. Measured there (2.00 bpw, default Faster
+88
View File
@@ -0,0 +1,88 @@
# GDN prompt-path contract (Flash-Next, CUDA reference)
Audited on main `870aedd9e` (v0.2.8), `src/prefill/kernels.cu` and `src/prefill/prefill.cpp`. Everything below is read from
that source; the harness (`gdn_harness`, `tests/gdn/gdn_fixture.hpp`) checks it against an FP64 oracle that starts from the
raw inputs, so a wrong reading shows up as a failed parity case.
## Shapes
`S = 128` (head size), `HK = 16` key heads, `HV = 48` value heads (3 per key head, `VPK = 3`), `C = 2*HK*S + HV*S = 10240`
channels per token. Value head `vh` uses key head `qh = vh % HK` (NOT `vh / 3`; FLA groups the other way, an adapter is needed
for any comparison with FLA). One layer's prompt call has `T` tokens (the engine's prefill chunk, not the whole prompt); the
state and the convolution history carry from call to call.
## One layer, in the order the engine runs it (`prefill.cpp`, `pt.mark` stages)
| Stage (STRATA_PREFILL_TIMING name) | Work | In the harness |
|---|---|---|
| `gdn` (kPfGdn) | attn_qkv, attn_gate (z), ssm_alpha, ssm_beta projections (quantized GEMMs) | no (not a GDN kernel) |
| `gdn conv+gates` (kPfGdnConv) | `gdn_gates`, `gdn_conv` | yes, timed separately |
| `gdn recurrence` (kPfGdnRec) | `gdn_recurrence`: scan + output norm | yes |
| `gdn out proj` (kPfGdnOut) | ssm_out projection from `y16` (quantized GEMM) | no |
Note the stage names: the profile line `gdn` is the INPUT projections, `gdn out proj` is the output projection. The task's
"GDN stage = kPfGdn + Conv + Rec + Out" therefore contains two dense GEMM stages the recurrence work cannot speed up.
## Tensors (all FP32 unless noted, row-major, `t` = token in the call)
| Tensor | Layout | Meaning |
|---|---|---|
| `qkv` | `[T][C]` | projection output, channels `q (HK*S) | k (HK*S) | v (HV*S)` |
| `ab` | `[T][2*HV]` | alpha at `[t][h]`, beta input at `[t][HV+h]` |
| `dt`, `ssm_a` | `[HV]` | per-head bias and (negative) decay base |
| `conv_w` | `[C][4]` | depthwise causal 4-tap filter, tap 3 is the current token |
| `history` | `[C][3]` | the three previous raw `qkv` inputs of every channel (a state object of its own) |
| `state` | `[S][HV][S]` (row i, head, column) | recurrent state, in place; `history` follows it in the engine's layer state |
| `gate`, `beta` | `[T][HV]` | outputs of `gdn_gates` |
| `h` | `[T][C]` | outputs of `gdn_conv` (SiLU, then q and k L2-normalized per head) |
| `z` | `[T][HV*S]` | output gate input |
| `y` | `[T][HV*S]` FP32 | scratch the recurrence writes the raw output to |
| `y16` | FP16, row stride `ld16` | normalized output the out projection reads |
`ld16 = HV*S = 6144`, or `6144 + 64` when STRATA_PF_PAD is on and `T >= max(pf_switch_min_t, 64)`. A kernel must write exactly
the 6144 columns and leave the pad columns untouched (the harness fills them with a sentinel and fails if they change).
## Math (per value head, `g_t = exp(gate_t)`)
- `gate_t = softplus(alpha_t + dt) * ssm_a` (softplus is the identity above 20); `beta_t = sigmoid(beta_input_t)`
- `h_c(t) = silu(sum_k w[c][k] * x_c(t-3+k))` with `x_c(t<0)` from `history`; q and k heads are then scaled by `1/sqrt(sum^2 + 1e-6)`
- `kv = S^T k`, `delta = (v - g*kv) * beta`, `S <- g*S + k delta^T`, `o = S^T q / sqrt(128)` (uses the UPDATED state)
- `y16 = fp16( o * rsqrt(mean(o^2) + 1e-6) * gamma[col] * sigmoid(z) )`
- after the call: `history <- last three raw inputs` (older history where `T < 3`)
## State mutation rules
- A call with `T <= 0` does nothing.
- The call advances `state` and `history` in place. A path that cannot run must decline BEFORE touching the state (the helper
returns an error and "nothing was launched"); once a kernel has started, there is no replay. A forced path that is not
eligible launches nothing and reports kGdnNone with the reason (`gdn_probe.hpp`); the harness never times it.
- Calls are sequential per layer state. The chunked path's scratch is one static buffer per device (single prompt stream per
device); concurrent prompt streams on one device are not supported by it.
## Dispatch on CUDA (production, no probe)
1. `STRATA_GDN_TC=1` and `T >= 128`: `gdn_rec_tc` (tensor-core scan; sm_80+, >= 48 SMs, ~122 KB smem per block). Not mentioned in the
2026-10-26 research pack: it landed with PR #3779. FP16 operands, FP32 accumulation: mixed-precision tier.
2. `STRATA_GDN_CHUNKED=1` (`=2` skips the SM-count test) and `T >= 128`: `gdn_rec_chunked` (FP32, C = 32, scan grid `HK x S/16 = 128`
blocks of 384 threads, 93.7 KB smem; sm_80+).
3. else `gdn_rec_kh_kernel` when `gdn_keyhead_ok()` (sm_80+, all 64 blocks resident), otherwise `gdn_rec_cols_pipe_kernel`
(`STRATA_GDN_PIPELINE=0`: `gdn_rec_cols_kernel`).
4. `gdn_out_norm_kernel` after any of them (it is the part that writes `y16`).
`STRATA_GDN_REC_WARP` and `STRATA_GDN_CHUNK` exist only in `STRATA_EXPERIMENTAL_SM60` builds and act only on a cc 7.0 device at run
time (`gdn_chunk_available()`): they do nothing on sm_60, sm_86 or sm_120.
## Numerical tiers (gdn-research 06)
1. exact: output and state bitwise equal to the incumbent (`kh` and `pipe` are the same bits by construction).
2. reordered FP32 (chunked): same recurrence, other summation order. Verdict in the harness: FP64-oracle error not above
`max(floor, 3 x the incumbent's own error)` with floors 2e-3 (normalized max of the FP16 output) and 1e-4 (state); finite; the
conv history exact.
3. mixed precision (tc): same checks, plus the stress fixtures (`--stress`).
The harness prints `normalized_max = max|d| / max|ref|` and relative L2 against the oracle and against the incumbent over ALL
heads, tokens and state elements.
## Test hook
`include/strata/prefill/gdn_probe.hpp`: a thread-local `GdnProbe` installed by a test selects/forces a path, a smaller chunked scratch
segment (`gsb`), or the SM-count bypass, and reads back `selected`, `wanted` and the fallback `reason`. With no probe installed the
dispatch is unchanged. The engine never installs one.
+3 -3
View File
@@ -38,10 +38,10 @@ hardware drivers. Models and updates need an internet connection to download.
1. Open the [latest release](https://github.com/Niko1221/Strata/releases/latest).
2. Download the one installer for your system. There is one per system, not one per graphics card:
- **Windows:** run `Strata-0.2.8-windows-x64-setup.exe`. It installs for your user and needs no administrator rights.
- **Linux:** make `Strata-0.2.8-linux-x86_64.AppImage` executable, then open it. If FUSE is unavailable, run the
- **Windows:** run `Strata-0.2.9-windows-x64-setup.exe`. It installs for your user and needs no administrator rights.
- **Linux:** make `Strata-0.2.9-linux-x86_64.AppImage` executable, then open it. If FUSE is unavailable, run the
downloaded file with `--appimage-extract-and-run`. [Linux help](TROUBLESHOOTING.md).
- **Mac:** open `Strata-0.2.8-macos-arm64.dmg`, drag Strata into Applications, then open it. [Mac help](MAC.md).
- **Mac:** open `Strata-0.2.9-macos-arm64.dmg`, drag Strata into Applications, then open it. [Mac help](MAC.md).
3. On Windows and Linux the installer has no engine inside. On first start the app shows the engine for your
graphics card with its size and one **Download** button, with progress and resume. If you already downloaded the
engine file, choose **I already have the file** and it installs without internet. The Mac file keeps its small
+10 -2
View File
@@ -23,8 +23,8 @@ Use the [current release](https://github.com/Niko1221/Strata/releases/latest). Y
| System | What to install | Engine variant |
| --- | --- | --- |
| Linux | `Strata-0.2.8-linux-x86_64.AppImage` (it downloads the engine on first start), or `strata launch` | `linux-x86_64-hip-halo` |
| Windows | `Strata-0.2.8-windows-x64-setup.exe` (it downloads the engine on first start), or `strata launch` | `windows-x86_64-hip-rdna3` |
| Linux | `Strata-0.2.9-linux-x86_64.AppImage` (it downloads the engine on first start), or `strata launch` | `linux-x86_64-hip-halo` |
| Windows | `Strata-0.2.9-windows-x64-setup.exe` (it downloads the engine on first start), or `strata launch` | `windows-x86_64-hip-rdna3` |
Packaged engines carry the matching hipBLASLt tuning table for gfx1151 and select it on their own. Setup recognizes `gfx1151` and
PCI ID `1002:1586`; other integrated Radeon chips have separate support limits. Unified memory is counted once. Setup does not
@@ -214,6 +214,14 @@ export STRATA_PREFILL_STREAM_MIN=128 # prompt reads of 128-1,023 tokens on the
integrated Radeon: `STRATA_IGPU_PROMPT_SET=1` ([AMD_HIP.md](AMD_HIP.md), "Integrated GPUs"), measured on a Radeon 780M only, where
it is the default. `STRATA_PF_FUSED`, `STRATA_PF_SWITCH_MIN_T` and `STRATA_PREFILL_STREAM_MIN` stay opt-in as well.
**On Windows the Strix Halo is the exception** (engine 0.1.52 and later): `gfx1151` takes the matrix-core set by itself -
`STRATA_HIP_WMMA`, `STRATA_PF_GEMM`, `STRATA_PA_FAST`, `STRATA_HC_UPMIX` and `STRATA_PF_FUSED`, without `STRATA_SELECT_WMMA`
(x1.003 there) - and the prompt keeps its own buffers instead of lending expert-cache slots. Measured on Qwen3.8-Flash-Next
IQ3_S, prompts of 4K to 64K: x1.59 to x1.72 over 0.2.7. The whole block is off with `STRATA_GFX1151_DEFAULTS=0`; the set with
`STRATA_IGPU_PROMPT_SET=0`, the buffers with `STRATA_PREFILL_BORROW=1`
([STRIX_HALO_WINDOWS.md](STRIX_HALO_WINDOWS.md#4-what-is-on-by-default)). Linux `gfx1151` keeps the opt-in above until it is
measured there.
| Switch | What it does |
| --- | --- |
| `STRATA_PREFILL_STREAM_MIN=N` | `STRATA_PF_FUSED=1` reaches only chunks of N tokens or more (1,024 by default, the floor measured on a discrete card). With every expert in unified memory nothing streams and MMQ is the slow part, so a smaller N (128) speeds up an agent's turn (a tool result of about 1,000 tokens on a cached conversation: 3.1 to 6.0 s at 1,024 against 0.9 to 2.5 s at 128). |
+19
View File
@@ -85,6 +85,23 @@ environment and the package's `rocm/bin` folder on the library path.
about 9% faster, near-tie tokens can differ); `STRATA_HC_Q8=0` keeps the old read.
- The hipBLASLt table for `gfx1151` ships in the package and is chosen without a setting (`prefill gemm: hipBLASLt tuning enabled (32 rows,
gfx1151, version 100500)` in the log). A hand-built engine has to be pointed at it: [AMD_HIP.md](AMD_HIP.md).
- **The matrix-core prompt set is on by default on `gfx1151` on Windows** (engine 0.1.52 and the build after it): `STRATA_HIP_WMMA`,
`STRATA_PF_GEMM`, `STRATA_PA_FAST`, `STRATA_HC_UPMIX` and `STRATA_PF_FUSED`. It is the set that has been on the Radeon 780M since
021-minipc, and the start line lists it (`integrated GPU (gfx1151): the matrix-core prompt set is on ...`). Prompts on Qwen IQ3_S:
4K 688.8 -> 1,192.8 tok/s, 8K 787.0 -> 1,283.5, 12K 750.4 -> 1,256.5, 32K 775.4 -> 1,245.1, 64K 748.5 -> 1,188.9 (x1.59 to x1.72;
interleaved pairs, one engine at a time). The answers differ in the last bits: the first-token KL against the 0.2.7 default is
0.0014 mean over four prompts of 4K to 24K, top-1 the same everywhere, and the measured noise floor is 0.00000 - against the
accepted `HC_Q8` bar of 0.0228 mean. `STRATA_IGPU_PROMPT_SET=0` turns it off, `STRATA_SELECT_WMMA=1` adds the block scorer back
(it measured x1.003), a single switch can be turned off on its own (`STRATA_HC_UPMIX=0`).
- **The prompt keeps its own buffers instead of lending expert-cache slots** (`--no-prefill-borrow`, engine 0.1.52 and after, on
`gfx1151` Windows only). A lent slot's expert has to be read back from the host arena during every prompt chunk, and on this PC
that copy runs at about 0.5 GB/s (the arena is not registered with the driver in UMA mode): 823 slots took 2.75 s of a 3.3 s 4K
prompt inside the copy thread. With its own buffers every expert stays in the cache (`experts streamed 0 ... resident 24576` in the
log) and prompts are 8.5% (4K) to 8.9% (8K) faster. `STRATA_PREFILL_BORROW=1` brings lending back. `--prefill auto` keeps a
16,384-token chunk on this PC instead of falling back to 2,048: the prompt reads every expert once per chunk, so a bigger chunk reads
them fewer times (32K 1,250.6-1,260.0 -> 1,286.3-1,307.9 tok/s, 64K 1,188.4-1,191.0 -> 1,234.3-1,238.2; at 4K and 8K the chunk is
the prompt itself and nothing changes). `STRATA_GFX1151_CHUNK=0` keeps 6,144, and `--prefill auto:32768` is another ~2% at 64K for
about 12.8 GiB of prompt buffers.
- `--mmap-experts` is not needed. The first Windows guide found that without it the pinned expert arena used up the graphics memory
budget and decode fell to 28 tok/s on engine 0.1.41; the engine on this page sizes the cache from the shared pool and reaches every slot.
- DeepSeek runs in "Faster" mode here: 5 of 6 experts per word. The app and `strata launch` say so; the quality change is small.
@@ -134,3 +151,5 @@ python tools/strata_bench.py --url http://127.0.0.1:8080 --runs 3 --long
`--long` adds the 32K prompt. For DeepSeek add `--context 64k` to `strata launch` first; the default 8K context skips the 16K prompt and
the agent session.
+1 -1
View File
@@ -18,7 +18,7 @@ strata launch --status
**I cannot find `START-HERE.bat` or `SETUP.bat`.**
Open the installed Strata app, or run `strata launch`. Those wrappers are in the source repository and are not the
startup path for the app. For a fresh desktop install, download the one installer for your system
(`Strata-0.2.8-windows-x64-setup.exe`, `Strata-0.2.8-linux-x86_64.AppImage` or `Strata-0.2.8-macos-arm64.dmg`), not a
(`Strata-0.2.9-windows-x64-setup.exe`, `Strata-0.2.9-linux-x86_64.AppImage` or `Strata-0.2.9-macos-arm64.dmg`), not a
`strata-engine-…` archive.
**`strata` is not found.**
+3
View File
@@ -90,6 +90,9 @@ struct PinnedArena {
std::vector<uint64_t> slice_starts;
void* mapping_base = nullptr; ///< actual mapping start; differs from base when a shared-file header exists
uint64_t mapping_bytes = 0; ///< bytes to release from mapping_base
/// shared-file arena only: the load state word in the file's header (0: nobody has loaded it, a pid: that engine is
/// loading it, ~0: it is complete). Shared by every engine that maps the file; null for a private arena.
std::atomic<uint64_t>* shared_state = nullptr;
~PinnedArena();
PinnedArena(const PinnedArena&) = delete;
PinnedArena& operator=(const PinnedArena&) = delete;
+61
View File
@@ -0,0 +1,61 @@
// include/strata/prefill/gdn_probe.hpp - the GDN recurrence dispatch, made observable and forceable by tests.
//
// strata::prefill::gdn_recurrence picks its kernel from environment switches that are read once per process. A
// test or benchmark that links the real strata_prefill library installs a GdnProbe on its thread to
// * read back which path ran for the last call (`selected`) and why a path that was asked for did not run
// (`reason`), instead of guessing from timings, and
// * force one path for the call (`force`): a forced path that is not eligible launches NOTHING and reports
// kGdnNone plus the reason, so a harness can never time a silent fallback as if it were the candidate.
// Without a probe (the engine) the dispatch is byte-for-byte what it was: one thread-local pointer read.
// Header only; CUDA builds fill the probe (src/prefill/kernels.cu), other backends may adopt the same struct.
#pragma once
namespace strata::prefill {
enum GdnPath : int {
kGdnNone = -1, // out: nothing was launched
kGdnAuto = 0, // in: the production choice (the environment switches decide)
kGdnKeyHead = 1, // gdn_rec_kh_kernel: the three value heads of a key head in one thread (sm_80+, 64 blocks resident)
kGdnPipe = 2, // gdn_rec_cols_pipe_kernel: the software-pipelined column kernel
kGdnCols = 3, // gdn_rec_cols_kernel (STRATA_GDN_PIPELINE=0)
kGdnChunked = 4, // STRATA_GDN_CHUNKED: FP32 chunked prep + scan (CUDA, C = 32)
kGdnTc = 5, // STRATA_GDN_TC=1: chunked, scan on the tensor cores (CUDA sm_80+, 125 KB of shared memory a block)
kGdnTcSplit = 6, // the same scan with v / T / P single-buffered: 98 KB, fits the 99-KB cards (sm_86, sm_89, sm_120)
kGdnTcS2 = 7, // experiment: the split scan with 2 passes a product (lo.hi + hi.hi), not for production
kGdnTcS1 = 8, // experiment: the split scan with plain f16 operands (1 pass), not for production
kGdnPathCount = 9
};
struct GdnProbe {
// in
int force = kGdnAuto; // kGdnAuto: dispatch as production does; otherwise only this path may run
bool any_sms = false; // chunked: skip the 128-SM test, as STRATA_GDN_CHUNKED=2 does
int tc_tiles = 0; // tcs: value tiles a head (1, 2; 0 = STRATA_GDN_TC_TILES, else automatic: 2 on cards with >= 96 SMs)
int gsb = 0; // chunked: tokens per scratch super-block (a multiple of 32, at most 2048; 0 = 2048)
// out (set by every gdn_recurrence call made on the thread that installed the probe)
int selected = kGdnNone; // the path that launched
int wanted = kGdnAuto; // the path that was asked for: `force`, or the first the environment enabled
const char* reason = ""; // when selected != wanted: why the wanted path did not run
long calls = 0;
};
inline thread_local GdnProbe* g_gdn_probe = nullptr;
inline void gdn_probe_install(GdnProbe* p) { g_gdn_probe = p; }
inline GdnProbe* gdn_probe_get() { return g_gdn_probe; }
inline const char* gdn_path_name(int p) {
switch (p) {
case kGdnKeyHead: return "keyhead";
case kGdnPipe: return "pipe";
case kGdnCols: return "cols";
case kGdnChunked: return "chunked";
case kGdnTc: return "tc";
case kGdnTcSplit: return "tcs";
case kGdnTcS2: return "tcs2";
case kGdnTcS1: return "tcs1";
case kGdnAuto: return "auto";
default: return "none";
}
}
} // namespace strata::prefill
+8
View File
@@ -9,6 +9,7 @@
#include <cstddef>
#include <cstdint>
#include <string>
#include <vector>
namespace strata::prefill {
@@ -92,6 +93,13 @@ private:
void* side_ready_[2] = {};
void* side_free_[2] = {};
int side_cur_ = 0;
// STRATA_PF_W16_CACHE_MIB=N (CUDA, opt-in): native() keeps the FP16 dequantization of up to N MiB of weights, so a
// repeat product skips the dequant kernel; the FP16 values and the product are the uncached form's, bit for bit.
// A weight is keyed by its device pointer and shape (the engine's dense weights never move).
struct W16Entry { const void* p; int type; int64_t n, k; uint16_t* d; };
std::vector<W16Entry> w16_;
int64_t w16_budget_ = -1; // bytes; -1: env not read yet, 0: off
int64_t w16_used_ = 0;
void* hipblaslt_state_ = nullptr;
void* rocblas_state_ = nullptr; // HIP: the rocBLAS solution table of f16_inplace (STRATA_ROCBLAS_TUNING)
bool f16_io_ = false;
+6
View File
@@ -75,6 +75,12 @@ void gdn_recurrence(float* state, const float* h, const float* gate, const float
void gdn_recurrence_variant(int variant, float* state, const float* h, const float* gate, const float* beta,
const float* z, const float* gamma, float eps, float* y, uint16_t* y16, int64_t T,
void* stream, int64_t ld16 = 0);
/// HIP gfx12 only, STRATA_GDN_CHUNKED=1 (STRATA_GDN_FUSE=0 off): gdn_conv + gdn_recurrence on the chunked matrix-core path with
/// the conv output kept as FP16 in `hbuf` (the FP32 h buffer of gdn_conv, reused). Returns false, having launched nothing, when
/// it does not apply (not gfx12, short prompt, unaligned stride, off): the caller then calls the two.
bool gdn_conv_recurrence_fused(float* history, const float* qkv, const float* conv_w, float* hbuf, float* state, const float* gate,
const float* beta, const float* z, const float* gamma, float eps, float l2eps, float* y,
uint16_t* y16, int64_t T, void* stream, int64_t ld16 = 0);
/// KVA (CUDA/HIP only): the same recurrence kernels as gdn_recurrence, bit for bit, without the output
/// normalization: state, h, gate, beta and the scratch `y` only. The output projection and the gated residual
/// write have no consumer when only the state advances, and their input (z, the gate projection) is not needed
+18
View File
@@ -10,6 +10,8 @@
// Requires the native weights (`--native`): every quantized projection must carry its GGUF blocks.
#pragma once
#include <array>
#include "strata/core/expert_cache.hpp"
#include "strata/core/expert_source.hpp"
#include "strata/core/layer.hpp"
@@ -265,6 +267,19 @@ public:
/// Layer-major prefill (STRATA_PREFILL_LAYER_MAJOR=1): true while `on_chunk` reports a sub-chunk before the end of
/// its super-chunk. The session state is already at the super-chunk's end then, so no checkpoint may be taken.
/// On a layer split it is the last stage's (the one that reports the rows), asked through the first.
/// One prompt of a multi-prompt read (run_multi): its tokens, the position it starts at and, per stage of a layer split
/// (index 0 = the first stage), the session it reads into (nullptr / missing: that stage's own main session).
struct Seg {
const int64_t* tokens = nullptr;
int64_t n = 0;
int64_t pos0 = 0;
std::vector<core::SessionState*> stage_ss;
};
/// Several prompts in ONE layer-major pass (the streamed experts of a layer are read once for all of them). Each
/// prompt fills its own sessions; `on_chunk` / `on_stage_chunk` report the FIRST prompt only. Returns false with `err`
/// when the layer-major walk does not apply (then nothing was read; the caller reads the prompts one by one).
bool run_multi(const std::vector<Seg>& segs, std::string& err);
bool layer_major_mid() const {
const Prefill* p = this;
while (p->next_ != nullptr) p = p->next_;
@@ -282,8 +297,11 @@ private:
int64_t lm_prepare_chain(int64_t n, std::string& why);
void lm_release_chain();
bool run_layer_major(const int64_t* tokens, int64_t n, int64_t pos0, std::string& err);
struct LmSub { int seg = 0; int64_t c0 = 0, T = 0, t0 = 0; }; ///< a sub-chunk: prompt, first token, tokens, row in the super-chunk
bool lm_run(const std::vector<Seg>& segs, int depth, const std::vector<LmSub>* given_super, std::string& err);
void lm_release();
bool lm_mid_ = false;
bool lm_force_ = false; ///< run_multi: the walk even for a short total
bool lm_by_default_ = false; ///< the walk is on because nothing was set (a wider VRAM margin, the measured super-chunk size)
// Stage-1 pipeline: intermediate stages return after handing their chunk to
// the direct successor. The public run() drains the chain once at prompt end.
+93
View File
@@ -0,0 +1,93 @@
// include/strata/program/par_counters.hpp - STRATA_PAR_COUNTERS=1: counters-only parallelism accounting (research, opt-in).
//
// Off (the default), every hook is one branch on a const bool and reads no clock. On, the batch loop (one host thread)
// adds plain integers and, at the places that already wait (a drain, an admission), two clock reads. Nothing here
// synchronizes a device. Printed to stderr as "strata par: ..." with the batch summary line (when the loop goes idle):
// windows group windows launched, rows carried, rows used by live slots, padded rows, histogram of live slots per window
// drains pipeline drains before a control line, by line kind (BGEN = an admission), groups in flight, ms waited
// admissions prompts admitted into a slot: prompt tokens, ms in the prompt phase, ms of the drain before it
// idle ticks with stage 0 free and no launchable group (no live slot / every group in flight)
// Stage busy/idle with reason stays in STRATA_P4_TIMING (pipe_probe.hpp); this adds what it lacked.
#pragma once
#include <chrono>
#include <cstdint>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <string>
namespace strata::program {
/// Expert-dispatch totals (cumulative in the engine); a span's delta is printed at flush.
struct ParEx { long long misses = 0, entries = 0, hits = 0, offload = 0, pcie = 0; double run_ms = 0, jobs_ms = 0, vwait_ms = 0, vpool_ms = 0; };
class ParCounters {
public:
using Ex = ParEx;
using Clock = std::chrono::steady_clock;
ParCounters() { const char* e = std::getenv("STRATA_PAR_COUNTERS"); on_ = e != nullptr && e[0] == '1' && e[1] == '\0'; }
bool on() const { return on_; }
void window(int rows, int live) {
if (!on_) return;
++windows_; rows_ += rows; used_ += live;
++hist_[live < 0 ? 0 : (live > kHist - 1 ? kHist - 1 : live)];
}
void drain(const std::string& line, int groups_in_flight, double ms) {
if (!on_) return;
const bool adm = line.rfind("BGEN", 0) == 0;
if (adm) { ++adm_drains_; adm_drain_ms_ += ms; } else { ++other_drains_; other_drain_ms_ += ms; }
if (groups_in_flight > 0) ++drains_busy_;
groups_at_drain_ += groups_in_flight;
}
void admission(long long prompt_tokens, double prompt_ms) {
if (!on_) return;
++adm_; adm_tokens_ += prompt_tokens; adm_ms_ += prompt_ms;
}
void begin(const Ex& e, int layers) { if (on_) { ex0_ = e; layers_ = layers > 0 ? layers : 1; } }
void adapt(double ms) { if (on_) { ++adapt_; adapt_ms_ += ms; } }
void no_launch(bool any_live) { if (on_) { if (any_live) ++idle_all_inflight_; else ++idle_no_live_; } }
void flush(double wall_ms, const ParEx& e = ParEx{}) {
if (!on_ || (windows_ == 0 && adm_ == 0 && adm_drains_ + other_drains_ == 0)) return;
std::fprintf(stderr, "strata par: wall %.0f ms; windows %lld rows %lld used %lld padded %lld (%.1f%% used); live slots/window:",
wall_ms, windows_, rows_, used_, rows_ - used_, rows_ ? 100.0 * used_ / rows_ : 0.0);
for (int i = 0; i < kHist; ++i) if (hist_[i]) std::fprintf(stderr, " %d:%lld", i, hist_[i]);
std::fprintf(stderr, "; drains: admission %lld (%.0f ms) other %lld (%.0f ms), %lld with groups in flight, avg %.2f groups; "
"admissions %lld, %lld prompt tokens in %.0f ms; adapt rounds %lld %.0f ms; "
"ticks without a launch: no live slot %lld, all groups in flight %lld\n",
adm_drains_, adm_drain_ms_, other_drains_, other_drain_ms_, drains_busy_,
(adm_drains_ + other_drains_) ? (double) groups_at_drain_ / (adm_drains_ + other_drains_) : 0.0, adm_, adm_tokens_, adm_ms_,
adapt_, adapt_ms_, idle_no_live_, idle_all_inflight_);
if (windows_ > 0 && (e.misses | e.entries | e.hits | e.offload) != 0) {
const double wl = (double) windows_ * layers_;
std::fprintf(stderr, "strata par experts: per window-layer: routed entries (CPU) %.2f, distinct CPU-computed experts %.2f, "
"GPU cache hits %.2f, PCIe/offload entries %.2f, distinct PCIe experts %.2f; ms per window: CPU pool run %.2f, "
"job build %.2f, verifier wait %.2f, verifier pool %.2f (of %.2f wall)\n",
(e.entries - ex0_.entries) / wl, (e.misses - ex0_.misses) / wl, (e.hits - ex0_.hits) / wl,
(e.offload - ex0_.offload) / wl, (e.pcie - ex0_.pcie) / wl, (e.run_ms - ex0_.run_ms) / windows_,
(e.jobs_ms - ex0_.jobs_ms) / windows_, (e.vwait_ms - ex0_.vwait_ms) / windows_,
(e.vpool_ms - ex0_.vpool_ms) / windows_, wall_ms / windows_);
}
std::fflush(stderr);
reset();
}
private:
static constexpr int kHist = 18;
void reset() {
windows_ = rows_ = used_ = adm_drains_ = other_drains_ = drains_busy_ = groups_at_drain_ = adm_ = adm_tokens_ = adapt_ = 0;
idle_no_live_ = idle_all_inflight_ = 0;
adm_drain_ms_ = other_drain_ms_ = adm_ms_ = adapt_ms_ = 0;
std::memset(hist_, 0, sizeof hist_);
}
bool on_ = false;
Ex ex0_{};
int layers_ = 1;
long long windows_ = 0, rows_ = 0, used_ = 0, adm_drains_ = 0, other_drains_ = 0, drains_busy_ = 0, groups_at_drain_ = 0;
long long adm_ = 0, adm_tokens_ = 0, adapt_ = 0, idle_no_live_ = 0, idle_all_inflight_ = 0, hist_[kHist] = {};
double adm_drain_ms_ = 0, other_drain_ms_ = 0, adm_ms_ = 0, adapt_ms_ = 0;
};
} // namespace strata::program
+8 -3
View File
@@ -39,7 +39,8 @@ target_compile_definitions(mtl_rt PRIVATE
target_link_libraries(mtl_rt PUBLIC "-framework Metal" "-framework Foundation")
file(GLOB STRATA_METAL_TESTS CONFIGURE_DEPENDS ${CMAKE_CURRENT_SOURCE_DIR}/tests/*.cpp)
list(REMOVE_ITEM STRATA_METAL_TESTS ${CMAKE_CURRENT_SOURCE_DIR}/tests/runtime_hello.cpp)
list(REMOVE_ITEM STRATA_METAL_TESTS ${CMAKE_CURRENT_SOURCE_DIR}/tests/runtime_hello.cpp
${CMAKE_CURRENT_SOURCE_DIR}/tests/dsu_ref_vs_ggml.cpp) # (needs ggml: below)
foreach(src ${STRATA_METAL_TESTS})
get_filename_component(t ${src} NAME_WE)
add_executable(${t} ${src})
@@ -86,6 +87,10 @@ set(GGML_BACKEND_DL OFF CACHE BOOL "" FORCE)
set(GGML_BUILD_TESTS OFF CACHE BOOL "" FORCE)
set(GGML_BUILD_EXAMPLES OFF CACHE BOOL "" FORCE)
add_subdirectory(${STRATA_GGML_DIR}/ggml ${CMAKE_BINARY_DIR}/ggml EXCLUDE_FROM_ALL)
# ref_quant's IQ1_S / IQ1_M / Q6_K dequantizers against ggml's own (the Unsloth sets' kernels are checked against ref_quant: dsu_parity)
add_executable(dsu_ref_vs_ggml tests/dsu_ref_vs_ggml.cpp)
target_include_directories(dsu_ref_vs_ggml PRIVATE ${STRATA_GGML_DIR}/ggml/include)
target_link_libraries(dsu_ref_vs_ggml PRIVATE mtl_rt ggml-base ggml-cpu)
set(S ${STRATA_ROOT}/src)
set(STRATA_METAL_HOST_SOURCES
@@ -109,7 +114,7 @@ file(GLOB STRATA_METAL_ENGINE_SOURCES CONFIGURE_DEPENDS ${CMAKE_CURRENT_SOURCE_D
add_library(strata_metal_host STATIC ${STRATA_METAL_HOST_SOURCES})
target_include_directories(strata_metal_host PUBLIC ${STRATA_ROOT}/include ${S}/core ${STRATA_ROOT}/third_party/ggml
${STRATA_GGML_DIR}/ggml/include ${STRATA_GGML_DIR}/ggml/src ${STRATA_GGML_DIR}/ggml/src/ggml-cpu)
target_compile_definitions(strata_metal_host PUBLIC STRATA_NATIVE_EXPERTS=1 STRATA_VERSION="0.1.54-metal")
target_compile_definitions(strata_metal_host PUBLIC STRATA_NATIVE_EXPERTS=1 STRATA_VERSION="0.1.55-metal")
target_compile_options(strata_metal_host PRIVATE -Wno-unused-parameter -Wno-deprecated-declarations)
target_link_libraries(strata_metal_host PUBLIC strata_gpu_metal ggml-cpu ggml-base)
@@ -176,7 +181,7 @@ if(NOT STRATA_BUILD_ID)
set(STRATA_BUILD_ID unknown)
endif()
set_source_files_properties(${DS4}/ds4_main.cpp PROPERTIES COMPILE_DEFINITIONS "DS4_BUILD_ID=\"${STRATA_BUILD_ID}\"")
# -DSTRATA_DS4_VERSION=0.1.54-metal: the version this DeepSeek engine reports, when its package is released apart from the Qwen engine's
# -DSTRATA_DS4_VERSION=0.1.55-metal: the version this DeepSeek engine reports, when its package is released apart from the Qwen engine's
if(STRATA_DS4_VERSION)
set_source_files_properties(${DS4}/ds4_main.cpp PROPERTIES COMPILE_OPTIONS "-USTRATA_VERSION;-DSTRATA_VERSION=\"${STRATA_DS4_VERSION}\"")
endif()
+8
View File
@@ -67,3 +67,11 @@ template [[host_name("gemm_f16_64_64_4")]] kernel decltype(gemm_f16<64, 64, 4>)
template [[host_name("gemm_f16_128_64_4")]] kernel decltype(gemm_f16<128, 64, 4>) gemm_f16<128, 64, 4>;
template [[host_name("gemm_f16_64_128_4")]] kernel decltype(gemm_f16<64, 128, 4>) gemm_f16<64, 128, 4>;
template [[host_name("gemm_f16_64_32_4")]] kernel decltype(gemm_f16<64, 32, 4>) gemm_f16<64, 32, 4>;
template [[host_name("gemm_f16_128_128_4")]] kernel decltype(gemm_f16<128, 128, 4>) gemm_f16<128, 128, 4>;
template [[host_name("gemm_f16_128_128_8")]] kernel decltype(gemm_f16<128, 128, 8>) gemm_f16<128, 128, 8>;
template [[host_name("gemm_f16_128_64_8")]] kernel decltype(gemm_f16<128, 64, 8>) gemm_f16<128, 64, 8>;
template [[host_name("gemm_f16_64_64_8")]] kernel decltype(gemm_f16<64, 64, 8>) gemm_f16<64, 64, 8>;
template [[host_name("gemm_f16_256_64_8")]] kernel decltype(gemm_f16<256, 64, 8>) gemm_f16<256, 64, 8>;
template [[host_name("gemm_f16_64_256_8")]] kernel decltype(gemm_f16<64, 256, 8>) gemm_f16<64, 256, 8>;
template [[host_name("gemm_f16_128_32_4")]] kernel decltype(gemm_f16<128, 32, 4>) gemm_f16<128, 32, 4>;
template [[host_name("gemm_f16_32_128_4")]] kernel decltype(gemm_f16<32, 128, 4>) gemm_f16<32, 128, 4>;
+22 -7
View File
@@ -14,6 +14,12 @@
// `hist` and the committed one written to `hist_out` (ping-pong: the q/k channels are shared by three threadgroups).
constant int GDN_NTOK [[function_constant(2)]];
// GDN_ROWS (function constant 31, with GDN_NTOK = 1): the grid is (h_v, rows) and row y is a DIFFERENT sequence (a batch slot) with its own
// state and conv history; rinfo as in qsa.metal (header [5] state stride, [6] history stride per slot and parity; row y at [8 + 4 y] =
// {slot, cell, history parity, 0}). hist / hist_out are then one buffer (hist), the parities side by side per slot. The inputs and the
// output are row y of the window's arrays. Off: exactly the plain kernel.
constant bool GDN_ROWS_C [[function_constant(31)]];
constant bool gdn_rows = is_function_constant_defined(GDN_ROWS_C) && GDN_ROWS_C;
struct GdnArgs {
uint h_k; // 16
@@ -40,6 +46,7 @@ kernel void gdn_step_t(device float* state [[buffer(0)]], device float* hist [[b
device const float* ssm_a [[buffer(8)]], device const float* gamma [[buffer(9)]],
device float* y [[buffer(10)]], constant GdnArgs& a [[buffer(11)]],
device float* hist_out [[buffer(12)]],
device const uint* rinfo [[buffer(13), function_constant(gdn_rows)]],
uint3 tg [[threadgroup_position_in_grid]], ushort tid [[thread_index_in_threadgroup]],
ushort sg [[simdgroup_index_in_threadgroup]], ushort lane [[thread_index_in_simdgroup]]) {
constexpr int S = 128;
@@ -53,6 +60,13 @@ kernel void gdn_step_t(device float* state [[buffer(0)]], device float* hist [[b
const int cb = sg & 3, ib = sg >> 2;
const int j = 32 * cb + lane, i0 = RPT * ib;
const float scale = 1.0f / sqrt(float(S));
const int row0 = gdn_rows ? int(tg.y) : 0;
if (gdn_rows) {
const ulong sl = rinfo[8 + 4 * row0], par = rinfo[8 + 4 * row0 + 2];
state += sl * rinfo[5];
hist_out = hist + (sl * 2 + (1 - par)) * rinfo[6];
hist += (sl * 2 + par) * rinfo[6];
}
// the channel this thread convolves: 0..127 q of head hk, 128..255 k of head hk, 256..383 v of head h
int ch = -1;
@@ -70,9 +84,10 @@ kernel void gdn_step_t(device float* state [[buffer(0)]], device float* hist [[b
const int commit_at = a.commit == 1u ? GDN_NTOK : (a.commit >> 8) ? int(a.commit & 0xFFu) : 0;
for (int t = 0; t < GDN_NTOK; ++t) {
// conv + SiLU
const int tr = t + row0;
float cv = 0.0f;
if (ch >= 0) {
const float in = qkv[t * int(a.channels) + ch];
const float in = qkv[tr * int(a.channels) + ch];
const float vals[4] = {hst[0], hst[1], hst[2], in};
float s = 0.0f;
for (int k = 0; k < 4; ++k) s = fma(vals[k], cw[k], s);
@@ -86,12 +101,12 @@ kernel void gdn_step_t(device float* state [[buffer(0)]], device float* hist [[b
const float ssq = simd_sum(cv * cv);
if (lane == 0) part[sg] = ssq;
if (tid == 0) {
const float v = alpha[t * hv + h] + dt[h];
const float v = alpha[tr * hv + h] + dt[h];
// log1p(e^v) (Kahan: log(u) * e / (u - 1), u = 1 + e; exact where 1 + e rounds to 1)
const float e = exp(v), u = 1.0f + e;
const float sp = v > 20.0f ? v : (u == 1.0f ? e : log(u) * e / (u - 1.0f));
gb[0] = exp(sp * ssm_a[h]);
gb[1] = 1.0f / (1.0f + exp(-beta[t * hv + h]));
gb[1] = 1.0f / (1.0f + exp(-beta[tr * hv + h]));
}
threadgroup_barrier(mem_flags::mem_threadgroup);
if (tid < 256) {
@@ -132,14 +147,14 @@ kernel void gdn_step_t(device float* state [[buffer(0)]], device float* hist [[b
if (ib == 0) {
const float tot = (part[0] + part[1]) + (part[2] + part[3]);
const float rs = rsqrt(tot / float(S) + a.eps);
const float zz = z[t * hv * S + h * S + j];
const float zz = z[tr * hv * S + h * S + j];
const float yv = ((rs * o) * gamma[j]) * (1.0f / (1.0f + exp(-zz)));
y[t * hv * S + h * S + j] = yv;
y[tr * hv * S + h * S + j] = yv;
if (a.plane) {
const float q2s[8] = {1.0f, 0.25f, 0.0625f, 0.015625f, 0.00390625f, 0.0009765625f, 0.000244140625f,
0.00006103515625f};
const float p4s[4] = {1.0f, 0.00390625f, 1.52587890625e-05f, 5.9604644775390625e-08f};
device float* raw = y + t * hv * S + h * S + j;
device float* raw = y + tr * hv * S + h * S + j;
const int jj = j & 15;
raw[a.plane] = yv * q2s[jj & 7];
raw[2 * a.plane] = yv * p4s[jj & 3];
@@ -161,7 +176,7 @@ kernel void gdn_step_t(device float* state [[buffer(0)]], device float* hist [[b
if (a.plane && ib == 0 && (j & 15) == 0) {
float s0 = 0.0f, s1 = 0.0f;
for (int k = 0; k < 16; k += 2) { s0 += ys[j + k]; s1 += ys[j + k + 1]; }
y[4 * a.plane + t * (hv * S / 16) + (h * S + j) / 16] = s0 + s1;
y[4 * a.plane + tr * (hv * S / 16) + (h * S + j) / 16] = s0 + s1;
}
}
}
+309 -5
View File
@@ -23,6 +23,12 @@ struct GdnPArgs {
uint f32; // gdn_post: also write fp32 y
};
// function constant 7 (optional): the row stride in floats of ONE fused output [qkv | z | alpha | beta] of the four input
// projections (16480). gdn_pre then reads qkv / alpha / beta and gdn_post reads z at that stride (the buffers bound at offsets);
// undefined (every other caller): the separate buffers of channels / h_v / h_v * 128 floats a row.
constant int GDN_LD [[function_constant(7)]];
constant bool gdn_ld_on = is_function_constant_defined(GDN_LD);
static inline float gp_conv(device const float* qkv, device const float* hist, device const float* cw, int ch, int t,
int channels) {
float s = 0.0f;
@@ -52,11 +58,12 @@ kernel void gdn_pre(device const float* qkv [[buffer(0)]], device const float* h
int ch;
if (x < hk_n) ch = tid < 128 ? x * S + tid : hk_n * S + x * S + (tid - 128);
else ch = 2 * hk_n * S + (2 * (x - hk_n) + (tid >> 7)) * S + (tid & 127);
const float cv = gp_conv(qkv, hist, conv_w, ch, t, C);
const int QL = gdn_ld_on ? GDN_LD : C;
const float cv = gp_conv(qkv, hist, conv_w, ch, t, QL);
if (t == T - 1)
for (int k = 0; k < 3; ++k) {
const int src = T - 3 + k;
hist_out[ch * 3 + k] = src >= 0 ? qkv[src * C + ch] : hist[ch * 3 + (src + 3)];
hist_out[ch * 3 + k] = src >= 0 ? qkv[src * QL + ch] : hist[ch * 3 + (src + 3)];
}
if (x < hk_n) {
const float ssq = simd_sum(cv * cv);
@@ -82,10 +89,11 @@ kernel void gdn_pre(device const float* qkv [[buffer(0)]], device const float* h
const int h = 2 * (x - hk_n) + (tid >> 7);
vc[(t * hv + h) * S + (tid & 127)] = cv;
if ((tid & 127) == 0) {
const float v = alpha[t * hv + h] + dt[h];
const int AL = gdn_ld_on ? GDN_LD : hv;
const float v = alpha[t * AL + h] + dt[h];
const float e = exp(v), u = 1.0f + e;
const float sp = v > 20.0f ? v : (u == 1.0f ? e : log(u) * e / (u - 1.0f));
gb[t * hv + h] = float2(exp(sp * ssm_a[h]), 1.0f / (1.0f + exp(-beta[t * hv + h])));
gb[t * hv + h] = float2(exp(sp * ssm_a[h]), 1.0f / (1.0f + exp(-beta[t * AL + h])));
}
}
}
@@ -309,7 +317,8 @@ kernel void gdn_post(device const float* o [[buffer(0)]], device const float* z
threadgroup_barrier(mem_flags::mem_threadgroup);
const float tot = (part[0] + part[1]) + (part[2] + part[3]);
const float rs = rsqrt(tot / 128.0f + a.eps);
const float yv = ((rs * ov) * gamma[tid]) * (1.0f / (1.0f + exp(-z[i])));
const float zv = gdn_ld_on ? z[uint(t) * uint(GDN_LD) + uint(h) * 128u + tid] : z[i];
const float yv = ((rs * ov) * gamma[tid]) * (1.0f / (1.0f + exp(-zv)));
yh[i] = half(yv);
if (a.f32) y[i] = yv;
}
@@ -321,6 +330,113 @@ template [[host_name("gdn_scanr_2")]] kernel decltype(gdn_scan_r<2, 16>) gdn_sca
template [[host_name("gdn_scanr_4")]] kernel decltype(gdn_scan_r<4, 16>) gdn_scan_r<4, 16>;
template [[host_name("gdn_scanr_8")]] kernel decltype(gdn_scan_r<8, 16>) gdn_scan_r<8, 16>;
template [[host_name("gdn_scanr_4_8")]] kernel decltype(gdn_scan_r<4, 8>) gdn_scan_r<4, 8>;
template [[host_name("gdn_scanr_4_4")]] kernel decltype(gdn_scan_r<4, 4>) gdn_scan_r<4, 4>;
template [[host_name("gdn_scanr_4_2")]] kernel decltype(gdn_scan_r<4, 2>) gdn_scan_r<4, 2>;
template [[host_name("gdn_scanr_8_8")]] kernel decltype(gdn_scan_r<8, 8>) gdn_scan_r<8, 8>;
template [[host_name("gdn_scanr_8_4")]] kernel decltype(gdn_scan_r<8, 4>) gdn_scan_r<8, 4>;
template [[host_name("gdn_scanr_2_8")]] kernel decltype(gdn_scan_r<2, 8>) gdn_scan_r<2, 8>;
template [[host_name("gdn_scanr_2_4")]] kernel decltype(gdn_scan_r<2, 4>) gdn_scan_r<2, 4>;
// ---- gdn_scanf: gdn_scan_r<4, TB> with gdn_post folded in. One threadgroup a value head (grid h_v), 16 simdgroups =
// 4 column blocks x 4 row blocks, 512 threads. The scan is gdn_scan_r's, token for token (the same operations in the
// same order, so o is the same bits); a column block's row-block-0 simdgroup holds o for its 32 columns, writes them and
// their simd_sum of squares to threadgroup memory, and once every TB tokens the whole threadgroup runs gdn_post's
// arithmetic on those tokens (the sum of the four partials in gdn_post's order: the same bits as gdn_post). Outputs:
// y (fp16) and, with `f32`, fp32 y; o itself never reaches device memory.
template <int TB>
kernel void gdn_scan_f(device float* state [[buffer(0)]], device const float4* qn [[buffer(1)]],
device const float4* kn [[buffer(2)]], device const float* vc [[buffer(3)]],
device const float2* gb [[buffer(4)]], device const float* kq [[buffer(5)]],
device const float* z [[buffer(6)]], device const float* gamma [[buffer(7)]],
device half* yh [[buffer(8)]], device float* y32 [[buffer(9)]], constant GdnPArgs& a [[buffer(10)]],
uint3 tg [[threadgroup_position_in_grid]], ushort tid [[thread_index_in_threadgroup]],
ushort sg [[simdgroup_index_in_threadgroup]], ushort lane [[thread_index_in_simdgroup]]) {
constexpr int S = 128, IB = 4, R = S / IB, R4 = R / 4;
threadgroup float4 qs[TB][32], ks[TB][32];
threadgroup float2 red[2][16][32];
threadgroup float vs[TB][128];
threadgroup float4 gk[TB];
threadgroup float ot[TB][128];
threadgroup float osq[TB][4];
const int h = int(tg.x), hk = h % int(a.h_k), hv = int(a.h_v), hkn = int(a.h_k);
const int cb = sg >> 2, ib = sg & 3;
const int j = cb * 32 + lane, i0 = ib * R;
const int T = int(a.n_tok);
const float scale = 1.0f / sqrt(float(S));
float st[R];
#pragma clang loop unroll(full)
for (int i = 0; i < R; ++i) st[i] = state[((i0 + i) * hv + h) * S + j];
for (int t0 = 0; t0 < T; t0 += TB) {
const int nb = min(TB, T - t0);
threadgroup_barrier(mem_flags::mem_threadgroup);
for (int idx = tid; idx < nb * 32; idx += 512) {
const int tt = idx >> 5, w = idx & 31;
qs[tt][w] = qn[((t0 + tt) * hkn + hk) * 32 + w];
ks[tt][w] = kn[((t0 + tt) * hkn + hk) * 32 + w];
}
for (int idx = tid; idx < nb * 128; idx += 512) {
const int tt = idx >> 7, w = idx & 127;
vs[tt][w] = vc[((t0 + tt) * hv + h) * S + w];
}
for (int tt = tid; tt < nb; tt += 512) {
const float2 g_b = gb[(t0 + tt) * hv + h];
gk[tt] = float4(g_b.x, g_b.y, kq[(t0 + tt) * hkn + hk], 0.0f);
}
threadgroup_barrier(mem_flags::mem_threadgroup);
for (int tt = 0; tt < nb; ++tt) {
const int t = t0 + tt, slot = t & 1;
float ka = 0.0f, qa = 0.0f;
#pragma clang loop unroll(full)
for (int w = 0; w < R4; ++w) {
const float4 k4 = ks[tt][i0 / 4 + w], q4 = qs[tt][i0 / 4 + w];
ka = fma(st[4 * w], k4.x, ka); ka = fma(st[4 * w + 1], k4.y, ka);
ka = fma(st[4 * w + 2], k4.z, ka); ka = fma(st[4 * w + 3], k4.w, ka);
qa = fma(st[4 * w], q4.x, qa); qa = fma(st[4 * w + 1], q4.y, qa);
qa = fma(st[4 * w + 2], q4.z, qa); qa = fma(st[4 * w + 3], q4.w, qa);
}
const float4 g_b = gk[tt];
const float v = vs[tt][j];
red[slot][sg][lane] = float2(ka, qa);
threadgroup_barrier(mem_flags::mem_threadgroup);
float2 s = red[slot][cb * 4][lane];
#pragma clang loop unroll(full)
for (int b = 1; b < IB; ++b) s += red[slot][cb * 4 + b][lane];
const float g = g_b.x;
const float delta = (v - g * s.x) * g_b.y;
if (ib == 0) {
const float ov = fma(g, s.y, delta * g_b.z) * scale;
ot[tt][j] = ov;
const float ss = simd_sum(ov * ov);
if (lane == 0) osq[tt][cb] = ss;
}
#pragma clang loop unroll(full)
for (int w = 0; w < R4; ++w) {
const float4 k4 = ks[tt][i0 / 4 + w];
st[4 * w] = fma(g, st[4 * w], k4.x * delta);
st[4 * w + 1] = fma(g, st[4 * w + 1], k4.y * delta);
st[4 * w + 2] = fma(g, st[4 * w + 2], k4.z * delta);
st[4 * w + 3] = fma(g, st[4 * w + 3], k4.w * delta);
}
}
threadgroup_barrier(mem_flags::mem_threadgroup);
// gdn_post for these tokens
for (int idx = tid; idx < nb * 128; idx += 512) {
const int tt = idx >> 7, c = idx & 127;
const float tot = (osq[tt][0] + osq[tt][1]) + (osq[tt][2] + osq[tt][3]);
const float rs = rsqrt(tot / 128.0f + a.eps);
const ulong i = ulong((t0 + tt) * hv + h) * 128 + c;
const float ov = ot[tt][c];
const float yv = ((rs * ov) * gamma[c]) * (1.0f / (1.0f + exp(-z[i])));
yh[i] = half(yv);
if (a.f32) y32[i] = yv;
}
}
#pragma clang loop unroll(full)
for (int i = 0; i < R; ++i) state[((i0 + i) * hv + h) * S + j] = st[i];
}
template [[host_name("gdn_scanf_8")]] kernel decltype(gdn_scan_f<8>) gdn_scan_f<8>;
template [[host_name("gdn_scanf_4")]] kernel decltype(gdn_scan_f<4>) gdn_scan_f<4>;
template [[host_name("gdn_scanf_2")]] kernel decltype(gdn_scan_f<2>) gdn_scan_f<2>;
// ---- the chunked form (chunks of GC = 32 tokens), the WY / UT form of the gated delta rule. With G_t the product
// of the chunk's gates up to t (log space: lg_t) and S0 the state entering the chunk:
@@ -483,3 +599,191 @@ kernel void gdn_cout(device const float* qn [[buffer(0)]], device const float* k
o[((t0 + t) * hv + h) * S + j] = s * scale;
}
}
// ---- the chunked form v2: the same algebra as gdn_cprep / gdn_cstate / gdn_cout (WY / UT, chunks of GC = 32), organised for the
// GPU. Staging the chunk's k in threadgroup memory removes the redundant device loads of gdn_cprep, the state update, the
// inter-chunk and the intra-chunk products run as 8x8 simdgroup_matrix products (fp32), and the intra-chunk part is folded into
// the state kernel (no gdn_cout, no delta buffer). The chunk's q.k matrix M (with the decay factors) comes from the prep.
// gdn_cprep2 per (head, chunk): lg, A, the forward substitution for U and W, and M_ts = (q_t . k_s) G_t / G_s (s <= t)
// gdn_cstate2 per (head, 32 state columns): the chunks in order - delta = U - W S, o = (G q S + M delta) / sqrt(128),
// S = G_L S + sum_s (G_L / G_s) k_s delta_s^T. The q and k rows of the last chunk's 32 tokens are read in
// full (rows past n_tok are read and multiplied by zero): the caller's buffers hold a multiple of 32 rows.
kernel void gdn_cprep2(device const float* qn [[buffer(0)]], device const float* kn [[buffer(1)]],
device const float* vc [[buffer(2)]], device const float2* gb [[buffer(3)]],
device float* U [[buffer(4)]], device float* W [[buffer(5)]], device float* lgo [[buffer(6)]],
device float* Mo [[buffer(7)]], constant GdnPArgs& a [[buffer(8)]],
uint3 tg [[threadgroup_position_in_grid]], ushort tid [[thread_index_in_threadgroup]]) {
constexpr int S = 128;
threadgroup float4 Ks[GC * 32];
threadgroup float4 A4[GC * 8];
threadgroup float* A = (threadgroup float*)A4;
threadgroup float lg[GC], bt[GC];
const int h = int(tg.x), c = int(tg.y), hv = int(a.h_v), hkn = int(a.h_k), hk = h % hkn, T = int(a.n_tok);
const int NC = (T + GC - 1) / GC, t0 = c * GC, nb = min(GC, T - t0);
if (tid == 0) {
float s = 0.0f;
for (int t = 0; t < GC; ++t) {
if (t < nb) s += max(log(gb[(t0 + t) * hv + h].x), -80.0f);
lg[t] = s;
bt[t] = t < nb ? gb[(t0 + t) * hv + h].y : 0.0f;
}
}
device const float4* kn4 = (device const float4*)kn;
for (int idx = tid; idx < GC * 32; idx += 256) {
const int t = idx >> 5, w = idx & 31;
Ks[idx] = t < nb ? kn4[((t0 + t) * hkn + hk) * 32 + w] : float4(0.0f);
}
threadgroup_barrier(mem_flags::mem_threadgroup);
for (int idx = tid; idx < GC * GC; idx += 256) {
const int t = idx / GC, s = idx % GC;
float av = 0.0f, mv = 0.0f;
if (t < nb && s <= t && gdn_probe != 1) {
device const float4* qt = (device const float4*)(qn + ((t0 + t) * hkn + hk) * S);
float dm = 0.0f, da = 0.0f;
for (int i = 0; i < S / 4; ++i) {
const float4 ks = Ks[s * 32 + i];
dm += dot(qt[i], ks);
da += dot(Ks[t * 32 + i], ks);
}
const float f = exp(lg[t] - lg[s]);
mv = dm * f;
if (s < t) av = bt[t] * da * f;
}
A[t * GC + s] = av;
Mo[((ulong(h) * NC + c) * GC + t) * GC + s] = mv;
}
threadgroup_barrier(mem_flags::mem_threadgroup);
const int j = tid & 127;
const bool isw = tid >= 128;
threadgroup const float* Kf = (threadgroup const float*)Ks;
float x[GC];
#pragma clang loop unroll(full)
for (int t = 0; t < GC; ++t) x[t] = 0.0f;
#pragma clang loop unroll(full)
for (int t = 0; t < GC; ++t) {
float b = 0.0f;
if (t < nb) b = isw ? bt[t] * exp(lg[t]) * Kf[t * S + j] : bt[t] * vc[((t0 + t) * hv + h) * S + j];
#pragma clang loop unroll(full)
for (int s4 = 0; s4 < (t + 3) / 4; ++s4) {
const float4 ar = A4[t * 8 + s4];
b = fma(-ar.x, x[4 * s4], b);
b = fma(-ar.y, x[4 * s4 + 1], b);
b = fma(-ar.z, x[4 * s4 + 2], b);
b = fma(-ar.w, x[4 * s4 + 3], b);
}
x[t] = b;
}
device float* o = (isw ? W : U) + (ulong(h) * NC + c) * GC * S + j;
#pragma clang loop unroll(full)
for (int t = 0; t < GC; ++t) o[t * S] = x[t];
if (tid < GC) lgo[(h * NC + c) * GC + tid] = lg[tid];
}
// grid (h_v * 4), 128 threads = 4 simdgroups. A simdgroup owns rows 32 sg .. 32 sg + 31 of the 128 x 32 state block as 4 x 4
// simdgroup matrices that stay in registers for the whole call; each chunk the block is stored to threadgroup memory (Sm) once,
// where the two products that need all 128 rows (W S and Q S, sharing the S tiles) read it.
kernel void gdn_cstate2(device float* state [[buffer(0)]], device const float* qn [[buffer(1)]],
device const float* kn [[buffer(2)]], device const float* U [[buffer(3)]],
device const float* W [[buffer(4)]], device const float* lgo [[buffer(5)]],
device const float* Mo [[buffer(6)]], device float* o [[buffer(7)]], constant GdnPArgs& a [[buffer(8)]],
uint3 tg [[threadgroup_position_in_grid]], ushort tid [[thread_index_in_threadgroup]],
ushort sg [[simdgroup_index_in_threadgroup]]) {
constexpr int S = 128;
threadgroup float Sm[S][32];
threadgroup float Dl[GC][32];
threadgroup float X[GC][32];
threadgroup float Y[GC][32];
const int h = int(tg.x) / 4, hv = int(a.h_v), hkn = int(a.h_k), hk = h % hkn, T = int(a.n_tok);
const int NC = (T + GC - 1) / GC, j0 = (int(tg.x) % 4) * 32;
const float scale = 1.0f / sqrt(float(S));
simdgroup_float8x8 st[4][4];
for (int rt = 0; rt < 4; ++rt)
for (int jt = 0; jt < 4; ++jt)
simdgroup_load(st[rt][jt], state + ((32 * sg + 8 * rt) * hv + h) * S + j0 + 8 * jt, hv * S);
for (int c = 0; c < NC; ++c) {
const int t0 = c * GC, nb = min(GC, T - t0);
device const float* Uc = U + (ulong(h) * NC + c) * GC * S;
device const float* Wc = W + (ulong(h) * NC + c) * GC * S;
device const float* Mc = Mo + (ulong(h) * NC + c) * GC * GC;
device const float* lg = lgo + (h * NC + c) * GC;
for (int rt = 0; rt < 4; ++rt)
for (int jt = 0; jt < 4; ++jt) simdgroup_store(st[rt][jt], &Sm[32 * sg + 8 * rt][8 * jt], 32);
threadgroup_barrier(mem_flags::mem_threadgroup);
// P1 = W S0 (into X) and P2 = Q S0 (into Y), the S tiles read once for both
simdgroup_float8x8 acc1[4], acc2[4];
for (int jt = 0; jt < 4; ++jt) {
acc1[jt] = make_filled_simdgroup_matrix<float, 8, 8>(0.0f);
acc2[jt] = make_filled_simdgroup_matrix<float, 8, 8>(0.0f);
}
device const float* qrow = qn + ((t0 + 8 * sg) * hkn + hk) * S;
if (gdn_probe != 1)
#pragma clang loop unroll(full)
for (int kb = 0; kb < 16; ++kb) {
simdgroup_float8x8 aw, aq, bs;
simdgroup_load(aw, Wc + (8 * sg) * S + 8 * kb, S);
simdgroup_load(aq, qrow + 8 * kb, hkn * S);
for (int jt = 0; jt < 4; ++jt) {
simdgroup_load(bs, &Sm[8 * kb][8 * jt], 32);
simdgroup_multiply_accumulate(acc1[jt], aw, bs, acc1[jt]);
simdgroup_multiply_accumulate(acc2[jt], aq, bs, acc2[jt]);
}
}
for (int jt = 0; jt < 4; ++jt) {
simdgroup_store(acc1[jt], &X[8 * sg][8 * jt], 32);
simdgroup_store(acc2[jt], &Y[8 * sg][8 * jt], 32);
}
threadgroup_barrier(mem_flags::mem_threadgroup);
for (int idx = tid; idx < GC * 32; idx += 128) {
const int t = idx >> 5, j = idx & 31;
Dl[t][j] = t < nb ? Uc[t * S + j0 + j] - X[t][j] : 0.0f;
}
threadgroup_barrier(mem_flags::mem_threadgroup);
// intra = M delta (into X)
for (int jt = 0; jt < 4; ++jt) acc1[jt] = make_filled_simdgroup_matrix<float, 8, 8>(0.0f);
if (gdn_probe != 2)
#pragma clang loop unroll(full)
for (int kb = 0; kb < 4; ++kb) {
simdgroup_float8x8 am, bd;
simdgroup_load(am, Mc + (8 * sg) * GC + 8 * kb, GC);
for (int jt = 0; jt < 4; ++jt) {
simdgroup_load(bd, &Dl[8 * kb][8 * jt], 32);
simdgroup_multiply_accumulate(acc1[jt], am, bd, acc1[jt]);
}
}
for (int jt = 0; jt < 4; ++jt) simdgroup_store(acc1[jt], &X[8 * sg][8 * jt], 32);
threadgroup_barrier(mem_flags::mem_threadgroup);
const float lgl = lg[nb - 1], gl = exp(lgl);
if (gdn_probe != 4)
for (int idx = tid; idx < GC * 32; idx += 128) {
const int t = idx >> 5, j = idx & 31;
if (t < nb) {
o[((t0 + t) * hv + h) * S + j0 + j] = fma(exp(lg[t]), Y[t][j], X[t][j]) * scale;
Dl[t][j] *= exp(lgl - lg[t]);
} else {
Dl[t][j] = 0.0f;
}
}
threadgroup_barrier(mem_flags::mem_threadgroup);
// S = G_L S + K^T delta'
for (int rt = 0; rt < 4; ++rt)
for (int jt = 0; jt < 4; ++jt) {
st[rt][jt].thread_elements()[0] *= gl;
st[rt][jt].thread_elements()[1] *= gl;
}
if (gdn_probe != 3)
#pragma clang loop unroll(full)
for (int kb = 0; kb < 4; ++kb) {
simdgroup_float8x8 bd[4];
for (int jt = 0; jt < 4; ++jt) simdgroup_load(bd[jt], &Dl[8 * kb][8 * jt], 32);
for (int rt = 0; rt < 4; ++rt) {
simdgroup_float8x8 ak;
simdgroup_load(ak, kn + ((t0 + 8 * kb) * hkn + hk) * S + 32 * sg + 8 * rt, hkn * S, ulong2(0, 0), true);
for (int jt = 0; jt < 4; ++jt) simdgroup_multiply_accumulate(st[rt][jt], ak, bd[jt], st[rt][jt]);
}
}
}
for (int rt = 0; rt < 4; ++rt)
for (int jt = 0; jt < 4; ++jt)
simdgroup_store(st[rt][jt], state + ((32 * sg + 8 * rt) * hv + h) * S + j0 + 8 * jt, hv * S);
}
+75
View File
@@ -0,0 +1,75 @@
// metal/kernels/gdn_tprobe.metal - go / no-go probe for a tensor-op (MPP matmul2d) chunk-state kernel: the five products of one GDN
// chunk step (W S, Q S, M delta, K^T delta' accumulated into S) on fp16 operands, S kept in fp32 threadgroup memory, 64 chunk steps
// a threadgroup, grid h_v * 4 (the shape of gdn_cstate2). No numerics: timing only.
#include <MetalPerformancePrimitives/MetalPerformancePrimitives.h>
using namespace metal;
using namespace mpp::tensor_ops;
constant int PROBE [[function_constant(0)]];
constant int probe = is_function_constant_defined(PROBE) ? PROBE : 0; // 1 = skip the products (SIMT parts only)
kernel void gdn_tprobe(device float* out [[buffer(0)]], device half* Wh [[buffer(1)]], device half* Qh [[buffer(2)]],
device half* Kh [[buffer(3)]], device half* Mh [[buffer(4)]], constant int& NC [[buffer(5)]],
uint3 tg [[threadgroup_position_in_grid]], ushort tid [[thread_index_in_threadgroup]]) {
threadgroup half Sh[128 * 32];
threadgroup half Dh[32 * 32];
threadgroup float X[32 * 32], Y[32 * 32], Z[32 * 32];
tensor<threadgroup half, dextents<int32_t, 2>, tensor_inline> tSh(Sh, dextents<int32_t, 2>(32, 128));
tensor<threadgroup half, dextents<int32_t, 2>, tensor_inline> tDh(Dh, dextents<int32_t, 2>(32, 32));
tensor<threadgroup float, dextents<int32_t, 2>, tensor_inline> tX(X, dextents<int32_t, 2>(32, 32));
tensor<threadgroup float, dextents<int32_t, 2>, tensor_inline> tY(Y, dextents<int32_t, 2>(32, 32));
tensor<threadgroup float, dextents<int32_t, 2>, tensor_inline> tZ(Z, dextents<int32_t, 2>(32, 32));
constexpr auto d_ws = matmul2d_descriptor(32, 32, 128, false, false, false); // [32 x 128] . [128 x 32]
constexpr auto d_md = matmul2d_descriptor(32, 32, 32, false, false, false); // [32 x 32] . [32 x 32]
constexpr auto d_up = matmul2d_descriptor(128, 32, 32, true, false, false, matmul2d_descriptor::mode::multiply_accumulate); // K^T delta'
matmul2d<d_ws, execution_simdgroups<4>> op_ws;
matmul2d<d_md, execution_simdgroups<4>> op_md;
matmul2d<d_up, execution_simdgroups<4>> op_up;
const ulong hb = ulong(tg.x) / 4;
using KT = tensor<device half, dextents<int32_t, 2>, tensor_inline>;
using DT = tensor<threadgroup half, dextents<int32_t, 2>, tensor_inline>;
auto cS = op_up.get_destination_cooperative_tensor<KT, DT, float>();
#pragma clang loop unroll(full)
for (uint16_t i = 0; i < cS.get_capacity(); ++i)
if (cS.is_valid_element(i)) cS[i] = 0.001f * float(i & 15);
for (int c = 0; c < NC; ++c) {
const ulong cb = (hb * NC + c);
#pragma clang loop unroll(full)
for (uint16_t i = 0; i < cS.get_capacity(); ++i)
if (cS.is_valid_element(i)) {
auto ids = cS.get_multidimensional_index(i);
Sh[ids[1] * 32 + ids[0]] = half(cS[i]);
}
threadgroup_barrier(mem_flags::mem_threadgroup);
if (probe != 1) {
tensor<device half, dextents<int32_t, 2>, tensor_inline> tW(Wh + cb * 32 * 128, dextents<int32_t, 2>(128, 32));
tensor<device half, dextents<int32_t, 2>, tensor_inline> tQ(Qh + cb * 32 * 128, dextents<int32_t, 2>(128, 32));
op_ws.run(tW, tSh, tX);
op_ws.run(tQ, tSh, tY);
}
threadgroup_barrier(mem_flags::mem_threadgroup);
for (int i = tid; i < 32 * 32; i += 128) Dh[i] = half(0.01f * X[i]);
threadgroup_barrier(mem_flags::mem_threadgroup);
if (probe != 1) {
tensor<device half, dextents<int32_t, 2>, tensor_inline> tM(Mh + cb * 32 * 32, dextents<int32_t, 2>(32, 32));
op_md.run(tM, tDh, tZ);
}
threadgroup_barrier(mem_flags::mem_threadgroup);
for (int i = tid; i < 32 * 32; i += 128) Dh[i] = half(Z[i] + 0.01f * Y[i]);
#pragma clang loop unroll(full)
for (uint16_t i = 0; i < cS.get_capacity(); ++i)
if (cS.is_valid_element(i)) cS[i] *= 0.99f;
threadgroup_barrier(mem_flags::mem_threadgroup);
if (probe != 1) {
tensor<device half, dextents<int32_t, 2>, tensor_inline> tK(Kh + cb * 32 * 128, dextents<int32_t, 2>(128, 32));
op_up.run(tK, tDh, cS);
}
threadgroup_barrier(mem_flags::mem_threadgroup);
}
#pragma clang loop unroll(full)
for (uint16_t i = 0; i < cS.get_capacity(); ++i)
if (cS.is_valid_element(i)) {
auto ids = cS.get_multidimensional_index(i);
out[ulong(tg.x) * 4096 + ids[1] * 32 + ids[0]] = cS[i];
}
}
+29
View File
@@ -712,6 +712,35 @@ template <> struct W8<T_IQ1_M> {
}
};
// IQ1_S (Unsloth's UD-IQ1_S / UD-IQ1_M experts), ggml's dequantize_row_iq1_s: f16 d, qs[32], qh[8] u16; a 32-value sub-block has
// one scale dl = d (2 s + 1) (qh bits 12-14), a +-0.125 delta (qh bit 15) and four 8-value groups whose grid index is
// qs[4 ib + g] | 3 bits of qh << 8. Same float arithmetic as ggml (dl (grid + delta)), so the half4 is what the CPU reference rounds to.
template <typename P>
static inline half4 iq1s_w4(P row, int k) {
const P bl = row + (k >> 8) * 50;
const int ib = (k & 255) >> 5, g = (k & 31) >> 3, hh = (k & 7) >> 2;
const uint qh = uint(bl[34 + 2 * ib]) | (uint(bl[35 + 2 * ib]) << 8);
const float dl = float(as_type<half>(ushort(uint(bl[0]) | (uint(bl[1]) << 8)))) * float(2u * ((qh >> 12) & 7u) + 1u);
const float delta = (qh & 0x8000u) != 0u ? -0.125f : 0.125f;
const uint idx = uint(bl[2 + 4 * ib + g]) | (((qh >> (3u * uint(g))) & 7u) << 8);
const ulong gr = iq1s_grid[idx];
float v[4];
for (int j = 0; j < 4; ++j) {
const uint by = uint((gr >> (8u * uint(4 * hh + j))) & 0xffu);
const int c = (by & 0x80u) != 0u ? int(by) - 256 : int(by);
v[j] = dl * (float(c) + delta);
}
return half4(v[0], v[1], v[2], v[3]);
}
template <> struct W8<T_IQ1_S> {
template <typename P>
static inline void run(P row, int k, threadgroup const uint* tab, thread half4& a, thread half4& b) {
(void) tab;
a = iq1s_w4(row, k);
b = iq1s_w4(row, k + 16);
}
};
template <> struct W8<T_IQ3_S> {
template <typename P>
static inline void run(P row, int k, threadgroup const uint* tab, thread half4& a, thread half4& b) {
+37 -12
View File
@@ -35,6 +35,13 @@ constant int QSA_NTOK [[function_constant(3)]];
// the pooled indexer keys too (position pos_base + 4 b of a block). Off: exactly the plain RoPE.
constant bool QSA_MROPE [[function_constant(24)]];
constant bool qsa_mrope = is_function_constant_defined(QSA_MROPE) && QSA_MROPE;
constant bool QSA_ROWS_C [[function_constant(30)]];
constant bool qsa_rows = is_function_constant_defined(QSA_ROWS_C) && QSA_ROWS_C;
// QSA_ROWS (function constant 30): the rows of the window are DIFFERENT sequences (batch slots), one token each, each at its own
// cell and in its own slot of the caches. A buffer rinfo (uint) carries the strides between the slots' caches, in elements:
// [0] kq / vq (chars), [1] ksc / vsc (halves), [2] tail, [3] dead, [4] pooled (floats), [5] GDN state (floats), [6] conv history
// (floats, per slot and parity), [7] PLE history (floats); then row t at [8 + 4 t] = {slot, cell, history parity, 0}. Off: exactly the
// plain kernels.
struct QsaArgs {
uint pos0; // the window's first cell (relative to the sequence)
@@ -92,6 +99,7 @@ kernel void qsa_prep(device const float* qfull [[buffer(0)]], device const float
device half* vsc [[buffer(15)]], device float* tail [[buffer(16)]], device float* dead [[buffer(17)]],
device float* pooled [[buffer(18)]], constant QsaArgs& a [[buffer(19)]],
device const int* mtab [[buffer(20)]],
device const uint* rinfo [[buffer(21), function_constant(qsa_rows)]],
uint3 tg [[threadgroup_position_in_grid]], ushort tid [[thread_index_in_threadgroup]],
ushort sg [[simdgroup_index_in_threadgroup]], ushort lane [[thread_index_in_simdgroup]]) {
threadgroup float part[8];
@@ -102,27 +110,38 @@ kernel void qsa_prep(device const float* qfull [[buffer(0)]], device const float
// the indexer keys, every window token in order (only threadgroup (32, 0) works)
if (tg.y != 0) return;
for (int t = 0; t < int(a.n_tok); ++t) {
const uint cell = a.pos0 + uint(t);
uint cell = a.pos0 + uint(t);
device float* tail_t = tail; device float* dead_t = dead; device float* pooled_t = pooled;
if (qsa_rows) {
const ulong sl = rinfo[8 + 4 * t];
cell = rinfo[8 + 4 * t + 1];
tail_t = tail + sl * rinfo[2]; dead_t = dead + sl * rinfo[3]; pooled_t = pooled + sl * rinfo[4];
}
const float x = tid < ID ? ikraw[t * ID + tid] : 0.0f;
if (tid < ID) tail[(cell & 3) * ID + tid] = x;
if (tid < ID) tail_t[(cell & 3) * ID + tid] = x;
threadgroup_barrier(mem_flags::mem_device);
if (cell == 0) {
const float o = norm_rope(x, 0.0f, ikn, row, part, tid, sg, lane, ID, a.eps, cs, 0, false, mtab);
if (tid < ID) dead[tid] = o;
if (tid < ID) dead_t[tid] = o;
}
if ((cell & 3) == 3) {
float m = 0.0f;
if (tid < ID) m = (((tail[tid] + tail[ID + tid]) + tail[2 * ID + tid]) + tail[3 * ID + tid]) * 0.25f;
if (tid < ID) m = (((tail_t[tid] + tail_t[ID + tid]) + tail_t[2 * ID + tid]) + tail_t[3 * ID + tid]) * 0.25f;
const uint b = cell >> 2;
const float o = norm_rope(m, 0.0f, ikn, row, part, tid, sg, lane, ID, a.eps, cs, a.pos_base + 4 * b, true, mtab);
if (tid < ID) pooled[b * ID + tid] = o;
if (tid < ID) pooled_t[b * ID + tid] = o;
}
threadgroup_barrier(mem_flags::mem_device);
}
return;
}
const int t = int(tg.y);
const uint cell = a.pos0 + uint(t), absp = a.pos_base + cell;
const uint cell = qsa_rows ? rinfo[8 + 4 * t + 1] : a.pos0 + uint(t), absp = a.pos_base + cell;
device char* kqr = kq; device char* vqr = vq; device half* kscr = ksc; device half* vscr = vsc;
if (qsa_rows) {
const ulong sl = rinfo[8 + 4 * t];
kqr = kq + sl * rinfo[0]; vqr = vq + sl * rinfo[0]; kscr = ksc + sl * rinfo[1]; vscr = vsc + sl * rinfo[1];
}
if (r < QH) {
const float x = qfull[(t * QH + r) * 2 * QD + tid];
const float o = norm_rope(x, 0.0f, qn, row, part, tid, sg, lane, QD, a.eps, cs, absp, true, mtab);
@@ -143,8 +162,8 @@ kernel void qsa_prep(device const float* qfull [[buffer(0)]], device const float
int q = 0;
if (sf > 0.0f) q = clamp(int(rint(x / sf)), -127, 127);
const uint rowi = cell * QKV + uint(h);
(isv ? vq : kq)[rowi * QD + tid] = char(q);
if ((tid & 63) == 0) (isv ? vsc : ksc)[rowi * 4 + g] = sh;
(isv ? vqr : kqr)[rowi * QD + tid] = char(q);
if ((tid & 63) == 0) (isv ? vscr : kscr)[rowi * 4 + g] = sh;
} else {
const int h = r - QH - 2 * QKV;
const float x = tid < ID ? iqraw[(t * IH + h) * ID + tid] : 0.0f;
@@ -157,13 +176,16 @@ kernel void qsa_prep(device const float* qfull [[buffer(0)]], device const float
kernel void qsa_score(device const float* qidx [[buffer(0)]], device const float* pooled [[buffer(1)]],
device const float* dead [[buffer(2)]], device float* scores [[buffer(3)]],
constant QsaArgs& a [[buffer(4)]],
device const uint* rinfo [[buffer(5), function_constant(qsa_rows)]],
uint3 tg [[threadgroup_position_in_grid]], ushort sg [[simdgroup_index_in_threadgroup]],
ushort lane [[thread_index_in_simdgroup]]) {
const int t = int(tg.y);
const uint n_kv = a.pos0 + uint(t) + 1, n_bid = n_kv >> 2;
const uint n_kv = (qsa_rows ? rinfo[8 + 4 * t + 1] : a.pos0 + uint(t)) + 1, n_bid = n_kv >> 2;
const uint b = tg.x * 8 + sg;
if (b > n_bid) return;
device const float* key = b == n_bid ? dead : pooled + b * ID;
device const float* pooled_s = pooled; device const float* dead_s = dead;
if (qsa_rows) { const ulong sl = rinfo[8 + 4 * t]; pooled_s = pooled + sl * rinfo[4]; dead_s = dead + sl * rinfo[3]; }
device const float* key = b == n_bid ? dead_s : pooled_s + b * ID;
float k[4];
for (int i = 0; i < 4; ++i) k[i] = key[lane + 32 * i];
float s = 0.0f;
@@ -275,6 +297,7 @@ kernel void qsa_attn(device const float* qcur [[buffer(0)]], device const char*
device const char* vq [[buffer(2)]], device const half* ksc [[buffer(3)]],
device const half* vsc [[buffer(4)]], device const int* ids [[buffer(5)]],
device float* part [[buffer(6)]], constant QsaArgs& a [[buffer(7)]],
device const uint* rinfo [[buffer(8), function_constant(qsa_rows)]],
uint3 tg [[threadgroup_position_in_grid]], ushort tid [[thread_index_in_threadgroup]],
ushort sg [[simdgroup_index_in_threadgroup]], ushort lane [[thread_index_in_simdgroup]]) {
constexpr int HPG = QH / QKV; // 12 query heads per kv head
@@ -282,7 +305,8 @@ kernel void qsa_attn(device const float* qcur [[buffer(0)]], device const char*
threadgroup float s[HPG][QCH];
threadgroup int cid[QCH];
const int chunk = int(tg.x), g = int(tg.y), t = int(tg.z);
const uint n_kv = a.pos0 + uint(t) + 1, width = min(n_kv, uint(QBUDGET));
const uint n_kv = (qsa_rows ? rinfo[8 + 4 * t + 1] : a.pos0 + uint(t)) + 1, width = min(n_kv, uint(QBUDGET));
if (qsa_rows) { const ulong sl = rinfo[8 + 4 * t]; kq += sl * rinfo[0]; vq += sl * rinfo[0]; ksc += sl * rinfo[1]; vsc += sl * rinfo[1]; }
const int c0 = chunk * QCH;
const int nchunks = (QBUDGET + QCH - 1) / QCH;
device float* pp = part + ((uint(t) * nchunks + chunk) * QH) * (QD + 2);
@@ -345,11 +369,12 @@ kernel void qsa_attn(device const float* qcur [[buffer(0)]], device const char*
// grid (QH, n_tok), 256 threads: out = merge * sigmoid(gate), as prepared activations of n = QH * QD per token
kernel void qsa_merge(device const float* part [[buffer(0)]], device const float* qfull [[buffer(1)]],
device float* xp [[buffer(2)]], constant QsaArgs& a [[buffer(3)]],
device const uint* rinfo [[buffer(4), function_constant(qsa_rows)]],
uint3 tg [[threadgroup_position_in_grid]], ushort tid [[thread_index_in_threadgroup]]) {
threadgroup float msum[256];
const int h = int(tg.x), t = int(tg.y);
const int nchunks = (QBUDGET + QCH - 1) / QCH;
const uint n_kv = a.pos0 + uint(t) + 1, width = min(n_kv, uint(QBUDGET));
const uint n_kv = (qsa_rows ? rinfo[8 + 4 * t + 1] : a.pos0 + uint(t)) + 1, width = min(n_kv, uint(QBUDGET));
const int nc = int((width + QCH - 1) / QCH);
device const float* pp = part + (uint(t) * nchunks * QH + uint(h)) * (QD + 2);
float M = -INFINITY;
+2 -1
View File
@@ -120,13 +120,14 @@ kernel void qsa_merge2(device const float* part [[buffer(0)]], device const floa
kernel void qsa_topk2(device const float* scores [[buffer(0)]], device int* ids [[buffer(1)]],
constant QsaArgs& a [[buffer(2)]],
device const uint* rinfo [[buffer(3), function_constant(qsa_rows)]],
uint3 tg [[threadgroup_position_in_grid]], ushort tid [[thread_index_in_threadgroup]],
ushort sg [[simdgroup_index_in_threadgroup]], ushort lane [[thread_index_in_simdgroup]]) {
threadgroup atomic_uint hist[256];
threadgroup uint sgs[32], sgs2[32];
threadgroup uint sel[2];
const int t = int(tg.x);
const uint n_kv = a.pos0 + uint(t) + 1, n_bid = n_kv >> 2, nb = n_bid + 1;
const uint n_kv = (qsa_rows ? rinfo[8 + 4 * t + 1] : a.pos0 + uint(t)) + 1, n_bid = n_kv >> 2, nb = n_bid + 1;
device int* out = ids + t * QBUDGET;
device const float* sc = scores + uint(t) * (a.max_cells / 4 + 2);
if (n_kv <= QBUDGET) {
+3
View File
@@ -25,6 +25,7 @@
#define T_IQ3_XXS 18
#define T_IQ4_NL 20
#define T_IQ3_S 21
#define T_IQ1_S 19 // Unsloth UD-IQ1_S / UD-IQ1_M experts (50-byte blocks)
#define T_IQ1_M 29 // the 2.00 bpw pack's 1.75-bit type
#define T_IQ2_S 22
#define T_IQ4_XS 23
@@ -41,6 +42,7 @@ template <> struct fmt<T_Q2_0R> { enum { QK = 64, BB = 18, UNIT = 16, TAB =
template <> struct fmt<T_IQ3_XXS> { enum { QK = 256, BB = 98, UNIT = 16, TAB = 256 + 32 }; };
template <> struct fmt<T_IQ2_XXS> { enum { QK = 256, BB = 66, UNIT = 16, TAB = 512 + 32 }; };
template <> struct fmt<T_IQ1_M> { enum { QK = 256, BB = 56, UNIT = 16, TAB = 1 }; };
template <> struct fmt<T_IQ1_S> { enum { QK = 256, BB = 50, UNIT = 16, TAB = 1 }; };
template <> struct fmt<T_IQ2_XS> { enum { QK = 256, BB = 74, UNIT = 16, TAB = 1024 + 32 }; };
template <> struct fmt<T_IQ2_S> { enum { QK = 256, BB = 82, UNIT = 16, TAB = 2048 + 32 }; };
template <> struct fmt<T_IQ3_S> { enum { QK = 256, BB = 110, UNIT = 16, TAB = 512 + 32 }; };
@@ -393,6 +395,7 @@ template <> inline void load_tab<T_IQ2_XS>(threadgroup uint* tab, ushort tid, us
}
// IQ1_M: no threadgroup table - the tile reads iq1s_grid from the constants (the w4 helper in moe_mm.metal)
template <> inline void load_tab<T_IQ1_M>(threadgroup uint*, ushort, ushort) {}
template <> inline void load_tab<T_IQ1_S>(threadgroup uint*, ushort, ushort) {} // (same: iq1s_grid from the constants)
template <> inline void load_tab<T_IQ2_S>(threadgroup uint* tab, ushort tid, ushort nth) {
if (ctab) return;
for (int i = tid; i < 1024; i += nth) { tab[2 * i] = uint(iq2s_grid[i]); tab[2 * i + 1] = uint(iq2s_grid[i] >> 32); }
+8 -6
View File
@@ -746,10 +746,12 @@ bool iq_mv(const void* jobs, int nj) {
const char* tn = nullptr;
switch (q.type) {
case 13: tn = "ds4_q5k_mvn"; break;
case 14: tn = "ds4_q6k_mvn"; break; // (the Unsloth sets' Q6_K dense matrices)
case 16: tn = "ds4_iq2xxs_mvn"; break;
case 17: tn = "ds4_iq2xs_mvn"; break;
case 18: tn = "ds4_iq3xxs_mvn"; break;
case 22: tn = "ds4_iq2s_mvn"; break;
case 19: tn = "ds4_iq1s_mvn"; break; // (Unsloth UD-IQ1_S / UD-IQ1_M experts: ds4_w32_iq1s)
case 29: tn = "ds4_iq1m_mvn"; break; // (the 2.00 bpw pack's IQ1_M: ds4_iq.metal's ds4_w32_iq1m)
default: break;
}
@@ -2328,13 +2330,13 @@ bool rows_f16(const float* x, int ldx, const int* idx, int nrows, int ncols, uin
return true;
}
// kind: 0 BF16 values, 8 Q8_0, 13 Q5_K. n rows of k values, ld the row stride (bytes; values for BF16), out half rows.
// kind: 0 BF16 values, 8 Q8_0, 13 Q5_K, 14 Q6_K. n rows of k values, ld the row stride (bytes; values for BF16), out half rows.
bool dequant_rows(int kind, const void* raw, size_t ld, int k, int n, uint16_t* out) {
if (n <= 0 || k <= 0) return true;
if (!ensure()) return false;
const char* fn = kind == 13 ? "ds4_dq_q5k" : kind == 8 ? "ds4_dq_q8" : kind == 0 ? "ds4_dq_bf16" : nullptr;
const char* fn = kind == 13 ? "ds4_dq_q5k" : kind == 14 ? "ds4_dq_q6k" : kind == 8 ? "ds4_dq_q8" : kind == 0 ? "ds4_dq_bf16" : nullptr;
if (!fn) return false;
if ((kind == 13 && k % 256 != 0) || (kind == 8 && k % 32 != 0)) return false;
if (((kind == 13 || kind == 14) && k % 256 != 0) || (kind == 8 && k % 32 != 0)) return false;
void* hw = strata::gpu::host_view((void*) raw);
void* ho = strata::gpu::host_view(out);
if (!hw || !ho) return false;
@@ -2345,7 +2347,7 @@ bool dequant_rows(int kind, const void* raw, size_t ld, int k, int n, uint16_t*
return g_cx->wrap((void*) base, wrap_len(base, end - base));
};
auto off = [&](void* host) { return (size_t) ((uintptr_t) host - (((uintptr_t) host) & ~(uintptr_t) (page - 1))); };
const size_t wbytes = (size_t) (n - 1) * ld * (kind == 0 ? 2 : 1) + (kind == 0 ? (size_t) k * 2 : (kind == 13 ? (size_t) (k / 256) * 176 : (size_t) (k / 32) * 34));
const size_t wbytes = (size_t) (n - 1) * ld * (kind == 0 ? 2 : 1) + (kind == 0 ? (size_t) k * 2 : (kind == 13 ? (size_t) (k / 256) * 176 : kind == 14 ? (size_t) (k / 256) * 210 : (size_t) (k / 32) * 34));
if (!std::getenv("DS4_NO_TOUCH")) { // (the matrix may be file-backed pages not read yet: the GPU does not page them in, it stalls)
volatile unsigned char sink = 0;
for (size_t o = 0; o < wbytes; o += 4096) sink = sink + ((const volatile unsigned char*) hw)[o];
@@ -2356,7 +2358,7 @@ bool dequant_rows(int kind, const void* raw, size_t ld, int k, int n, uint16_t*
if (!bw.handle || !bo.handle) return false;
mtl::Pipeline pl;
try { pl = g_cx->pipeline(g_lib, fn); } catch (...) { return false; }
size_t items = kind == 13 ? (size_t) n * (k / 256) * 8 : kind == 8 ? (size_t) n * (k / 32) : (size_t) n * k;
size_t items = kind == 13 ? (size_t) n * (k / 256) * 8 : kind == 14 ? (size_t) n * (k / 256) * 64 : kind == 8 ? (size_t) n * (k / 32) : (size_t) n * k;
mtl::Dispatch d;
d.p = &pl;
d.args = {mtl::Arg::B(bw, off(hw)), mtl::Arg::V((unsigned long) ld), mtl::Arg::V((unsigned) k), mtl::Arg::V((unsigned) n),
@@ -2454,7 +2456,7 @@ int g_lib_moe = -2;
mtl::Buffer g_mm_xs, g_mm_hs, g_mm_ys; // grow-only scratch: the sorted rows' half activations; the gate/up's swiglu rows (the down's input)
bool g_mm_hs_ok = false;
// bytes of a 256-value block per type: the tile kernels are instantiated for exactly these (DS4_MM_INST)
size_t iq_bs(int type) { return type == 16 ? 66 : type == 17 ? 74 : type == 22 ? 82 : type == 18 ? 98 : type == 29 ? 56 : 0; }
size_t iq_bs(int type) { return type == 16 ? 66 : type == 17 ? 74 : type == 22 ? 82 : type == 18 ? 98 : type == 29 ? 56 : type == 19 ? 50 : 0; }
struct DsMmArgsH {
unsigned long gate_off, up_off, down_off, stride;
unsigned gu_row, d_row, n_embd, n_ff;
+30
View File
@@ -2706,6 +2706,36 @@ kernel void ds4_dq_q5k(device const uchar* W [[buffer(0)]], constant ulong& ld [
}
}
// Q6_K (210-byte blocks: ql[128], qh[64], int8 scales[16], f16 d; the Unsloth sets' attention, shared-expert, head and engram_kv
// matrices): a thread four values of a block (two 128-value halves x 32 lanes), ggml's dequantize_row_q6_K arithmetic
// ((d * scale) * (q - 32), in this order) -> out[n][k] half
kernel void ds4_dq_q6k(device const uchar* W [[buffer(0)]], constant ulong& ld [[buffer(1)]], constant uint& k [[buffer(2)]],
constant uint& n [[buffer(3)]], device half* out [[buffer(4)]], uint gid [[thread_position_in_grid]]) {
const uint nb = k / 256u;
const ulong total = (ulong) n * nb * 64ul;
if (ulong(gid) >= total) return;
const uint l = gid & 31u;
const uint hf = (gid >> 5) & 1u;
const uint blk = (gid >> 6) % nb;
const uint row = uint(ulong(gid) / (64ul * nb));
const device uchar* p = W + (ulong) row * ld + (ulong) blk * 210ul;
const float d = float(as_type<half>(*(const device ushort*) (p + 208)));
const device uchar* ql = p + 64u * hf;
const device uchar* qh = p + 128u + 32u * hf;
const device char* sc = (const device char*) (p + 192u + 8u * hf);
const uint is = l >> 4;
const uint qlo = uint(ql[l]), qhi = uint(ql[l + 32u]), h = uint(qh[l]);
const int q1 = int((qlo & 0xfu) | (((h >> 0) & 3u) << 4)) - 32;
const int q2 = int((qhi & 0xfu) | (((h >> 2) & 3u) << 4)) - 32;
const int q3 = int((qlo >> 4) | (((h >> 4) & 3u) << 4)) - 32;
const int q4 = int((qhi >> 4) | (((h >> 6) & 3u) << 4)) - 32;
device half* o = out + (ulong) row * k + (ulong) blk * 256ul + 128ul * hf + l;
o[0] = half(d * float(sc[is + 0u]) * float(q1));
o[32] = half(d * float(sc[is + 2u]) * float(q2));
o[64] = half(d * float(sc[is + 4u]) * float(q3));
o[96] = half(d * float(sc[is + 6u]) * float(q4));
}
// Q8_0: 34-byte blocks (fp16 d, 32 int8)
kernel void ds4_dq_q8(device const uchar* W [[buffer(0)]], constant ulong& ld [[buffer(1)]], constant uint& k [[buffer(2)]],
constant uint& n [[buffer(3)]], device half* out [[buffer(4)]], uint gid [[thread_position_in_grid]]) {
+97 -5
View File
@@ -198,8 +198,9 @@ kernel void ds4_iq_mv_k(device const uchar* jobs [[buffer(0)]], uint2 tgp [[thre
const int s0 = int(((const device char*) scl)[8u * h + 2u * quarter]);
const int s1 = int(((const device char*) scl)[8u * h + 2u * quarter + 1u]);
for (uint t = 0; t < 8u; ++t) {
const uint x = ((*(const device uint*) (qlp + 4u * t) >> sh) & 0x0f0f0f0fu) |
(((*(const device uint*) (qhp + 4u * t) >> (2u * quarter)) & 0x03030303u) << 4);
// (packed_uchar4: a 210-byte block starts only 2-byte aligned - a plain uint load reads the wrong word on odd blocks)
const uint x = ((as_type<uint>(uchar4(*(const device packed_uchar4*) (qlp + 4u * t))) >> sh) & 0x0f0f0f0fu) |
(((as_type<uint>(uchar4(*(const device packed_uchar4*) (qhp + 4u * t))) >> (2u * quarter)) & 0x03030303u) << 4);
const uint off = blk * 256u + ib * 32u + t * 4u;
const float s = float(t < 4u ? s0 : s1);
for (uint c = 0; c < 16u; ++c) {
@@ -264,9 +265,13 @@ struct Ds4IqSub {
// the block bytes of the types these kernels take: 16 IQ2_XXS, 17 IQ2_XS, 18 IQ3_XXS, 22 IQ2_S (ds4_iq.cu's Ty<T>::B)
static inline uint ds4_iq_bs(int type) {
return type == 16 ? 66u : type == 17 ? 74u : type == 18 ? 98u : 82u;
return type == 16 ? 66u : type == 17 ? 74u : type == 18 ? 98u : type == 19 ? 50u : type == 29 ? 56u : 82u;
}
// the 1-bit types' sub-block decoders (defined with the multi-column matvec below): 32 codes as 8*grid +- 1 (int8) and the two halves' scales
static inline void ds4_w32_iq1s(const device uchar* p, uint ib, thread int (&w)[32], thread float& db0, thread float& db1);
static inline void ds4_w32_iq1m(const device uchar* p, uint ib, thread int (&w)[32], thread float& db0, thread float& db1);
// ds4_iq.cu:139 decode<T>: the block's 8 sub-blocks, kb's is the (kb & 7)'th; the grids are ggml-common.h's
static inline Ds4IqSub ds4_iq_sub(device const uchar* row, uint bs, int kb, int type) {
Ds4IqSub s;
@@ -275,6 +280,15 @@ static inline Ds4IqSub ds4_iq_sub(device const uchar* row, uint bs, int kb, int
const float d = float(as_type<half>(*(const device ushort*) p));
uint idx[4], sm[4];
float db0, db1;
if (type == 19 || type == 29) { // IQ1_S (50 B) / IQ1_M (56 B): the integer form 8*code +- 1, the scale / 8 in db
int w[32];
if (type == 19) ds4_w32_iq1s(p, uint(ib), w, db0, db1); else ds4_w32_iq1m(p, uint(ib), w, db0, db1);
for (uint t = 0; t < 8u; ++t)
s.q[t] = (w[4u * t] & 0xff) | ((w[4u * t + 1u] & 0xff) << 8) | ((w[4u * t + 2u] & 0xff) << 16) | ((w[4u * t + 3u] & 0xff) << 24);
s.s0 = db0;
s.s1 = db1;
return s;
}
if (type == 18) { // IQ3_XXS: d, qs[64] grid bytes (4 a sub-block), scales[8] (the top nibble the group's s)
const device uchar* qs = p + 2;
const uint w0 = uint(qs[8 * ib]) | (uint(qs[8 * ib + 1]) << 8) | (uint(qs[8 * ib + 2]) << 16) |
@@ -939,6 +953,60 @@ kernel void ds4_q5k_mvn(device const uchar* jobs [[buffer(0)]], uint2 tgp [[thre
ds4_mvn_store<NC>(b, row, lane, acc);
}
// Q6_K (210-byte blocks; the Unsloth sets' attention, shared-expert, head and engram_kv matrices) for NC activations: four lanes a
// block, a lane one 128-value half and one of its two quarter pairs (quarters p and p + 2 share the 32 ql bytes at 32 p: the low
// nibbles are quarter p, the high ones p + 2; quarter q's 2 high bits are qh >> 2 q), eight blocks at once. The loads go through
// packed_uchar4: a block starts only 2-byte aligned. ggml's dequantize_row_q6_K arithmetic: d * scale * (q - 32).
template <uint NC>
kernel void ds4_q6k_mvn(device const uchar* jobs [[buffer(0)]], uint2 tgp [[threadgroup_position_in_grid]],
uint tid [[thread_index_in_threadgroup]], ushort lane [[thread_index_in_simdgroup]]) {
const device Ds4MvJob& b = *(const device Ds4MvJob*) (jobs + (size_t) tgp.y * sizeof(Ds4MvJob));
const uint row = tgp.x * 8u + (tid >> 5);
if (int(row) >= b.n) return;
const uint nblk = uint(b.k) / 256u, nsb = uint(b.k) / 32u;
const device uchar* w = b.w + (size_t) row * b.ld;
const uint j = lane & 3u, h = j >> 1, pr = j & 1u;
const uint bsel = lane >> 2;
float acc[NC];
for (uint c = 0; c < NC; ++c) acc[c] = 0.0f;
for (uint blk = bsel; blk < nblk; blk += 8u) {
const device uchar* p = w + (size_t) blk * 210u;
const float d = float(as_type<half>(*(const device ushort*) (p + 208)));
const device uchar* ql = p + 64u * h + 32u * pr;
const device uchar* qh = p + 128u + 32u * h;
const device char* scl = (const device char*) (p + 192u + 8u * h);
uint qlw[8], qhw[8];
for (uint t = 0; t < 8u; ++t) {
qlw[t] = as_type<uint>(uchar4(*(const device packed_uchar4*) (ql + 4u * t)));
qhw[t] = as_type<uint>(uchar4(*(const device packed_uchar4*) (qh + 4u * t)));
}
for (uint qi = 0; qi < 2u; ++qi) {
const uint q = pr + 2u * qi;
const uint sh = 4u * qi;
int cw[32];
for (uint t = 0; t < 8u; ++t) {
const uint x = ((qlw[t] >> sh) & 0x0f0f0f0fu) | (((qhw[t] >> (2u * q)) & 0x03030303u) << 4);
for (uint i = 0; i < 4u; ++i) cw[t * 4u + i] = int((x >> (8u * i)) & 0xffu) - 32;
}
const float s0 = float(scl[2u * q]), s1 = float(scl[2u * q + 1u]);
const uint g = blk * 8u + 4u * h + q;
for (uint c = 0; c < NC; ++c) {
const device uint4* xq4 = (const device uint4*) (b.xq + (size_t) c * b.k + (size_t) blk * 256u + 128u * h + 32u * q);
const uint4 xa = xq4[0], xb = xq4[1];
const uint xw[8] = {xa.x, xa.y, xa.z, xa.w, xb.x, xb.y, xb.z, xb.w};
int sa = 0, sb = 0;
for (uint t = 0; t < 8u; ++t) {
int part = 0;
for (uint i = 0; i < 4u; ++i) part += cw[t * 4u + i] * int(char(xw[t] >> (8u * i)));
if (t < 4u) sa += part; else sb += part;
}
acc[c] = fma(b.xd[(size_t) c * nsb + g] * d, s0 * float(sa) + s1 * float(sb), acc[c]);
}
}
}
ds4_mvn_store<NC>(b, row, lane, acc);
}
// the grid-coded types: decode the sub-block's 32 weights (signs applied) into w[32] ...
static inline void ds4_w32_iq2xxs(const device uchar* p, uint ib, thread int (&w)[32], thread float& db) {
const float d = float(as_type<half>(*(const device ushort*) p));
@@ -1011,6 +1079,27 @@ static inline void ds4_w32_iq3xxs(const device uchar* p, uint ib, thread int (&w
}
// IQ1_S (UD-IQ1_S / UD-IQ1_M experts; 50-byte blocks): f16 d, qs[32] (4 grid bytes a 32-value sub-block), qh[8] u16 (3 bits a
// group of 8 more grid index, bits 12-14 the sub-block's 3-bit scale, bit 15 the sign of the +-0.125 delta). ggml's
// dequantize_row_iq1_s: dl = d (2 s + 1), w = dl (grid + delta). The integer form here (8 grid +- 1, scale / 8) is the same
// number with no rounding difference.
static inline void ds4_w32_iq1s(const device uchar* p, uint ib, thread int (&w)[32], thread float& db0, thread float& db1) {
const float d = float(as_type<half>(*(const device ushort*) p));
const uint qh = uint(p[34u + 2u * ib]) | (uint(p[35u + 2u * ib]) << 8);
db0 = d * float(2u * ((qh >> 12) & 7u) + 1u) * 0.125f;
db1 = db0;
const int dlt = (qh & 0x8000u) != 0u ? -1 : 1;
for (uint l = 0; l < 4u; ++l) {
const uint idx = uint(p[2u + 4u * ib + l]) | (((qh >> (3u * l)) & 7u) << 8);
const ulong g = iq1s_grid[idx];
for (uint j = 0; j < 8u; ++j) {
const uint code = uint((g >> (8u * j)) & 0xffu);
const int c8 = (code & 0x80u) != 0u ? int(code) - 256 : int(code); // -1 / 0 / +1
w[l * 8u + j] = 8 * c8 + dlt;
}
}
}
// IQ1_M (the 2.00 bpw pack's 1.75-bit type): the block has no d field - its F16 scale is the top nibbles of its four
// u16 scales - and each 32-value sub-block splits into two 16-value halves with their own 3-bit scale (ggml's
// dequantize_row_iq1_m: dl1 for the first two 8-value groups, dl2 for the last two, and a +-0.125 delta per group).
@@ -1048,7 +1137,7 @@ kernel void ds4_iqg_mvn(device const uchar* jobs [[buffer(0)]], uint2 tgp [[thre
const uint row = tgp.x * 8u + (tid >> 5);
if (int(row) >= b.n) return;
const uint nblk = uint(b.k) / 256u, nsb = uint(b.k) / 32u;
constexpr uint bs = TYPE == 16 ? 66u : (TYPE == 17 ? 74u : (TYPE == 18 ? 98u : (TYPE == 29 ? 56u : 82u)));
constexpr uint bs = TYPE == 16 ? 66u : (TYPE == 17 ? 74u : (TYPE == 18 ? 98u : (TYPE == 29 ? 56u : (TYPE == 19 ? 50u : 82u))));
const device uchar* w0p = b.w + (size_t) row * b.ld;
const uint ib = lane & 7u;
const uint bsel = lane >> 3;
@@ -1062,6 +1151,7 @@ kernel void ds4_iqg_mvn(device const uchar* jobs [[buffer(0)]], uint2 tgp [[thre
else if (TYPE == 17) ds4_w32_iq2xs(p, ib, w, db0, db1);
else if (TYPE == 22) ds4_w32_iq2s(p, ib, w, db0, db1);
else if (TYPE == 29) ds4_w32_iq1m(p, ib, w, db0, db1);
else if (TYPE == 19) ds4_w32_iq1s(p, ib, w, db0, db1);
else { ds4_w32_iq3xxs(p, ib, w, db0); db1 = db0; }
for (uint c = 0; c < NC; ++c) {
const device uint4* xq4 = (const device uint4*) (b.xq + (size_t) c * b.k + (size_t) blk * 256u + 32u * ib);
@@ -1081,11 +1171,13 @@ kernel void ds4_iqg_mvn(device const uchar* jobs [[buffer(0)]], uint2 tgp [[thre
#define DS4_MVN_INST(NC) \
template [[host_name("ds4_q5k_mvn" #NC)]] kernel void ds4_q5k_mvn<NC>(device const uchar*, uint2, uint, ushort); \
template [[host_name("ds4_q6k_mvn" #NC)]] kernel void ds4_q6k_mvn<NC>(device const uchar*, uint2, uint, ushort); \
template [[host_name("ds4_iq2xxs_mvn" #NC)]] kernel void ds4_iqg_mvn<NC, 16>(device const uchar*, uint2, uint, ushort); \
template [[host_name("ds4_iq2xs_mvn" #NC)]] kernel void ds4_iqg_mvn<NC, 17>(device const uchar*, uint2, uint, ushort); \
template [[host_name("ds4_iq2s_mvn" #NC)]] kernel void ds4_iqg_mvn<NC, 22>(device const uchar*, uint2, uint, ushort); \
template [[host_name("ds4_iq3xxs_mvn" #NC)]] kernel void ds4_iqg_mvn<NC, 18>(device const uchar*, uint2, uint, ushort); \
template [[host_name("ds4_iq1m_mvn" #NC)]] kernel void ds4_iqg_mvn<NC, 29>(device const uchar*, uint2, uint, ushort);
template [[host_name("ds4_iq1m_mvn" #NC)]] kernel void ds4_iqg_mvn<NC, 29>(device const uchar*, uint2, uint, ushort); \
template [[host_name("ds4_iq1s_mvn" #NC)]] kernel void ds4_iqg_mvn<NC, 19>(device const uchar*, uint2, uint, ushort);
DS4_MVN_INST(1)
DS4_MVN_INST(2)
DS4_MVN_INST(4)
+1
View File
@@ -132,6 +132,7 @@ kernel void ds4_mm_dn(device const uchar* W [[buffer(0)]], device half* YEH [[bu
#define DS4_MM_INST(F, T) template [[host_name("ds4_mm_gu_" #T)]] kernel decltype(ds4_mm_gu<F, 32, 32, 32, 4>) ds4_mm_gu<F, 32, 32, 32, 4>; template [[host_name("ds4_mm_dn_" #T)]] kernel decltype(ds4_mm_dn<F, 32, 32, 32, 4>) ds4_mm_dn<F, 32, 32, 32, 4>;
DS4_MM_INST(T_IQ1_M, 29)
DS4_MM_INST(T_IQ1_S, 19)
DS4_MM_INST(T_IQ2_XXS, 16)
DS4_MM_INST(T_IQ2_XS, 17)
DS4_MM_INST(T_IQ2_S, 22)
+14 -2
View File
@@ -73,16 +73,28 @@ kernel void ple_gate_norm(device const float* key [[buffer(0)]], device const fl
}
// grid (hc * n_embd / 256, T), 256 threads: the conv over [history | the window's rows], SiLU, and the residual
// PLE_ROWS (function constant 32): the rows are different sequences (batch slots): row t convolves its slot's history only (slot
// stride rinfo[7] floats, row t at rinfo[8 + 4 t] = {slot, ...}); the three taps before the row itself always read the history.
constant bool PLE_ROWS_C [[function_constant(32)]];
constant bool ple_rows = is_function_constant_defined(PLE_ROWS_C) && PLE_ROWS_C;
kernel void ple_conv_add(device const float* hist [[buffer(0)]], device const float* norm_w [[buffer(1)]],
device const half* kw [[buffer(2)]], device const float* gated [[buffer(3)]],
device float* R [[buffer(4)]], constant PleArgs& a [[buffer(5)]],
device const uint* rinfo [[buffer(6), function_constant(ple_rows)]],
uint3 tg [[threadgroup_position_in_grid]], ushort tid [[thread_index_in_threadgroup]]) {
const uint ch = tg.x * 256 + tid, t = tg.y, hcd = a.hc * a.n_embd;
if (ch >= hcd) return;
if (ple_rows) hist += ulong(rinfo[8 + 4 * t]) * rinfo[7];
float acc = 0.0f;
for (int k = 0; k < 4; ++k) {
const int p = int(t) - (3 - k) * 3; // the row this tap reads: < 0 = the committed history
const float v = p >= 0 ? norm_w[uint(p) * hcd + ch] : hist[uint(9 + p) + 9 * ch];
float v;
if (ple_rows) {
v = k == 3 ? norm_w[t * hcd + ch] : hist[uint(9 - (3 - k) * 3) + 9 * ch];
} else {
const int p = int(t) - (3 - k) * 3; // the row this tap reads: < 0 = the committed history
v = p >= 0 ? norm_w[uint(p) * hcd + ch] : hist[uint(9 + p) + 9 * ch];
}
acc += float(kw[k + 4 * ch]) * v;
}
const float conv = acc / (1.0f + exp(-acc));
+57 -22
View File
@@ -68,7 +68,14 @@ bool Window::pc_setup(int C) {
auto F16 = [&](size_t n) { return cx_->alloc(2 * n); };
b.R = F32(MT * HC * N); b.bo = F32(MT * N); b.inj[0] = F32(MT * HC); b.inj[1] = F32(MT * HC); b.xn = F32(MT * HC * N);
b.xnh = F16(MT * HC * N); b.V = F32(MT * (LR + HC)); b.loh = F16(MT * LR); b.mixed = F32(MT * N); b.mixh = F16(MT * N);
b.qkv = F32(MT * 10240); b.z = F32(MT * 6144); b.al = F32(MT * 48); b.be = F32(MT * 48); b.yg = F32(MT * 6144);
b.fused = gdn_fused_inputs(cx_->device_name());
if (b.fused) { // the four GDN input projections as one GEMM: [qkv | z | alpha | beta] rows of 16480 floats
b.yf = F32(MT * 16480);
b.qkv = F32(1); b.z = F32(1); b.al = F32(1); b.be = F32(1);
} else {
b.qkv = F32(MT * 10240); b.z = F32(MT * 6144); b.al = F32(MT * 48); b.be = F32(MT * 48);
}
b.yg = F32(MT * 6144);
b.ygh = F16(MT * 6144); b.gqn = F32(MT * 16 * 128); b.gkn = F32(MT * 16 * 128); b.gvc = F32(MT * 6144);
b.ggb = F32(MT * 48 * 2); b.gkq = F32(MT * 16); b.go = F32(MT * 6144);
b.qfull = F32(MT * 12288); b.kcur = F32(MT * 512); b.vcur = F32(MT * 512); b.iqr = F32(MT * 512); b.ikr = F32(MT * 128);
@@ -81,8 +88,14 @@ bool Window::pc_setup(int C) {
b.stok = cx_->alloc(4 * (size_t) b.maxrows); b.sw = F32(b.maxrows); b.pos = cx_->alloc(4 * MT * TOPK);
b.XS = F16((size_t) b.maxrows * N); b.HS = F16((size_t) b.maxrows * NFF); b.YE = F32((size_t) b.maxrows * N);
b.yr = F32(MT * N); b.gsh = F32(MT * NFF); b.ush = F32(MT * NFF); b.hh = F16(MT * NFF); b.ys = F32(MT * N); b.sgv = F32(MT);
b.WD = F16((size_t) (LR + HC) * HC * N); b.WU = F16((size_t) HC * N * LR); b.Wqkv = F16((size_t) 10240 * N);
b.Wz = F16((size_t) 6144 * N); b.Wal = F16((size_t) 48 * N); b.Wbe = F16((size_t) 48 * N); b.Wout = F16((size_t) N * 6144);
b.WD = F16((size_t) (LR + HC) * HC * N); b.WU = F16((size_t) HC * N * LR);
if (b.fused) {
b.Wcat = F16((size_t) 16480 * N);
b.Wqkv = F16(1); b.Wz = F16(1); b.Wal = F16(1); b.Wbe = F16(1);
} else {
b.Wqkv = F16((size_t) 10240 * N); b.Wz = F16((size_t) 6144 * N); b.Wal = F16((size_t) 48 * N); b.Wbe = F16((size_t) 48 * N);
}
b.Wout = F16((size_t) N * 6144);
b.Wq = F16((size_t) 12288 * N); b.Wk = F16((size_t) 512 * N); b.Wv = F16((size_t) 512 * N); b.Wiq = F16((size_t) 512 * N);
b.Wik = F16((size_t) 128 * N); b.Wr = F16((size_t) NEXP * N); b.Wsg = F16((size_t) N); b.Wshg = F16((size_t) NFF * N);
b.Wshu = F16((size_t) NFF * N); b.Wshd = F16((size_t) N * NFF);
@@ -150,7 +163,8 @@ bool Window::pc_chunk(const int64_t* tokens, int nT, int64_t pos0, std::string&
ds.push_back(std::move(d));
};
auto gemm = [&](const mtl::Buffer& A, const mtl::Buffer& B, const mtl::Buffer& Y, int M, int Nn, int K) {
const int tm = 64, tn = Nn <= 64 ? 32 : 64;
int tm = 64, tn = Nn <= 64 ? 32 : 64;
gemm_tile(cx_->device_name(), Nn, K, tm, tn);
const std::string fn = "gemm_f16_" + std::to_string(tm) + "_" + std::to_string(tn) + "_4";
PcGemmArgs g{M, Nn, K, 0};
D(P_(b.lib, fn), {mtl::Arg::B(A), mtl::Arg::B(B), mtl::Arg::B(Y), mtl::Arg::V(g)}, (Nn + tn - 1) / tn, (M + tm - 1) / tm, 128);
@@ -191,29 +205,50 @@ bool Window::pc_chunk(const int64_t* tokens, int nT, int64_t pos0, std::string&
const std::string bl = "blk." + std::to_string(l) + ".";
hc_read(bl + "hc_attn", l > 0 && !(l == 1 && ple_on_));
if (l % 4 != 3) {
dq(T(bl + "attn_qkv.weight"), b.Wqkv);
dq(T(bl + "attn_gate.weight"), b.Wz);
dq(T(bl + "ssm_alpha.weight"), b.Wal);
dq(T(bl + "ssm_beta.weight"), b.Wbe);
dq(T(bl + "ssm_out.weight"), b.Wout);
gemm(b.mixh, b.Wqkv, b.qkv, nT, 10240, N);
gemm(b.mixh, b.Wz, b.z, nT, 6144, N);
gemm(b.mixh, b.Wal, b.al, nT, 48, N);
gemm(b.mixh, b.Wbe, b.be, nT, 48, N);
PcGdnPArgs ga{(uint32_t) nT, 16, 48, 10240, eps_, 0};
D(P_(b.lgdn, "gdn_pre"),
{mtl::Arg::B(b.qkv), mtl::Arg::B(ghist_[hp][(size_t) gi]), mtl::Arg::B(W_, T(bl + "ssm_conv1d.weight").offset),
mtl::Arg::B(b.al), mtl::Arg::B(b.be), mtl::Arg::B(W_, T(bl + "ssm_dt.bias").offset),
mtl::Arg::B(W_, T(bl + "ssm_a").offset), mtl::Arg::B(b.gqn), mtl::Arg::B(b.gkn), mtl::Arg::B(b.gvc),
mtl::Arg::B(b.ggb), mtl::Arg::B(b.gkq), mtl::Arg::B(ghist_[1 - hp][(size_t) gi]), mtl::Arg::V(ga)},
16 + 24, nT, 256);
D(P_(b.lgdn, "gdn_scanr_4", mtl::Constants().i(6, 0)),
if (b.fused) {
dq(T(bl + "attn_qkv.weight"), b.Wcat, 0);
dq(T(bl + "attn_gate.weight"), b.Wcat, (size_t) 2 * 10240 * N);
dq(T(bl + "ssm_alpha.weight"), b.Wcat, (size_t) 2 * (10240 + 6144) * N);
dq(T(bl + "ssm_beta.weight"), b.Wcat, (size_t) 2 * (10240 + 6144 + 48) * N);
gemm(b.mixh, b.Wcat, b.yf, nT, 16480, N);
D(P_(b.lgdn, "gdn_pre", mtl::Constants().i(7, 16480), "gdn_pre_f"),
{mtl::Arg::B(b.yf), mtl::Arg::B(ghist_[hp][(size_t) gi]), mtl::Arg::B(W_, T(bl + "ssm_conv1d.weight").offset),
mtl::Arg::B(b.yf, (size_t) 4 * (10240 + 6144)), mtl::Arg::B(b.yf, (size_t) 4 * (10240 + 6144 + 48)),
mtl::Arg::B(W_, T(bl + "ssm_dt.bias").offset), mtl::Arg::B(W_, T(bl + "ssm_a").offset), mtl::Arg::B(b.gqn),
mtl::Arg::B(b.gkn), mtl::Arg::B(b.gvc), mtl::Arg::B(b.ggb), mtl::Arg::B(b.gkq),
mtl::Arg::B(ghist_[1 - hp][(size_t) gi]), mtl::Arg::V(ga)},
16 + 24, nT, 256);
} else {
dq(T(bl + "attn_qkv.weight"), b.Wqkv);
dq(T(bl + "attn_gate.weight"), b.Wz);
dq(T(bl + "ssm_alpha.weight"), b.Wal);
dq(T(bl + "ssm_beta.weight"), b.Wbe);
gemm(b.mixh, b.Wqkv, b.qkv, nT, 10240, N);
gemm(b.mixh, b.Wz, b.z, nT, 6144, N);
gemm(b.mixh, b.Wal, b.al, nT, 48, N);
gemm(b.mixh, b.Wbe, b.be, nT, 48, N);
D(P_(b.lgdn, "gdn_pre"),
{mtl::Arg::B(b.qkv), mtl::Arg::B(ghist_[hp][(size_t) gi]), mtl::Arg::B(W_, T(bl + "ssm_conv1d.weight").offset),
mtl::Arg::B(b.al), mtl::Arg::B(b.be), mtl::Arg::B(W_, T(bl + "ssm_dt.bias").offset),
mtl::Arg::B(W_, T(bl + "ssm_a").offset), mtl::Arg::B(b.gqn), mtl::Arg::B(b.gkn), mtl::Arg::B(b.gvc),
mtl::Arg::B(b.ggb), mtl::Arg::B(b.gkq), mtl::Arg::B(ghist_[1 - hp][(size_t) gi]), mtl::Arg::V(ga)},
16 + 24, nT, 256);
}
D(P_(b.lgdn, gdn_scan_name(cx_->device_name(), nT), mtl::Constants().i(6, 0)),
{mtl::Arg::B(gst_[(size_t) gi]), mtl::Arg::B(b.gqn), mtl::Arg::B(b.gkn), mtl::Arg::B(b.gvc), mtl::Arg::B(b.ggb),
mtl::Arg::B(b.gkq), mtl::Arg::B(b.go), mtl::Arg::V(ga)},
48 * 4, 1, 32 * 4);
D(P_(b.lgdn, "gdn_post"), {mtl::Arg::B(b.go), mtl::Arg::B(b.z), mtl::Arg::B(W_, T(bl + "ssm_norm.weight").offset),
mtl::Arg::B(b.ygh), mtl::Arg::B(b.yg), mtl::Arg::V(ga)},
48, nT, 128);
if (b.fused)
D(P_(b.lgdn, "gdn_post", mtl::Constants().i(7, 16480), "gdn_post_f"),
{mtl::Arg::B(b.go), mtl::Arg::B(b.yf, (size_t) 4 * 10240), mtl::Arg::B(W_, T(bl + "ssm_norm.weight").offset),
mtl::Arg::B(b.ygh), mtl::Arg::B(b.yg), mtl::Arg::V(ga)},
48, nT, 128);
else
D(P_(b.lgdn, "gdn_post"), {mtl::Arg::B(b.go), mtl::Arg::B(b.z), mtl::Arg::B(W_, T(bl + "ssm_norm.weight").offset),
mtl::Arg::B(b.ygh), mtl::Arg::B(b.yg), mtl::Arg::V(ga)},
48, nT, 128);
gemm(b.ygh, b.Wout, b.bo, nT, N, 6144);
++gi;
} else {
+3 -2
View File
@@ -284,8 +284,9 @@ namespace strata::kernels { void native_rope_apply(const float * p0, float * p1,
namespace strata::kernels { void native_route_resident(const float * p0, int32_t * p1, float * p2, const int32_t * p3, int p4, float p5, int p6, int p7, unsigned long long * p8, void * p9) { strata::metal_engine::missing("strata::kernels::native_route_resident"); } }
namespace strata::kernels { void native_router_top10(const float * p0, int32_t * p1, float * p2, void * p3) { strata::metal_engine::missing("strata::kernels::native_router_top10"); } }
namespace strata::kernels { void native_router_top10_multi(const float * p0, int32_t * p1, float * p2, int p3, void * p4) { strata::metal_engine::missing("strata::kernels::native_router_top10_multi"); } }
namespace strata::kernels { bool & pdl_scope() { strata::metal_engine::missing("strata::kernels::pdl_scope"); static std::remove_cv_t<std::remove_reference_t<bool &>> s{}; return s; } }
namespace strata::kernels { bool pdl_supported() { strata::metal_engine::missing("strata::kernels::pdl_supported"); return {}; } }
// programmatic dependent launch is a CUDA sm_90 feature: Metal never has it (hand-edited; the batch window asks this first, #3981)
namespace strata::kernels { bool & pdl_scope() { static bool s = false; return s; } }
namespace strata::kernels { bool pdl_supported() { return false; } }
namespace strata::kernels { void ple_block(const float * p0, const float * p1, const float * p2, const PleWeights & p3, PleOut & p4, void * p5, void * p6) { strata::metal_engine::missing("strata::kernels::ple_block"); } }
namespace strata::kernels { void ple_block_projected(const float * p0, const float * p1, const float * p2, const float * p3, const PleWeights & p4, PleOut & p5, void * p6, void * p7) { strata::metal_engine::missing("strata::kernels::ple_block_projected"); } }
namespace strata::kernels { void ple_history_advance(float * p0, const float * p1, void * p2) { strata::metal_engine::missing("strata::kernels::ple_history_advance"); } }
+399 -43
View File
@@ -50,8 +50,46 @@ namespace strata::gpu { uint64_t purge_device_allocations(size_t min_bytes); }
namespace strata::metal_engine {
// The prompt GDN scan kernel, chosen by chip and prompt length (gdn_prompt.metal, gdn_scan_r<IB, TB>; every choice is
// bit-identical to gdn_scanr_4: the token order and the arithmetic are the same, only the staged block size TB changes, which
// changes the threadgroup memory a core can hold at once). Measured by metal/tests/gdn_chunk_bench:
// Apple M4 (the base chip): gdn_scanr_4_8, 1.16x on the scan, 1.11x on the GDN prompt stage, T 512..32K
// Apple M5 Max: gdn_scanr_4_4 from T >= 4096 (1.04x at 4K, 1.10x at 16K); below that the default is as fast
// anything else (other chips, unknown names): gdn_scanr_4 as before
// STRATA_METAL_GDN_SCAN=<gdn_scanr_4 | gdn_scanr_4_8 | gdn_scanr_4_4 | gdn_scanr_4_2> forces a kernel (A/B and diagnosis).
static const char* gdn_scan_name(const std::string& device, int T) {
static const std::string forced = [] {
const char* e = std::getenv("STRATA_METAL_GDN_SCAN");
const std::string s = e ? e : "";
return (s == "gdn_scanr_4" || s == "gdn_scanr_4_8" || s == "gdn_scanr_4_4" || s == "gdn_scanr_4_2") ? s : std::string();
}();
if (!forced.empty()) return forced.c_str();
if (device == "Apple M4") return "gdn_scanr_4_8";
if (device == "Apple M5 Max" && T >= 4096) return "gdn_scanr_4_4";
return "gdn_scanr_4";
}
// The M5 family's dense GEMM tiles for the prompt (bit-identical to the 64 x 64 tile: the tile only splits the output; measured on
// an M5 Max, T = 2048 / 4096): the GDN out projection (N 2560, K 6144) 64 x 128 (1.17 -> 0.99 ms), the wide input projections
// (N >= 6144) 128 x 64. Other chips keep 64 x 64 (M4: no gain). STRATA_METAL_GEMM_TILES=0 keeps the old tiles everywhere.
static void gemm_tile(const std::string& device, int Nn, int K, int& tm, int& tn) {
static const bool off = [] { const char* e = std::getenv("STRATA_METAL_GEMM_TILES"); return e && std::string(e) == "0"; }();
if (off || device.rfind("Apple M5", 0) != 0) return;
if (Nn == 2560 && K == 6144) { tm = 64; tn = 128; }
else if (Nn >= 6144) { tm = 128; tn = 64; }
}
// The four GDN input projections (qkv, z, alpha, beta) as ONE GEMM with a 16480 float output row (M5 family: 3.09 -> 2.87 ms a layer at
// T = 2048; gdn_pre / gdn_post read the pieces at that stride, function constant 7). Bit-identical to the separate GEMMs.
// STRATA_METAL_GDN_FUSE=0 keeps the separate ones.
static bool gdn_fused_inputs(const std::string& device) {
static const bool off = [] { const char* e = std::getenv("STRATA_METAL_GDN_FUSE"); return e && std::string(e) == "0"; }();
return !off && device.rfind("Apple M5", 0) == 0;
}
namespace {
int g_slot_cap = 0; // --batch N: the window keeps N + 1 sequences' running states side by side (set_batch_slots)
constexpr int N = 2560, HC = 4, LR = 320, NFF = 640, TOPK = 10, NEXP = 512, MAXT = 8, NVOCAB_MAX = 248320;
constexpr int P_MAXE = 128, P_GEXP = 16, P_GSTART = P_GEXP + P_MAXE, P_ETOK = P_GSTART + P_MAXE + 4,
P_EW = P_ETOK + P_MAXE, P_TOKENT = P_EW + P_MAXE, P_INTS = P_TOKENT + P_MAXE, P_IGU = 4, P_IDN = 8;
@@ -99,14 +137,15 @@ double now_ms() { return std::chrono::duration<double, std::milli>(std::chrono::
struct PromptBufs {
int lib = -1, lgdn = -1, lqsa = -1;
bool k4 = false;
bool v3e = false; // the M5's expert kernels (v3): not for the trellis types, which have only v2
bool v3e = false;
bool fused = false; // the GDN input projections as one GEMM (gdn_fused_inputs) // the M5's expert kernels (v3): not for the trellis types, which have only v2
int C = 0, maxrows = 0;
double gpu_ms = 0, host_ms = 0, mtp_ms = 0;
int gu3[4] = {}, dn3[4] = {};
mtl::Buffer R, bo, inj[2], xn, xnh, V, loh, mixed, mixh, qkv, z, al, be, yg, ygh, gqn, gkn, gvc, ggb, gkq, go, qfull,
kcur, vcur, iqr, ikr, qcur, qcurh, qidx, scores, qids, part, XQ, xqh, logits, ids, wts, bcnt, boff, eoff, tiles, meta,
stok, sw, pos, XS, HS, YE, yr, gsh, ush, hh, ys, sgv, WD, WU, Wqkv, Wz, Wal, Wbe, Wout, Wq, Wk, Wv, Wiq, Wik, Wr,
Wsg, Wshg, Wshu, Wshd, pemb, pembh, pkey, pval, keyn, qn, gated, normW, Wpk, Wpv, mixr;
Wsg, Wshg, Wshu, Wshd, yf, Wcat, pemb, pembh, pkey, pval, keyn, qn, gated, normW, Wpk, Wpv, mixr;
};
// The vision path (--vision, GENI requests), from the shared host code: the image rows of the prompt being read
@@ -132,6 +171,21 @@ public:
bool checkpoint_save(strata::core::ConversationCheckpoint& c, std::string& err);
bool checkpoint_restore(const strata::core::ConversationCheckpoint& c, std::string& err);
void reset(); // a new sequence (session_zero): the running state zeroed
// ---- batch slots (--batch N): each slot is another sequence's running state (GDN recurrence and conv history, the PLE
// history, each QSA layer's K/V, indexer tail and pooled keys). The live members always hold ONE sequence's state (the
// main one between calls); a slot's lives in a stash and is swapped in for its window (activate): the buffer objects stay
// where they are (the cached dispatches point at them), only their contents are exchanged.
bool slots_init(const std::vector<strata::core::SessionState*>& slots, std::string& err);
int n_slots() const { return sets_.empty() ? 0 : (int) sets_.size() - 1; }
int set_of(const strata::core::SessionState* s) const {
auto it = set_of_ss_.find(s);
return it == set_of_ss_.end() ? 0 : it->second;
}
// one window of S one-token rows, row t = slot rows[t] at position pos[t]: each runs and commits in its own state
bool run_slots(const int* rows, int S, const int32_t* tokens, const int64_t* pos, int32_t* out, std::string& err);
// slot b's state <-> the main one (the GPU is idle between windows): the running state and the first `upto` cells of K/V
bool slot_copy(bool into_slot, int b, int64_t upto, bool kv_only, std::string& err);
void activate(int set);
void set_max_t(int t) { max_t_ = std::min(t, MAXT); }
// ---- the MTP draft layer (mtp.hpp's math on mac-kern's kernels; the MTP GGUF from tools/mtp_pack.py)
bool mtp_load(const std::string& gguf, int max_t, std::string& err);
@@ -207,7 +261,8 @@ private:
return it->second;
}
strata::core::SessionState* ss_ = nullptr;
strata::core::SessionState* ss_ = nullptr; // the session whose sequence the live state is (the main one, or a batch slot's)
strata::core::SessionState* main_ss_ = nullptr; // the main session: it owns the PLE table (read by every slot)
gguf::File f_;
std::unique_ptr<mtl::Context> cx_;
mtl::Buffer W_;
@@ -234,6 +289,40 @@ private:
int cur_inj_ = 0;
// state
std::vector<mtl::Buffer> gst_, ghist_[2], kq_, vq_, ksc_, vsc_, tail_, dead_, pooled_;
// --batch N: every one of those is a view of set 0 in a bigger buffer holding all N + 1 sets (the slots' running states side
// by side), so ONE dispatch can address any slot (rinfo_: the strides and each row's slot / cell / history parity)
bool bigs_ok_ = false;
std::vector<mtl::Buffer> gstBig_, hbBig_, kqBig_, vqBig_, kscBig_, vscBig_, tailBig_, deadBig_, pooledBig_;
mtl::Buffer pleBig_, rinfo_;
size_t st_gst_ = 0, st_hist_ = 0, st_kq_ = 0, st_sc_ = 0, st_tail_ = 0, st_dead_ = 0, st_pooled_ = 0, st_ple_ = 0; // bytes between sets
bool rows_ = false; // the window being built / run holds one token of each of rows_n_ DIFFERENT sequences (batch slots)
int rows_n_ = 0;
strata::core::SessionState* rows_ss_[MAXT] = {};
int rows_set_[MAXT] = {};
static mtl::Buffer view_of(const mtl::Buffer& big, size_t off, size_t bytes) {
mtl::Buffer v = big;
v.ptr = (char*) big.ptr + off;
v.bias = big.bias + off;
v.bytes = bytes;
return v;
}
bool run_rows(const int* rows, int S, const int32_t* tokens, const int64_t* pos, int32_t* out, std::string& err);
bool finish_rows(int S, const int32_t* tokens, int32_t* out, double t0);
struct SeqSet {
std::vector<mtl::Buffer> gst, gh0, gh1, kq, vq, ksc, vsc, tail, dead, pooled;
mtl::Buffer pleHist;
std::vector<float> ple_hist;
std::vector<float> last_norm;
int hp = 0, pend_fold = 0, last_T = 0;
bool last_head = true;
int32_t last_tokens[MAXT] = {};
strata::core::SessionState* ss = nullptr;
};
std::vector<std::unique_ptr<SeqSet>> sets_; // [0] the spare the main state is exchanged with, [b + 1] slot b
std::map<const strata::core::SessionState*, int> set_of_ss_;
int active_ = 0;
void swap_state(SeqSet& o);
void alloc_set(SeqSet& o, bool real, int idx = 0);
int hp_ = 0; // which of the two history buffers holds the committed conv history (all layers)
// commit(n) of a window of several tokens: the GDN fold of the kept prefix waits for the next window and runs at
// the start of its command buffer (one submit and wait less a round). Nothing else reads the GDN state between
@@ -435,6 +524,7 @@ long repack_pid_running(const std::string& model_file) {
bool Window::init(const strata::core::ModelGeometry& g, strata::core::SessionState& ss, const std::string& shard1,
int max_t, int64_t max_cells, std::string& err) {
ss_ = &ss;
main_ss_ = &ss;
max_t_ = std::min(max_t, MAXT);
max_cells_ = max_cells;
if (g.n_embd != N || g.hc != HC || g.hc_lr != LR || g.n_ff != NFF || g.n_expert != NEXP || g.n_layers != 48) {
@@ -515,7 +605,20 @@ bool Window::init(const strata::core::ModelGeometry& g, strata::core::SessionSta
zL_ = A(4 * (size_t) 36 * MAXT * 6144); ydump_ = A(4 * MAXT * 6144);
keyn_ = A(4 * MAXT * 10240); qn_ = A(4 * MAXT * 10240); gated_ = A(4 * MAXT * 10240); normW_ = A(4 * MAXT * 10240);
mrope_ = A(12 * (size_t) (MAXC + 16));
pleHist_ = A(4 * 9 * 10240);
bigs_ok_ = g_slot_cap > 0;
const size_t NSET = 1 + (size_t) std::max(0, g_slot_cap);
auto R256 = [](size_t b) { return (b + 255) / 256 * 256; };
st_gst_ = R256(4 * 128 * 48 * 128); st_hist_ = R256(4 * 10240 * 3); st_kq_ = R256((size_t) MAXC * 2 * 256);
st_sc_ = R256(2 * (size_t) MAXC * 2 * 4); st_tail_ = R256(4 * 4 * 128); st_dead_ = R256(4 * 128);
st_pooled_ = R256(4 * (size_t) MB * 128); st_ple_ = R256(4 * 9 * 10240);
// the set-0 view of a new buffer of `bytes` per set (or a plain buffer when there are no batch slots)
auto SV = [&](std::vector<mtl::Buffer>& bigs, size_t bytes, size_t stride, size_t per = 1, size_t which = 0) {
if (!bigs_ok_) { bigs.push_back(mtl::Buffer()); return A(bytes); }
if (which == 0) bigs.push_back(A(stride * per * NSET));
return view_of(bigs.back(), which * stride, bytes);
};
if (bigs_ok_) { pleBig_ = A(st_ple_ * NSET); pleHist_ = view_of(pleBig_, 0, 4 * 9 * 10240); } else pleHist_ = A(4 * 9 * 10240);
rinfo_ = A(4 * (8 + 4 * MAXT));
auto act = [&](int n) { Act a; a.n = n; a.b = A(4 * ((size_t) 4 * MAXT * n + (size_t) MAXT * n / 16)); return a; };
XA_ = act(N); XF_ = act(N); XY_ = act(6144); XQ_ = act(6144); XH_ = act(NFF); XE_ = act(N);
{ // Gyro (native_experts.txt v5 + hadamard.txt): the rotation's signs and the rotated gate/up input
@@ -552,17 +655,17 @@ bool Window::init(const strata::core::ModelGeometry& g, strata::core::SessionSta
for (int c = 0; c < 2051; ++c) qids_.as<int>()[t * 2051 + c] = c;
for (int l = 0; l < 48; ++l) {
if (l % 4 != 3) {
gst_.push_back(A(4 * 128 * 48 * 128));
ghist_[0].push_back(A(4 * 10240 * 3));
ghist_[1].push_back(A(4 * 10240 * 3));
gst_.push_back(SV(gstBig_, 4 * 128 * 48 * 128, st_gst_));
ghist_[0].push_back(SV(hbBig_, 4 * 10240 * 3, st_hist_, 2, 0)); // [set][parity]: the two parities side by side
ghist_[1].push_back(SV(hbBig_, 4 * 10240 * 3, st_hist_, 2, 1));
} else {
kq_.push_back(A((size_t) MAXC * 2 * 256));
vq_.push_back(A((size_t) MAXC * 2 * 256));
ksc_.push_back(A(2 * (size_t) MAXC * 2 * 4));
vsc_.push_back(A(2 * (size_t) MAXC * 2 * 4));
tail_.push_back(A(4 * 4 * 128));
dead_.push_back(A(4 * 128));
pooled_.push_back(A(4 * (size_t) MB * 128));
kq_.push_back(SV(kqBig_, (size_t) MAXC * 2 * 256, st_kq_));
vq_.push_back(SV(vqBig_, (size_t) MAXC * 2 * 256, st_kq_));
ksc_.push_back(SV(kscBig_, 2 * (size_t) MAXC * 2 * 4, st_sc_));
vsc_.push_back(SV(vscBig_, 2 * (size_t) MAXC * 2 * 4, st_sc_));
tail_.push_back(SV(tailBig_, 4 * 4 * 128, st_tail_));
dead_.push_back(SV(deadBig_, 4 * 128, st_dead_));
pooled_.push_back(SV(pooledBig_, 4 * (size_t) MB * 128, st_pooled_));
}
}
// ---- the HC read in Q8_0 (the owner's default on Metal, as STRATA_HC_Q8 on gfx12): the BF16 down / up / inject
@@ -796,6 +899,22 @@ void Window::layer(std::vector<mtl::Dispatch>& ds, int l, int ntok, int64_t pos0
mtl::Dispatch d;
d.p = P(lgdn_, "gdn_step", mtl::Constants().i(2, ntok), "gdn_step#" + std::to_string(ntok));
const int hp = hp_;
if (rows_) { // each row is a different sequence: its own state, parity and conv history (rinfo_), one grid row each
d.p = P(lgdn_, "gdn_step", mtl::Constants().i(2, 1).b(31, true), "gdn_step#rows");
GdnArgs ar{16, 48, 10240, 1u, eps_, fuse_ ? (uint32_t) (MAXT * 6144) : 0u};
d.args = {mtl::Arg::B(gstBig_[(size_t) gi]), mtl::Arg::B(hbBig_[(size_t) gi]), mtl::Arg::B(qkvL_, o_qkv),
mtl::Arg::B(alL_, o_ab), mtl::Arg::B(beL_, o_ab), mtl::Arg::B(zL_, o_z),
mtl::Arg::B(W_, T(b + "ssm_conv1d.weight").offset),
mtl::Arg::B(W_, T(b + "ssm_dt.bias").offset), mtl::Arg::B(W_, T(b + "ssm_a").offset),
mtl::Arg::B(W_, T(b + "ssm_norm.weight").offset), mtl::Arg::B(fuse_ ? XY_.b : yg_), mtl::Arg::V(ar),
mtl::Arg::B(hbBig_[(size_t) gi]), mtl::Arg::B(rinfo_)};
d.grid[0] = 48;
d.grid[1] = (uint32_t) rows_n_;
d.tg[0] = 512;
push(ds, d);
if (!fuse_) xprep(ds, yg_, XY_, 6144);
gemv(ds, T(b + "ssm_out.weight"), XY_, bo_, N, ntok);
} else {
d.args = {mtl::Arg::B(gst_[(size_t) gi]), mtl::Arg::B(ghist_[hp][(size_t) gi]), mtl::Arg::B(qkvL_, o_qkv),
mtl::Arg::B(alL_, o_ab), mtl::Arg::B(beL_, o_ab), mtl::Arg::B(zL_, o_z),
mtl::Arg::B(W_, T(b + "ssm_conv1d.weight").offset),
@@ -808,6 +927,7 @@ void Window::layer(std::vector<mtl::Dispatch>& ds, int l, int ntok, int64_t pos0
(void) self_commit; // the history ping-pong turns after the window (run) or at commit
if (!fuse_) xprep(ds, yg_, XY_, 6144);
gemv(ds, T(b + "ssm_out.weight"), XY_, bo_, N, ntok);
}
} else {
const int qi = l / 4;
gemv(ds, T(b + "attn_q.weight"), XA_, qfull_, 12288, ntok);
@@ -821,23 +941,34 @@ void Window::layer(std::vector<mtl::Dispatch>& ds, int l, int ntok, int64_t pos0
gemv(ds, T(b + "indexer.k_proj.weight"), XA_, ikr_, 128, ntok);
QsaArgs a{(uint32_t) pos0, 0, (uint32_t) max_cells_, (uint32_t) ntok, eps_};
mtl::Dispatch d;
d.p = P(lqsa_, "qsa_prep", qsa_c(), qsa_k());
const bool rw = rows_; // (batch slots: pos0 is the highest row's cell, the rows have their own in rinfo_)
const mtl::Constants rc = mtl::Constants().b(30, true);
const mtl::Buffer& KQ = rw ? kqBig_[(size_t) qi] : kq_[(size_t) qi];
const mtl::Buffer& VQ = rw ? vqBig_[(size_t) qi] : vq_[(size_t) qi];
const mtl::Buffer& KS = rw ? kscBig_[(size_t) qi] : ksc_[(size_t) qi];
const mtl::Buffer& VS = rw ? vscBig_[(size_t) qi] : vsc_[(size_t) qi];
const mtl::Buffer& TL = rw ? tailBig_[(size_t) qi] : tail_[(size_t) qi];
const mtl::Buffer& DD = rw ? deadBig_[(size_t) qi] : dead_[(size_t) qi];
const mtl::Buffer& PL = rw ? pooledBig_[(size_t) qi] : pooled_[(size_t) qi];
d.p = rw ? P(lqsa_, "qsa_prep", rc, "qsa_prep#rows") : P(lqsa_, "qsa_prep", qsa_c(), qsa_k());
d.args = {mtl::Arg::B(qfull_), mtl::Arg::B(kcur_), mtl::Arg::B(vcur_), mtl::Arg::B(iqr_), mtl::Arg::B(ikr_),
mtl::Arg::B(W_, T(b + "attn_q_norm.weight").offset), mtl::Arg::B(W_, T(b + "attn_k_norm.weight").offset),
mtl::Arg::B(W_, T(b + "indexer.q_norm.weight").offset), mtl::Arg::B(W_, T(b + "indexer.k_norm.weight").offset),
mtl::Arg::B(cs_), mtl::Arg::B(qcur_), mtl::Arg::B(qidx_), mtl::Arg::B(kq_[(size_t) qi]),
mtl::Arg::B(vq_[(size_t) qi]), mtl::Arg::B(ksc_[(size_t) qi]), mtl::Arg::B(vsc_[(size_t) qi]),
mtl::Arg::B(tail_[(size_t) qi]), mtl::Arg::B(dead_[(size_t) qi]), mtl::Arg::B(pooled_[(size_t) qi]),
mtl::Arg::B(cs_), mtl::Arg::B(qcur_), mtl::Arg::B(qidx_), mtl::Arg::B(KQ),
mtl::Arg::B(VQ), mtl::Arg::B(KS), mtl::Arg::B(VS),
mtl::Arg::B(TL), mtl::Arg::B(DD), mtl::Arg::B(PL),
mtl::Arg::V(a), mtl::Arg::B(mrope_)};
if (rw) d.args.push_back(mtl::Arg::B(rinfo_));
d.grid[0] = 33;
d.grid[1] = ntok;
d.tg[0] = 256;
push(ds, d);
if (rec_) rec_->push_back({(int) ds.size() - 1, 0});
mtl::Dispatch s;
s.p = P(lqsa_, "qsa_score", {}, "qsa_score");
s.args = {mtl::Arg::B(qidx_), mtl::Arg::B(pooled_[(size_t) qi]), mtl::Arg::B(dead_[(size_t) qi]), mtl::Arg::B(scores_),
s.p = rw ? P(lqsa_, "qsa_score", rc, "qsa_score#rows") : P(lqsa_, "qsa_score", {}, "qsa_score");
s.args = {mtl::Arg::B(qidx_), mtl::Arg::B(PL), mtl::Arg::B(DD), mtl::Arg::B(scores_),
mtl::Arg::V(a)};
if (rw) s.args.push_back(mtl::Arg::B(rinfo_));
s.grid[0] = (uint32_t) (((pos0 + ntok) / 4 + 1 + 7) / 8);
s.grid[1] = ntok;
s.tg[0] = 256;
@@ -847,8 +978,9 @@ void Window::layer(std::vector<mtl::Dispatch>& ds, int l, int ntok, int64_t pos0
if (rec_) rec_->push_back({(int) ds.size() - 1, 2});
}
mtl::Dispatch k;
k.p = P(lqsa_, "qsa_topk2", {}, "qsa_topk");
k.p = rw ? P(lqsa_, "qsa_topk2", rc, "qsa_topk#rows") : P(lqsa_, "qsa_topk2", {}, "qsa_topk");
k.args = {mtl::Arg::B(scores_), mtl::Arg::B(qids_), mtl::Arg::V(a)};
if (rw) k.args.push_back(mtl::Arg::B(rinfo_));
k.grid[0] = ntok;
k.tg[0] = 1024;
if (sel) {
@@ -856,9 +988,10 @@ void Window::layer(std::vector<mtl::Dispatch>& ds, int l, int ntok, int64_t pos0
if (rec_) rec_->push_back({(int) ds.size() - 1, 0});
}
mtl::Dispatch at;
at.p = P(lqsa_, "qsa_attn", {}, "qsa_attn");
at.args = {mtl::Arg::B(qcur_), mtl::Arg::B(kq_[(size_t) qi]), mtl::Arg::B(vq_[(size_t) qi]), mtl::Arg::B(ksc_[(size_t) qi]),
mtl::Arg::B(vsc_[(size_t) qi]), mtl::Arg::B(qids_), mtl::Arg::B(part_), mtl::Arg::V(a)};
at.p = rw ? P(lqsa_, "qsa_attn", rc, "qsa_attn#rows") : P(lqsa_, "qsa_attn", {}, "qsa_attn");
at.args = {mtl::Arg::B(qcur_), mtl::Arg::B(KQ), mtl::Arg::B(VQ), mtl::Arg::B(KS),
mtl::Arg::B(VS), mtl::Arg::B(qids_), mtl::Arg::B(part_), mtl::Arg::V(a)};
if (rw) at.args.push_back(mtl::Arg::B(rinfo_));
at.grid[0] = 33;
at.grid[1] = 2;
at.grid[2] = ntok;
@@ -866,10 +999,11 @@ void Window::layer(std::vector<mtl::Dispatch>& ds, int l, int ntok, int64_t pos0
push(ds, at);
if (rec_) rec_->push_back({(int) ds.size() - 1, 0});
mtl::Dispatch m;
m.p = P(lqsa_, "qsa_merge", {}, "qsa_merge");
m.p = rw ? P(lqsa_, "qsa_merge", rc, "qsa_merge#rows") : P(lqsa_, "qsa_merge", {}, "qsa_merge");
QsaArgs am = a;
am.n_tok = MAXT; // the plane stride
m.args = {mtl::Arg::B(part_), mtl::Arg::B(qfull_), mtl::Arg::B(XQ_.b), mtl::Arg::V(am)};
if (rw) m.args.push_back(mtl::Arg::B(rinfo_));
m.grid[0] = 24;
m.grid[1] = ntok;
m.tg[0] = 256;
@@ -1094,11 +1228,11 @@ bool Window::run(int Tn, const int32_t* tokens, int64_t pos0, bool head, int32_t
uint32_t rows[MAXT * strata::kernels::PLE_N_HEADS];
int32_t prev[2] = {ss.ple_prev[0], ss.ple_prev[1]};
for (int t = 0; t < Tn; ++t) {
strata::kernels::ngram_rows(&tokens[t], prev, 1, ss.ple.consts, rows + t * strata::kernels::PLE_N_HEADS);
prev[0] = prev[1];
prev[1] = tokens[t];
if (rows_) { prev[0] = rows_ss_[t]->ple_prev[0]; prev[1] = rows_ss_[t]->ple_prev[1]; } // (batch slots: each row its own sequence)
strata::kernels::ngram_rows(&tokens[t], prev, 1, main_ss_->ple.consts, rows + t * strata::kernels::PLE_N_HEADS);
if (!rows_) { prev[0] = prev[1]; prev[1] = tokens[t]; }
}
if (!ss.ple.table->gather_batch(rows, (size_t) Tn, emb_.as<float>(), err)) return false;
if (!main_ss_->ple.table->gather_batch(rows, (size_t) Tn, emb_.as<float>(), err)) return false;
}
tp_pre_ = now_ms();
const bool sel = !(fuse_ && pos0 + Tn <= 2051);
@@ -1108,8 +1242,9 @@ bool Window::run(int Tn, const int32_t* tokens, int64_t pos0, bool head, int32_t
for (int c = 0; c < 2051; ++c) qids_.as<int>()[t * 2051 + c] = c;
qdirty_ = false;
}
const int key = (((((Tn * 2 + (head ? 1 : 0)) * 2 + hp_) * (MAXT + 1)) + pend_fold_) * 2 + (sel ? 1 : 0)) * 2 +
(mrope_on() ? 1 : 0);
const int key = rows_ ? 1000000 + Tn * 2 + (sel ? 1 : 0) + (int) (pend_fold_ * 100)
: (((((Tn * 2 + (head ? 1 : 0)) * 2 + hp_) * (MAXT + 1)) + pend_fold_) * 2 + (sel ? 1 : 0)) * 2 +
(mrope_on() ? 1 : 0);
mrope_sync(pos0, Tn);
const int fold_n = pend_fold_;
pend_fold_ = 0;
@@ -1138,7 +1273,7 @@ bool Window::run(int Tn, const int32_t* tokens, int64_t pos0, bool head, int32_t
}
tp_rg_ = now_ms() - r0;
last_n_disp_ = (int) c.ds.size();
return finish(Tn, tokens, pos0, head, out, t0);
return rows_ ? finish_rows(Tn, tokens, out, t0) : finish(Tn, tokens, pos0, head, out, t0);
}
if (stream_ && !st_->ready() && !stream_arena(err)) return false;
Cached& cw = wcache_[key];
@@ -1185,9 +1320,10 @@ bool Window::run(int Tn, const int32_t* tokens, int64_t pos0, bool head, int32_t
mtl::Arg::B(gated_), mtl::Arg::B(normW_), mtl::Arg::V(pa)};
g3.grid[0] = HC; g3.grid[1] = Tn; g3.tg[0] = 256;
push(ds, g3);
g4.p = P(lple_, "ple_conv_add", {}, "ple_conv_add");
g4.args = {mtl::Arg::B(pleHist_), mtl::Arg::B(normW_), kw_arg(),
g4.p = rows_ ? P(lple_, "ple_conv_add", mtl::Constants().b(32, true), "ple_conv_add#rows") : P(lple_, "ple_conv_add", {}, "ple_conv_add");
g4.args = {mtl::Arg::B(rows_ ? pleBig_ : pleHist_), mtl::Arg::B(normW_), kw_arg(),
mtl::Arg::B(gated_), mtl::Arg::B(R_), mtl::Arg::V(pa)};
if (rows_) g4.args.push_back(mtl::Arg::B(rinfo_));
g4.grid[0] = HC * N / 256; g4.grid[1] = Tn; g4.tg[0] = 256;
push(ds, g4);
}
@@ -1217,7 +1353,56 @@ bool Window::run(int Tn, const int32_t* tokens, int64_t pos0, bool head, int32_t
last_gpu_ms_ = 1000.0 * run_ds(cw);
tp_rg_ = now_ms() - r0;
last_n_disp_ = (int) ds.size();
return finish(Tn, tokens, pos0, head, out, t0);
return rows_ ? finish_rows(Tn, tokens, out, t0) : finish(Tn, tokens, pos0, head, out, t0);
}
bool Window::run_rows(const int* rows, int S, const int32_t* tokens, const int64_t* pos, int32_t* out, std::string& err) {
if (S < 1 || S > MAXT || S > max_t_) { err = "metal window: batch window size out of range"; return false; }
if (active_ != 0) activate(0);
uint32_t* ri = rinfo_.as<uint32_t>() + 8;
int64_t maxpos = 0;
for (int t = 0; t < S; ++t) {
if (rows[t] < 0 || rows[t] >= n_slots()) { err = "metal window: a batch row names a slot that does not exist"; return false; }
for (int u = 0; u < t; ++u)
if (rows[u] == rows[t]) { err = "metal window: two rows of one batch window are the same slot"; return false; }
const int set = rows[t] + 1;
rows_ss_[t] = sets_[(size_t) set]->ss;
rows_set_[t] = set;
ri[4 * t] = (uint32_t) set;
ri[4 * t + 1] = (uint32_t) pos[t];
ri[4 * t + 2] = (uint32_t) sets_[(size_t) set]->hp;
ri[4 * t + 3] = 0;
maxpos = std::max<int64_t>(maxpos, pos[t]);
}
rows_ = true;
rows_n_ = S;
const bool ok = run(S, tokens, maxpos, true, out, err);
rows_ = false;
return ok;
}
// the end of a window of batch slots: the picks, and each slot's commit (a one-token step committed its row: the other history
// parity is the committed one; the PLE history takes the row's normalized values; the session's token pair advances)
bool Window::finish_rows(int Tn, const int32_t* tokens, int32_t* out, double t0) {
++win_n_[Tn];
win_ms_[Tn] += now_ms() - t0;
win_gpu_[Tn] += last_gpu_ms_;
for (int t = 0; t < Tn; ++t) out[t] = tokout_.as<int32_t>()[t];
for (int t = 0; t < Tn; ++t) {
SeqSet& s = *sets_[(size_t) rows_set_[t]];
s.hp = 1 - s.hp;
if (ple_on_) {
const float* norm = normW_.as<float>() + (size_t) t * strata::kernels::NG_HC_DIM;
for (int c = 0; c < strata::kernels::NG_HC_DIM; ++c) {
float* col = s.pleHist.as<float>() + (size_t) c * strata::kernels::NG_HIST;
for (int r = 0; r + 1 < strata::kernels::NG_HIST; ++r) col[r] = col[r + 1];
col[strata::kernels::NG_HIST - 1] = norm[c];
}
}
s.ss->ple_prev[0] = s.ss->ple_prev[1];
s.ss->ple_prev[1] = tokens[t];
}
return true;
}
double Window::run_ds(Cached& c) {
@@ -1286,7 +1471,7 @@ bool Window::stream_arena(std::string& err) {
if (sysctlbyname("vm.vm_page_filecache_min", &pages, &pl, nullptr, 0) == 0) am.filecache_min = (double) pages * (double) vm_page_size;
}
am.available = avail_now();
if (ss_ && ss_->ple.table && ss_->ple.table->mode() == strata::kernels::PleIo::Mmap)
if (main_ss_ && main_ss_->ple.table && main_ss_->ple.table->mode() == strata::kernels::PleIo::Mmap)
am.ple_mapped = (double) ss_->ple.table->rows() * 192.0; // a row is at most 192 bytes (Q8_0 + its scale)
if (const char* e = std::getenv("STRATA_METAL_KEEP_FREE_GB")) am.keep_gb = std::atof(e);
if (const char* e = std::getenv("STRATA_METAL_ARENA_GB")) am.arena_gb = std::atof(e);
@@ -1499,6 +1684,154 @@ void Window::fold(std::vector<mtl::Dispatch>& ds, int n_keep, int hp) {
nb_ = false; // what follows reads the folded state: a barrier
}
void Window::swap_state(SeqSet& o) {
auto sw = [](std::vector<mtl::Buffer>& a, std::vector<mtl::Buffer>& b) {
for (size_t i = 0; i < a.size() && i < b.size(); ++i) std::swap(a[i], b[i]);
};
sw(gst_, o.gst); sw(ghist_[0], o.gh0); sw(ghist_[1], o.gh1); sw(kq_, o.kq); sw(vq_, o.vq); sw(ksc_, o.ksc); sw(vsc_, o.vsc);
sw(tail_, o.tail); sw(dead_, o.dead); sw(pooled_, o.pooled);
std::swap(pleHist_, o.pleHist);
ple_hist_.swap(o.ple_hist);
last_norm_.swap(o.last_norm);
std::swap(hp_, o.hp); std::swap(pend_fold_, o.pend_fold); std::swap(last_T_, o.last_T); std::swap(last_head_, o.last_head);
for (int t = 0; t < MAXT; ++t) std::swap(last_tokens_[t], o.last_tokens[t]);
std::swap(ss_, o.ss);
}
// the buffers of one sequence's running state, shaped like the live ones (real: allocated and zero; else empty placeholders)
void Window::alloc_set(SeqSet& o, bool real, int idx) {
const size_t MAXC = (size_t) max_cells_, MB = MAXC / 4 + 2;
if (real && bigs_ok_) { // set idx's part of the big buffers
for (size_t i = 0; i < gst_.size(); ++i) {
o.gst.push_back(view_of(gstBig_[i], (size_t) idx * st_gst_, 4 * 128 * 48 * 128));
o.gh0.push_back(view_of(hbBig_[i], ((size_t) idx * 2 + 0) * st_hist_, 4 * 10240 * 3));
o.gh1.push_back(view_of(hbBig_[i], ((size_t) idx * 2 + 1) * st_hist_, 4 * 10240 * 3));
}
for (size_t j = 0; j < kq_.size(); ++j) {
o.kq.push_back(view_of(kqBig_[j], (size_t) idx * st_kq_, MAXC * 2 * 256));
o.vq.push_back(view_of(vqBig_[j], (size_t) idx * st_kq_, MAXC * 2 * 256));
o.ksc.push_back(view_of(kscBig_[j], (size_t) idx * st_sc_, 2 * MAXC * 2 * 4));
o.vsc.push_back(view_of(vscBig_[j], (size_t) idx * st_sc_, 2 * MAXC * 2 * 4));
o.tail.push_back(view_of(tailBig_[j], (size_t) idx * st_tail_, 4 * 4 * 128));
o.dead.push_back(view_of(deadBig_[j], (size_t) idx * st_dead_, 4 * 128));
o.pooled.push_back(view_of(pooledBig_[j], (size_t) idx * st_pooled_, 4 * MB * 128));
}
o.pleHist = view_of(pleBig_, (size_t) idx * st_ple_, 4 * 9 * 10240);
o.ple_hist.assign(ple_hist_.size(), 0.0f);
return;
}
auto A = [&](size_t bytes) { return real ? cx_->alloc(bytes) : mtl::Buffer(); };
for (size_t i = 0; i < gst_.size(); ++i) {
o.gst.push_back(A(4 * 128 * 48 * 128));
o.gh0.push_back(A(4 * 10240 * 3));
o.gh1.push_back(A(4 * 10240 * 3));
}
for (size_t j = 0; j < kq_.size(); ++j) {
o.kq.push_back(A(MAXC * 2 * 256));
o.vq.push_back(A(MAXC * 2 * 256));
o.ksc.push_back(A(2 * MAXC * 2 * 4));
o.vsc.push_back(A(2 * MAXC * 2 * 4));
o.tail.push_back(A(4 * 4 * 128));
o.dead.push_back(A(4 * 128));
o.pooled.push_back(A(4 * MB * 128));
}
o.pleHist = A(4 * 9 * 10240);
if (real) o.ple_hist.assign(ple_hist_.size(), 0.0f);
}
bool Window::slots_init(const std::vector<strata::core::SessionState*>& slots, std::string& err) {
if (!sets_.empty()) return true; // (asked once)
if (cx_ == nullptr) { err = "metal window: slots before the window is up"; return false; }
std::vector<std::unique_ptr<SeqSet>> v;
v.push_back(std::make_unique<SeqSet>());
alloc_set(*v[0], false);
v[0]->ss = ss_;
set_of_ss_[ss_] = 0;
uint64_t bytes = 0;
for (size_t b = 0; b < slots.size(); ++b) {
if ((int) b >= g_slot_cap) bigs_ok_ = false; // more slots than the window was sized for: serial windows only
auto x = std::make_unique<SeqSet>();
alloc_set(*x, true, (int) b + 1);
x->ss = slots[b];
for (auto* vec : {&x->gst, &x->gh0, &x->gh1, &x->kq, &x->vq, &x->ksc, &x->vsc, &x->tail, &x->dead, &x->pooled})
for (const auto& bf : *vec) bytes += bf.bytes;
set_of_ss_[slots[b]] = (int) b + 1;
v.push_back(std::move(x));
}
sets_ = std::move(v);
active_ = 0;
if (bigs_ok_) { // the strides the row kernels need (elements of each array's type)
uint32_t* h = rinfo_.as<uint32_t>();
h[0] = (uint32_t) st_kq_; h[1] = (uint32_t) (st_sc_ / 2); h[2] = (uint32_t) (st_tail_ / 4); h[3] = (uint32_t) (st_dead_ / 4);
h[4] = (uint32_t) (st_pooled_ / 4); h[5] = (uint32_t) (st_gst_ / 4); h[6] = (uint32_t) (st_hist_ / 4); h[7] = (uint32_t) (st_ple_ / 4);
}
std::fprintf(stderr, "strata metal: %zu batch slots, each with its own running state and K/V (%.2f GB each, %.2f GB in all)\n",
slots.size(), slots.empty() ? 0.0 : (double) bytes / (double) slots.size() / 1e9, (double) bytes / 1e9);
return true;
}
// Makes `set` the live state. The live members and the stashes exchange contents (two swaps through the active set's own stash).
void Window::activate(int set) {
if (set == active_ || set < 0 || set >= (int) sets_.size()) return;
swap_state(*sets_[(size_t) active_]); // the stash of the active one now holds its state, the live members the spare
swap_state(*sets_[(size_t) set]); // ... and now the wanted one
active_ = set;
}
bool Window::run_slots(const int* rows, int S, const int32_t* tokens, const int64_t* pos, int32_t* out, std::string& err) {
if (sets_.empty()) { err = "metal window: no batch slots"; return false; }
static const bool one_window = !(std::getenv("STRATA_METAL_BATCH_ROWS") && std::string(std::getenv("STRATA_METAL_BATCH_ROWS")) == "0");
if (one_window && bigs_ok_) {
activate(0);
return run_rows(rows, S, tokens, pos, out, err);
}
bool ok = true;
for (int t = 0; t < S && ok; ++t) {
if (rows[t] < 0 || rows[t] >= n_slots()) { err = "metal window: a batch row names a slot that does not exist"; ok = false; break; }
activate(rows[t] + 1);
// a one-token window commits its row in its own step; commit(1) adds the PLE history and the session's token pair,
// which read buffers the next window rewrites
ok = run(1, tokens + t, pos[t], true, out + t, err) && commit(1, err);
}
activate(0);
return ok;
}
bool Window::slot_copy(bool into_slot, int b, int64_t upto, bool kv_only, std::string& err) {
if (b < 0 || b >= n_slots()) { err = "metal window: no such batch slot"; return false; }
activate(0);
SeqSet& o = *sets_[(size_t) b + 1];
if (pend_fold_ > 0 || o.pend_fold > 0) { err = "metal window: a slot copy with a fold pending"; return false; }
const size_t cells = (size_t) std::max<int64_t>(0, std::min<int64_t>(upto, max_cells_));
auto cp = [&](const mtl::Buffer& live, const mtl::Buffer& stash, size_t n) {
n = std::min({n, live.bytes, stash.bytes});
if (into_slot) std::memcpy(stash.ptr, live.ptr, n); else std::memcpy(live.ptr, stash.ptr, n);
};
const int hs = into_slot ? hp_ : o.hp; // the committed history parity of the source
if (!kv_only) {
if (into_slot) o.hp = hp_; else hp_ = o.hp;
for (size_t i = 0; i < gst_.size(); ++i) {
cp(gst_[i], o.gst[i], gst_[i].bytes);
cp(ghist_[hs][i], hs ? o.gh1[i] : o.gh0[i], ghist_[hs][i].bytes);
}
}
for (size_t j = 0; j < kq_.size(); ++j) {
cp(kq_[j], o.kq[j], cells * 512);
cp(vq_[j], o.vq[j], cells * 512);
cp(ksc_[j], o.ksc[j], cells * 16);
cp(vsc_[j], o.vsc[j], cells * 16);
cp(pooled_[j], o.pooled[j], ((cells + 3) / 4 + 2) * 512);
if (kv_only) continue;
cp(tail_[j], o.tail[j], tail_[j].bytes);
cp(dead_[j], o.dead[j], dead_[j].bytes);
}
if (kv_only) return true;
cp(pleHist_, o.pleHist, pleHist_.bytes);
if (into_slot) { o.ple_hist = ple_hist_; o.ss->ple_prev[0] = ss_->ple_prev[0]; o.ss->ple_prev[1] = ss_->ple_prev[1]; }
else { ple_hist_ = o.ple_hist; ss_->ple_prev[0] = o.ss->ple_prev[0]; ss_->ple_prev[1] = o.ss->ple_prev[1]; }
return true;
}
bool Window::checkpoint_save(strata::core::ConversationCheckpoint& c, std::string& err) {
(void) err;
if (pend_fold_ > 0) { // the last commit's fold first: the checkpoint is the state after it
@@ -2358,7 +2691,7 @@ bool Window::prompt_chunk(const int32_t* tokens, int Tn, int64_t pos0, std::stri
mtl::Arg::B(p.al), mtl::Arg::B(p.be), mtl::Arg::B(W_, TN(b + "ssm_dt.bias").offset), mtl::Arg::B(W_, TN(b + "ssm_a").offset),
mtl::Arg::B(p.gqn), mtl::Arg::B(p.gkn), mtl::Arg::B(p.gvc), mtl::Arg::B(p.ggb), mtl::Arg::B(p.gkq),
mtl::Arg::B(ghist_[1 - hp_][(size_t) gi]), mtl::Arg::V(ga)}, 16 + 24, T, 256);
D(PP(p.lgdn, "gdn_scanr_4", mtl::Constants().i(6, 0), "gdn_scanr_4"),
D(PP(p.lgdn, gdn_scan_name(cx_->device_name(), T), mtl::Constants().i(6, 0), gdn_scan_name(cx_->device_name(), T)),
{mtl::Arg::B(gst_[(size_t) gi]), mtl::Arg::B(p.gqn), mtl::Arg::B(p.gkn), mtl::Arg::B(p.gvc), mtl::Arg::B(p.ggb),
mtl::Arg::B(p.gkq), mtl::Arg::B(p.go), mtl::Arg::V(ga)}, 48 * 4, 1, 32 * 4);
D(PP(p.lgdn, "gdn_post"), {mtl::Arg::B(p.go), mtl::Arg::B(p.z), mtl::Arg::B(W_, TN(b + "ssm_norm.weight").offset),
@@ -2585,6 +2918,7 @@ std::string g_shard1;
void window_register(void* w);
bool mtp_load_pending(Window* w, std::string& err);
void set_model_shard(const char* shard1) { g_shard1 = shard1 ? shard1 : ""; }
void set_batch_slots(int n) { g_slot_cap = n > 0 ? std::min(n, 8) : 0; }
// One decode window per process: the prompt path (Prefill::init) may bring it up before the Verifier does.
namespace {
@@ -2628,16 +2962,38 @@ bool window_run(void* w, int T, const int32_t* tokens, int64_t pos0, bool head,
static const bool logpos = std::getenv("STRATA_LOGPOS") != nullptr;
return ((Window*) w)->run(T, tokens, pos0, head || logpos, out, err);
}
void window_reset() {
if (g_the_window != nullptr && g_the_window->ready()) g_the_window->reset();
void window_reset(const strata::core::SessionState* ss) {
if (g_the_window == nullptr || !g_the_window->ready()) return;
g_the_window->activate(g_the_window->set_of(ss));
g_the_window->reset();
g_the_window->activate(0);
}
bool window_checkpoint_save(strata::core::ConversationCheckpoint& c, std::string& err) {
bool window_checkpoint_save(strata::core::ConversationCheckpoint& c, const strata::core::SessionState* ss, std::string& err) {
if (g_the_window == nullptr || !g_the_window->ready()) { err = "metal window: not up"; return false; }
return g_the_window->checkpoint_save(c, err);
g_the_window->activate(g_the_window->set_of(ss)); // a batch slot's session: that slot's running state
const bool ok = g_the_window->checkpoint_save(c, err);
g_the_window->activate(0);
return ok;
}
bool window_checkpoint_restore(const strata::core::ConversationCheckpoint& c, std::string& err) {
bool window_checkpoint_restore(const strata::core::ConversationCheckpoint& c, const strata::core::SessionState* ss, std::string& err) {
if (g_the_window == nullptr || !g_the_window->ready()) { err = "metal window: not up"; return false; }
return g_the_window->checkpoint_restore(c, err);
g_the_window->activate(g_the_window->set_of(ss));
const bool ok = g_the_window->checkpoint_restore(c, err);
g_the_window->activate(0);
return ok;
}
bool window_init_slots(void* w, const std::vector<strata::core::SessionState*>& slots, std::string& err) {
return ((Window*) w)->slots_init(slots, err);
}
bool window_run_slots(void* w, const int* rows, int S, const int32_t* tokens, const int64_t* pos, int32_t* out, std::string& err) {
const double t0 = now_ms();
const bool ok = ((Window*) w)->run_slots(rows, S, tokens, pos, out, err);
((Window*) w)->prof_add(true, now_ms() - t0);
return ok;
}
bool window_slot_copy_main(bool into_slot, int slot, int64_t upto, bool kv_only, std::string& err) {
if (g_the_window == nullptr || !g_the_window->ready()) { err = "metal window: not up"; return false; }
return g_the_window->slot_copy(into_slot, slot, upto, kv_only, err);
}
bool window_commit(void* w, int n_keep, std::string& err) {
const double t0 = now_ms();
+1 -5
View File
@@ -65,11 +65,7 @@ struct Context::Impl {
static inline id<MTLBuffer> B(const Buffer& b) { return (__bridge id<MTLBuffer>)b.handle; }
// the MTLBuffer and offset that bind byte `off` of b (a chunked buffer: the chunk the offset selects)
static inline id<MTLBuffer> BO(const Buffer& b, size_t off, size_t& o) {
#if defined(STRATA_DS4_RUNTIME)
if (!b.chunks) { o = off + b.bias; return B(b); }
#else
if (!b.chunks) { o = off; return B(b); }
#endif
if (!b.chunks) { o = off + b.bias; return B(b); } // (bias: a view onto part of a bigger allocation; 0 otherwise)
const size_t k = std::min(off / b.stride, b.chunks->size() - 1);
o = off - k * b.stride;
return (__bridge id<MTLBuffer>)(*b.chunks)[k];
+46 -2
View File
@@ -24,7 +24,7 @@ const char* type_name(int t) {
case Q3_K: return "Q3_K"; case Q4_K: return "Q4_K"; case Q5_K: return "Q5_K"; case Q6_K: return "Q6_K";
case IQ2_XXS: return "IQ2_XXS"; case IQ2_XS: return "IQ2_XS"; case IQ2_S: return "IQ2_S";
case IQ3_XXS: return "IQ3_XXS"; case IQ3_S: return "IQ3_S"; case IQ4_NL: return "IQ4_NL";
case IQ4_XS: return "IQ4_XS"; case Q2_0: return "Q2_0";
case IQ4_XS: return "IQ4_XS"; case Q2_0: return "Q2_0"; case IQ1_S: return "IQ1_S"; case IQ1_M: return "IQ1_M";
case TQ2_T: return "TQ2_T"; case TQK6: return "TQK6"; case TQK7: return "TQK7";
}
return "?";
@@ -46,7 +46,7 @@ int block_bytes(int t) {
case Q4_0: return 18; case Q5_0: return 22; case Q5_1: return 24; case Q8_0: return 34; case IQ4_NL: return 18; case Q2_0: return 18;
case Q3_K: return 110; case Q4_K: return 144; case Q5_K: return 176; case Q6_K: return 210;
case IQ2_XXS: return 66; case IQ2_XS: return 74; case IQ2_S: return 82; case IQ3_XXS: return 98;
case IQ3_S: return 110; case IQ4_XS: return 136;
case IQ3_S: return 110; case IQ4_XS: return 136; case IQ1_S: return 50; case IQ1_M: return 56;
case TQ2_T: return 34; case TQK6: return 26; case TQK7: return 30;
}
std::fprintf(stderr, "ref: unknown type %d\n", t);
@@ -292,6 +292,48 @@ static void deq_block(int t, const uint8_t* b, float* y) {
}
return;
}
case IQ1_S: { // ggml dequantize_row_iq1_s: dl = d (2 s + 1), y = dl (grid + delta)
const float d = f16_to_f32(rd16(b));
const uint8_t* qs = b + 2;
for (int ib = 0; ib < 8; ++ib) {
const uint16_t qh = rd16(b + 34 + 2 * ib);
const float dl = d * (float)(2 * ((qh >> 12) & 7) + 1);
const float delta = qh & 0x8000 ? -IQ1S_DELTA : IQ1S_DELTA;
for (int l = 0; l < 4; ++l) {
const int8_t* grid = (const int8_t*)(iq1s_grid + (qs[l] | (((qh >> 3 * l) & 7) << 8)));
for (int j = 0; j < 8; ++j) y[j] = dl * (grid[j] + delta);
y += 8;
}
qs += 4;
}
return;
}
case IQ1_M: { // ggml dequantize_row_iq1_m: the f16 scale in the top nibbles of the four u16 scales
const uint8_t* qs = b;
const uint8_t* qh = b + 32;
const uint16_t* sc = (const uint16_t*)(b + 48);
uint16_t s16 = (uint16_t)((sc[0] >> 12) | ((sc[1] >> 8) & 0x00f0) | ((sc[2] >> 4) & 0x0f00) | (sc[3] & 0xf000));
const float d = f16_to_f32(s16);
for (int ib = 0; ib < 8; ++ib) {
const float dl1 = d * (float)(2 * ((sc[ib / 2] >> (6 * (ib % 2) + 0)) & 7) + 1);
const float dl2 = d * (float)(2 * ((sc[ib / 2] >> (6 * (ib % 2) + 3)) & 7) + 1);
uint16_t idx[4];
idx[0] = qs[0] | ((qh[0] << 8) & 0x700);
idx[1] = qs[1] | ((qh[0] << 4) & 0x700);
idx[2] = qs[2] | ((qh[1] << 8) & 0x700);
idx[3] = qs[3] | ((qh[1] << 4) & 0x700);
const float delta[4] = {qh[0] & 0x08 ? -IQ1S_DELTA : IQ1S_DELTA, qh[0] & 0x80 ? -IQ1S_DELTA : IQ1S_DELTA,
qh[1] & 0x08 ? -IQ1S_DELTA : IQ1S_DELTA, qh[1] & 0x80 ? -IQ1S_DELTA : IQ1S_DELTA};
for (int l = 0; l < 4; ++l) {
const int8_t* grid = (const int8_t*)(iq1s_grid + idx[l]);
for (int j = 0; j < 8; ++j) y[j] = (l < 2 ? dl1 : dl2) * (grid[j] + delta[l]);
y += 8;
}
qs += 4;
qh += 2;
}
return;
}
case IQ2_XXS: {
const float d = f16_to_f32(rd16(b));
const uint8_t* qs = b + 2;
@@ -462,6 +504,8 @@ void random_blocks(int t, uint8_t* dst, int rows, int cols, uint64_t seed, float
case Q6_K: put16(b + 208, f32_to_f16(scale * u / 32 / 64)); break;
case Q3_K: put16(b + 108, f32_to_f16(scale * u / 32 / 2)); break;
case IQ2_XXS: case IQ2_XS: case IQ2_S: put16(b, f32_to_f16(scale * u / 4)); break;
case IQ1_S: put16(b, f32_to_f16(scale * u / 2)); break;
case IQ1_M: { uint16_t h = f32_to_f16(scale * u / 2); for (int j = 0; j < 4; ++j) { uint16_t w = (uint16_t)(b[48 + 2 * j] | (b[49 + 2 * j] << 8)); w = (uint16_t)((w & 0x0fff) | (((h >> (4 * j)) & 0xf) << 12)); b[48 + 2 * j] = (uint8_t)w; b[49 + 2 * j] = (uint8_t)(w >> 8); } break; }
case IQ3_XXS: put16(b, f32_to_f16(scale * u / 8)); break;
case IQ3_S: put16(b, f32_to_f16(scale * u / 64)); break;
case TQ2_T: case TQK6: case TQK7: put16(b, f32_to_f16(scale * u / 1.5f)); break; // codebook |point| ~ 1
+1 -1
View File
@@ -12,7 +12,7 @@ namespace ref {
// GGML type ids (stable, as in native_mmvq.hpp)
enum : int {
F32 = 0, F16 = 1, Q4_0 = 2, Q5_0 = 6, Q5_1 = 7, Q8_0 = 8, Q3_K = 11, Q4_K = 12, Q5_K = 13, Q6_K = 14,
IQ2_XXS = 16, IQ2_XS = 17, IQ3_XXS = 18, IQ3_S = 21, IQ2_S = 22, IQ4_NL = 20, IQ4_XS = 23, BF16 = 30, Q2_0 = 42,
IQ2_XXS = 16, IQ2_XS = 17, IQ3_XXS = 18, IQ1_S = 19, IQ1_M = 29, IQ3_S = 21, IQ2_S = 22, IQ4_NL = 20, IQ4_XS = 23, BF16 = 30, Q2_0 = 42,
TQ2_T = 144, TQK6 = 145, TQK7 = 146, // agentionai's trellis types (Gyro): 128 values a block, 34 / 26 / 30 bytes
};
+45
View File
@@ -0,0 +1,45 @@
// metal/tests/dsu_job.metal - test-only helpers appended to ds4_iq.metal / moe_mm.metal in dsu_parity.cpp.
// dsu_mkjob writes a Ds4MvJob whose pointers are inside the one buffer it was given (so everything the real kernel
// dereferences is a bound, resident buffer): mem + offsets.
#ifdef DSU_MKJOB
kernel void dsu_mkjob(device uchar* mem [[buffer(0)]], constant ulong* o [[buffer(1)]], constant int* a [[buffer(2)]]) {
// o: job, w, xq, xd, y offsets; a: type, k, n, ncol, ld
device Ds4MvJob& j = *(device Ds4MvJob*) (mem + o[0]);
j.w = mem + o[1];
j.ld = ulong(a[4]);
j.type = a[0];
j.k = a[1];
j.n = a[2];
j.ncol = a[3];
j.xq = (device const char*) (mem + o[2]);
j.xd = (device const float*) (mem + o[3]);
j.y = (device float*) (mem + o[4]);
j.ycs = ulong(a[2]);
j.bias = nullptr;
j.alpha = 1.0f;
}
#endif
#ifdef DSU_W8
// every 4-value run the tile loader reads (k and k + 16 of each 32-group's first half) for T, into half rows
template <int F>
static inline void dsu_w8_row(device const uchar* row, int k_total, device half* out, uint gid, threadgroup uint* tab) {
const int runs = k_total / 32 * 4; // per row: 4 runs of 4 values (k = 0,4,8,12 of the group), each giving k and k+16
if (int(gid) >= runs) return;
const int grp = int(gid) >> 2, r = int(gid) & 3;
const int k = grp * 32 + r * 4;
half4 a, b;
W8<F>::run(row, k, tab, a, b);
out[k] = a.x; out[k + 1] = a.y; out[k + 2] = a.z; out[k + 3] = a.w;
out[k + 16] = b.x; out[k + 17] = b.y; out[k + 18] = b.z; out[k + 19] = b.w;
}
kernel void dsu_w8_iq1s(device const uchar* W [[buffer(0)]], device half* out [[buffer(1)]], constant uint& k [[buffer(2)]],
constant uint& rb [[buffer(3)]], uint2 gid [[thread_position_in_grid]]) {
threadgroup uint tab[1];
dsu_w8_row<T_IQ1_S>(W + ulong(gid.y) * rb, int(k), out + ulong(gid.y) * k, gid.x, tab);
}
kernel void dsu_w8_iq1m(device const uchar* W [[buffer(0)]], device half* out [[buffer(1)]], constant uint& k [[buffer(2)]],
constant uint& rb [[buffer(3)]], uint2 gid [[thread_position_in_grid]]) {
threadgroup uint tab[1];
dsu_w8_row<T_IQ1_M>(W + ulong(gid.y) * rb, int(k), out + ulong(gid.y) * k, gid.x, tab);
}
#endif
+191
View File
@@ -0,0 +1,191 @@
// metal/tests/dsu_parity.cpp - the kernels Unsloth's UD-IQ1_S / UD-IQ1_M DeepSeek sets need on the Metal engine, against the CPU
// references in ref_quant.cpp (transcribed from ggml's dequantize_row_*; dsu_ref_vs_ggml checks that transcription against ggml itself):
// 1. ds4_iq1s_mvn1/2/4/8 (IQ1_S expert matvec; IQ1_M and IQ2_XXS as controls) and ds4_iq_mv_k (Q6_K dense matvec): the real engine
// kernels run on int8 activations; relative L2 error against the double-precision dot of the dequantized weights.
// 2. W8<T_IQ1_S> (the prompt tile's weight loader, ds4_mm_gu_19 / ds4_mm_dn_19): bit-exact against the half-rounded reference.
// 3. ds4_dq_q6k (the dense prompt matrices' Q6_K -> half): bit-exact against the half-rounded reference.
#include "mtl_rt.h"
#include "ref_quant.h"
#include <cmath>
#include <cstdio>
#include <cstring>
#include <string>
#include <vector>
namespace {
int fails = 0;
void report(const char* what, bool ok, double err) {
std::printf("%-62s err %.3e %s\n", what, err, ok ? "PASS" : "FAIL");
fails += !ok;
}
size_t al(size_t v) { return (v + 255) & ~size_t(255); }
// activations as the engine's q8 quantiser: per 32 values d = amax / 127, q = round(x / d)
void quant_x(const std::vector<float>& x, int k, int ncol, std::vector<int8_t>& q, std::vector<float>& d) {
q.assign((size_t)ncol * k, 0);
d.assign((size_t)ncol * k / 32, 0.f);
for (int c = 0; c < ncol; ++c)
for (int g = 0; g < k / 32; ++g) {
float am = 0;
for (int i = 0; i < 32; ++i) am = std::fmax(am, std::fabs(x[(size_t)c * k + g * 32 + i]));
const float dd = am / 127.f;
d[(size_t)c * (k / 32) + g] = dd;
for (int i = 0; i < 32; ++i)
q[(size_t)c * k + g * 32 + i] = dd == 0 ? 0 : (int8_t)std::lround(x[(size_t)c * k + g * 32 + i] / dd);
}
}
} // namespace
int main(int argc, char** argv) {
const bool bench = argc > 1 && std::string(argv[1]) == "--bench";
mtl::Context cx;
std::printf("device %s\n", cx.device_name().c_str());
const int lib = cx.compile({"../src/engine/msl/ds4_iq.metal", "../tests/dsu_job.metal"}, "#define DSU_MKJOB 1\n");
mtl::Pipeline mk = cx.pipeline(lib, "dsu_mkjob");
struct Case { const char* kern; int type; int nc; };
const std::vector<Case> dummy;
const std::vector<Case> cases = {{"ds4_iq1s_mvn1", 19, 1}, {"ds4_iq1s_mvn2", 19, 2}, {"ds4_iq1s_mvn4", 19, 4}, {"ds4_iq1s_mvn8", 19, 8},
{"ds4_iq1m_mvn4", 29, 4}, {"ds4_iq2xxs_mvn4", 16, 4}, {"ds4_iq_mv_k", 14, 8}, {"ds4_iq_mv_k", 14, 1},
{"ds4_q6k_mvn1", 14, 1}, {"ds4_q6k_mvn2", 14, 2}, {"ds4_q6k_mvn4", 14, 4}, {"ds4_q6k_mvn8", 14, 8}};
if (bench) { // dense decode matvec speed on a real attention-sized matrix: Q5_K (the 2.00 set's) against Q6_K (the Unsloth sets')
for (const Case& cs : std::vector<Case>{{"ds4_q5k_mvn1", 13, 1}, {"ds4_iq_mv_k", 14, 1}, {"ds4_iq1s_mvn1", 19, 1}, {"ds4_iq2xxs_mvn1", 16, 1}, {"ds4_iq_mv_k", 14, 8}, {"ds4_q5k_mvn8", 13, 8}, {"ds4_q6k_mvn1", 14, 1}, {"ds4_q6k_mvn8", 14, 8}}) {
const int k = 4096, n = 16384, nc = cs.nc;
const size_t rb = ref::row_bytes(cs.type, k);
std::vector<uint8_t> w(rb * n);
ref::random_blocks(cs.type, w.data(), n, k, 3, 0.05f);
const size_t o_w = al(80), o_xq = al(o_w + w.size()), o_xd = al(o_xq + (size_t)nc * k), o_y = al(o_xd + (size_t)nc * k / 8);
mtl::Buffer mem = cx.alloc(al(o_y + (size_t)nc * n * 4)), offs = cx.alloc(64), args = cx.alloc(64);
std::memcpy(mem.as<uint8_t>() + o_w, w.data(), w.size());
std::memset(mem.as<uint8_t>() + o_xq, 3, (size_t)nc * k);
unsigned long ov[5] = {0, o_w, o_xq, o_xd, o_y};
int av[5] = {cs.type, k, n, nc, (int)rb};
std::memcpy(offs.ptr, ov, sizeof ov);
std::memcpy(args.ptr, av, sizeof av);
mtl::Pipeline pk = cx.pipeline(lib, cs.kern);
mtl::Dispatch d0;
d0.p = &mk; d0.args = {mtl::Arg::B(mem), mtl::Arg::B(offs), mtl::Arg::B(args)}; d0.grid[0] = 1; d0.tg[0] = 1;
mtl::Dispatch d1;
d1.p = &pk; d1.args = {mtl::Arg::B(mem, 0)}; d1.grid[0] = (n + 7) / 8; d1.grid[1] = 1; d1.tg[0] = 256;
cx.run({d0});
std::vector<mtl::Dispatch> ds(20, d1);
cx.warm(ds, 0.3);
double best = 1e9;
for (int i = 0; i < 7; ++i) best = std::fmin(best, cx.run(ds) / 20);
std::printf("bench %-16s %-7s ncol %d: %.1f us %.0f GB/s of weights\n", cs.kern, ref::type_name(cs.type), nc, best * 1e6, (double)w.size() / best / 1e9);
}
return 0;
}
for (const Case& cs : cases) {
for (int k : {512, 4096}) {
const int n = 83; // not a multiple of 8: the last threadgroup's tail rows
const int nc = cs.nc;
const size_t rb = ref::row_bytes(cs.type, k);
std::vector<uint8_t> w(rb * n);
ref::random_blocks(cs.type, w.data(), n, k, 11 + cs.type + k, 0.05f);
ref::Rng r(77);
std::vector<float> x((size_t)nc * k);
for (auto& v : x) v = r.sym() * 3.f;
std::vector<int8_t> xq;
std::vector<float> xd;
quant_x(x, k, nc, xq, xd);
const size_t o_job = 0, o_w = al(80), o_xq = al(o_w + w.size()), o_xd = al(o_xq + xq.size()), o_y = al(o_xd + xd.size() * 4);
const size_t total = al(o_y + (size_t)nc * n * 4);
mtl::Buffer mem = cx.alloc(total), offs = cx.alloc(64), args = cx.alloc(64);
std::memcpy(mem.as<uint8_t>() + o_w, w.data(), w.size());
std::memcpy(mem.as<uint8_t>() + o_xq, xq.data(), xq.size());
std::memcpy(mem.as<uint8_t>() + o_xd, xd.data(), xd.size() * 4);
unsigned long ov[5] = {o_job, o_w, o_xq, o_xd, o_y};
int av[5] = {cs.type, k, n, nc, (int)rb};
std::memcpy(offs.ptr, ov, sizeof ov);
std::memcpy(args.ptr, av, sizeof av);
mtl::Pipeline pk = cx.pipeline(lib, cs.kern);
mtl::Dispatch d0;
d0.p = &mk; d0.args = {mtl::Arg::B(mem), mtl::Arg::B(offs), mtl::Arg::B(args)}; d0.grid[0] = 1; d0.tg[0] = 1;
mtl::Dispatch d1;
d1.p = &pk; d1.args = {mtl::Arg::B(mem, o_job)}; d1.grid[0] = (n + 7) / 8; d1.grid[1] = 1; d1.tg[0] = 256;
cx.run({d0, d1});
std::vector<float> wf((size_t)n * k);
ref::dequant(cs.type, w.data(), wf.data(), wf.size());
double se = 0, sr = 0;
const float* y = (const float*)(mem.as<uint8_t>() + o_y);
for (int c = 0; c < nc; ++c)
for (int row = 0; row < n; ++row) {
double acc = 0;
for (int i = 0; i < k; ++i)
acc += (double)wf[(size_t)row * k + i] * ((double)xq[(size_t)c * k + i] * xd[(size_t)c * (k / 32) + i / 32]);
const double g = y[(size_t)c * n + row];
se += (g - acc) * (g - acc);
sr += acc * acc;
}
const double e = std::sqrt(se / sr);
char nm[128];
std::snprintf(nm, sizeof nm, "%s %s k=%d n=%d ncol=%d", cs.kern, ref::type_name(cs.type), k, n, nc);
report(nm, e < 2e-6, e);
}
}
// 1b. the engine's own prompt-tile library compiles with the IQ1_S instantiation
{
const int libm = cx.compile({"quant.metal", "units.metal", "mma.metal", "moe_mm.metal", "../src/engine/msl/ds4_mm.metal"});
for (const char* fn : {"ds4_mm_gu_19", "ds4_mm_dn_19", "ds4_mm_gu_29", "ds4_mm_gu_16"}) {
mtl::Pipeline pp = cx.pipeline(libm, fn);
report(fn, pp.handle != nullptr, 0);
}
}
// 2. the tile loader
{
const int libw = cx.compile({"quant.metal", "units.metal", "mma.metal", "moe_mm.metal", "../tests/dsu_job.metal"}, "#define DSU_W8 1\n");
for (int ty : {19, 29}) {
const int k = 2048, n = 37;
const size_t rb = ref::row_bytes(ty, k);
mtl::Buffer W = cx.alloc(rb * n), out = cx.alloc((size_t)n * k * 2);
ref::random_blocks(ty, W.as<uint8_t>(), n, k, 5 + ty, 0.05f);
mtl::Pipeline pw = cx.pipeline(libw, ty == 19 ? "dsu_w8_iq1s" : "dsu_w8_iq1m");
mtl::Dispatch d;
d.p = &pw; d.args = {mtl::Arg::B(W), mtl::Arg::B(out), mtl::Arg::V((uint32_t)k), mtl::Arg::V((uint32_t)rb)};
d.grid[0] = (k / 32 * 4 + 63) / 64; d.grid[1] = n; d.tg[0] = 64;
cx.run({d});
std::vector<float> wf((size_t)n * k);
ref::dequant(ty, W.ptr, wf.data(), wf.size());
size_t bad = 0;
const uint16_t* o = out.as<uint16_t>();
for (size_t i = 0; i < wf.size(); ++i)
if (o[i] != ref::f32_to_f16(wf[i])) ++bad;
char nm[112];
std::snprintf(nm, sizeof nm, "tile loader W8<%s>, %zu values, mismatching half bits", ref::type_name(ty), wf.size());
report(nm, bad == 0, (double)bad);
}
}
// 3. Q6_K -> half
{
const int libd = cx.compile({"../src/engine/msl/ds4_init.metal"});
mtl::Pipeline pq = cx.pipeline(libd, "ds4_dq_q6k");
for (int k : {256, 2048}) {
const int n = 45;
const size_t rb = ref::row_bytes(14, k);
mtl::Buffer W = cx.alloc(rb * n), out = cx.alloc((size_t)n * k * 2);
ref::random_blocks(14, W.as<uint8_t>(), n, k, 3 + k, 0.05f);
mtl::Dispatch d;
const unsigned long ld = rb;
const uint32_t uk = k, un = n;
d.p = &pq; d.args = {mtl::Arg::B(W), mtl::Arg::V(ld), mtl::Arg::V(uk), mtl::Arg::V(un), mtl::Arg::B(out)};
d.grid[0] = (uint32_t)(((size_t)n * (k / 256) * 64 + 255) / 256); d.tg[0] = 256;
cx.run({d});
std::vector<float> wf((size_t)n * k);
ref::dequant(14, W.ptr, wf.data(), wf.size());
size_t bad = 0;
const uint16_t* o = out.as<uint16_t>();
for (size_t i = 0; i < wf.size(); ++i)
if (o[i] != ref::f32_to_f16(wf[i])) ++bad;
char nm[96];
std::snprintf(nm, sizeof nm, "ds4_dq_q6k k=%d n=%d, mismatching half bits", k, n);
report(nm, bad == 0, (double)bad);
}
}
std::printf("%s\n", fails ? "SOME FAILED" : "ALL PASS");
return fails ? 1 : 0;
}
+29
View File
@@ -0,0 +1,29 @@
// metal/tests/dsu_ref_vs_ggml.cpp - ref_quant.cpp's IQ1_S / IQ1_M / Q6_K dequantizers against ggml's own to_float (the pinned llama.cpp
// tree the engine build links), bit for bit. Built only with the engine (needs ggml-base).
#include "ref_quant.h"
#include "ggml.h"
#include <cstdio>
#include <cstring>
#include <vector>
int main() {
int fails = 0;
for (int ty : {ref::IQ1_S, ref::IQ1_M, ref::Q6_K, ref::IQ2_XXS}) {
const int n = 64, k = 2048;
std::vector<uint8_t> w(ref::row_bytes(ty, k) * n);
ref::random_blocks(ty, w.data(), n, k, 21 + ty, 0.05f);
std::vector<float> a((size_t)n * k), b((size_t)n * k);
ref::dequant(ty, w.data(), a.data(), a.size());
const ggml_type_traits* tr = ggml_get_type_traits((ggml_type)ty);
if (!tr || !tr->to_float) { std::printf("%s: ggml has no to_float\n", ref::type_name(ty)); return 2; }
tr->to_float(w.data(), b.data(), (int64_t)b.size());
const size_t bad = std::memcmp(a.data(), b.data(), a.size() * 4) == 0 ? 0 : 1;
size_t nbad = 0;
for (size_t i = 0; i < a.size(); ++i) nbad += std::memcmp(&a[i], &b[i], 4) != 0;
std::printf("%-8s ref_quant vs ggml to_float: %zu of %zu values differ %s\n", ref::type_name(ty), nbad, a.size(), nbad == 0 ? "PASS" : "FAIL");
fails += (bad || nbad);
}
return fails ? 1 : 0;
}
+374
View File
@@ -0,0 +1,374 @@
// metal/tests/gdn_chunk_bench.cpp - GDN prompt stage microbenchmark and numerical check (30 Oct GDN-M, phase 1).
// The sequential scans of gdn_prompt.metal (gdn_scanr_2/4/8, production = gdn_scanr_4) against the chunked C = 32 form
// (gdn_cprep -> gdn_cstate -> gdn_cout) at the real Flash-Next shape (h_k 16, h_v 48, S 128, 10240 conv channels),
// T = 512 .. 32768, with the same gdn_pre in front and gdn_post behind (the whole GDN stage). Checks against an FP64
// sequential oracle on a few heads (output o and final state), the pipeline thread limits, and continuation (a call
// split in two, chunked or sequential on either side, the second call on buffer offsets).
// gdn_chunk_bench [--T 512,2048,..] [--check] [--regime normal|strong] [--heads 0,17,47] [--reps 5] [--noscan]
#include "mtl_rt.h"
#include "ref_quant.h"
#include <algorithm>
#include <cmath>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <string>
#include <vector>
namespace {
constexpr int HK = 16, HV = 48, CH = 10240, S = 128, GC = 32, NSC = 14;
struct GdnPArgs { uint32_t n_tok, h_k, h_v, channels; float eps; uint32_t f32; };
double median(std::vector<double> v) { std::sort(v.begin(), v.end()); return v[v.size() / 2]; }
std::vector<int> ints(const char* s) {
std::vector<int> r;
for (const char* p = s; *p;) { r.push_back(std::atoi(p)); while (*p && *p != ',') ++p; if (*p) ++p; }
return r;
}
} // namespace
int main(int argc, char** argv) {
std::vector<int> Ts = {512, 1024, 2048, 4096, 8192, 16384, 32768}, heads = {0, 17, 47};
bool check = false, noscan = false, strong = false;
int reps = 5;
for (int i = 1; i < argc; ++i) {
std::string a = argv[i];
if (a == "--T") Ts = ints(argv[++i]);
else if (a == "--heads") heads = ints(argv[++i]);
else if (a == "--check") check = true;
else if (a == "--noscan") noscan = true;
else if (a == "--regime") strong = std::string(argv[++i]) == "strong";
else if (a == "--reps") reps = std::atoi(argv[++i]);
}
const int TMAX = *std::max_element(Ts.begin(), Ts.end());
mtl::Context cx;
const int lib = cx.compile({"gdn_prompt.metal"}, "#define NO_GGML_COMMON 1\n");
std::printf("device %s, regime %s, TMAX %d\n", cx.device_name().c_str(), strong ? "strong-decay" : "normal", TMAX);
auto F = [&](size_t n) { return cx.alloc(4 * n); };
const size_t NCM = (TMAX + GC - 1) / GC;
mtl::Buffer qkv = F((size_t)TMAX * CH), al = F((size_t)TMAX * HV), be = F((size_t)TMAX * HV), z = F((size_t)TMAX * HV * S),
cw = F(CH * 4), dt = F(HV), sa = F(HV), gam = F(S), st0 = F((size_t)S * HV * S), stX = F((size_t)S * HV * S),
stRef = F((size_t)S * HV * S), h0 = F(CH * 3), hOut = F(CH * 3), yh = cx.alloc(2 * (size_t)TMAX * HV * S),
yB = F((size_t)TMAX * HV * S), qn = F((size_t)(TMAX + 32) * HK * S), kn = F((size_t)(TMAX + 32) * HK * S),
vc = F((size_t)TMAX * HV * S), gb = F(2 * (size_t)TMAX * HV), kq = F((size_t)TMAX * HK),
o = F((size_t)TMAX * HV * S), oRef = F((size_t)TMAX * HV * S), cU = F((size_t)HV * NCM * GC * S),
cW = F((size_t)HV * NCM * GC * S), clg = F((size_t)HV * NCM * GC), cD = F((size_t)HV * NCM * GC * S), cM = F((size_t)HV * NCM * GC * GC);
ref::Rng r(21);
for (size_t i = 0; i < (size_t)TMAX * CH; ++i) qkv.as<float>()[i] = r.sym() * 2.0f;
for (size_t i = 0; i < (size_t)TMAX * HV; ++i) { al.as<float>()[i] = r.sym() * 3.0f; be.as<float>()[i] = r.sym() * 2.0f; }
for (size_t i = 0; i < (size_t)TMAX * HV * S; ++i) z.as<float>()[i] = r.sym() * 2.0f;
for (int i = 0; i < CH * 4; ++i) cw.as<float>()[i] = r.sym() * 0.6f;
for (int i = 0; i < HV; ++i) { dt.as<float>()[i] = r.sym(); sa.as<float>()[i] = strong ? -5.0f - r.uni() * 25.0f : -0.2f - r.uni() * 2.0f; }
for (int i = 0; i < S; ++i) gam.as<float>()[i] = 0.5f + r.uni();
for (int i = 0; i < S * HV * S; ++i) st0.as<float>()[i] = r.sym() * 0.1f;
for (int i = 0; i < CH * 3; ++i) h0.as<float>()[i] = r.sym();
mtl::Pipeline pre = cx.pipeline(lib, "gdn_pre"), post = cx.pipeline(lib, "gdn_post");
// the existing sequential scans: {name, threadgroups, threads}; index 1 = gdn_scanr_4 = production
struct Sv { const char* nm; int grid, tg; };
const Sv svs[NSC] = {{"gdn_scanr_2", HV * 4, 64}, {"gdn_scanr_4", HV * 4, 128}, {"gdn_scanr_8", HV * 4, 256},
{"gdn_scanr_4_8", HV * 4, 128}, {"gdn_scan_v", HV * 4, 256}, {"gdn_scan_1", HV * 4, 32},
{"gdn_scan_2", HV * 2, 64}, {"gdn_scan_4", HV, 128},
{"gdn_scanr_4_4", HV * 4, 128}, {"gdn_scanr_4_2", HV * 4, 128}, {"gdn_scanr_8_8", HV * 4, 256},
{"gdn_scanr_8_4", HV * 4, 256}, {"gdn_scanr_2_8", HV * 4, 64}, {"gdn_scanr_2_4", HV * 4, 64}};
std::vector<mtl::Pipeline> scan;
for (const Sv& v : svs) scan.push_back(cx.pipeline(lib, v.nm, std::string(v.nm) == "gdn_scan_v" || std::string(v.nm).find("scanr") != std::string::npos ? mtl::Constants().i(6, 0) : mtl::Constants()));
mtl::Pipeline pcp = cx.pipeline(lib, "gdn_cprep"), pcs = cx.pipeline(lib, "gdn_cstate"), pco = cx.pipeline(lib, "gdn_cout"), pcp2 = cx.pipeline(lib, "gdn_cprep2"), pcs2 = cx.pipeline(lib, "gdn_cstate2");
bool v2 = false; // chunk_d builds the v2 chunked form (gdn_cprep2 + gdn_cstate2)
std::printf("pipeline limits (max threads / simd / static tg mem; threads asked):");
for (size_t i = 0; i < scan.size(); ++i) std::printf(" %s %d/%d/%d (%d)", svs[i].nm, scan[i].max_threads, scan[i].simd_width, scan[i].static_tg_mem, svs[i].tg);
std::printf("\n pre %d/%d/%d (256) cprep %d/%d/%d (256) cstate %d/%d/%d (256) cout %d/%d/%d (128) post %d/%d/%d (128)\n",
pre.max_threads, pre.simd_width, pre.static_tg_mem, pcp.max_threads, pcp.simd_width, pcp.static_tg_mem, pcs.max_threads,
pcs.simd_width, pcs.static_tg_mem, pco.max_threads, pco.simd_width, pco.static_tg_mem, post.max_threads, post.simd_width,
post.static_tg_mem);
std::printf("scratch at T: U+W+delta+lg = %.1f KB per token (%.0f MB at T 2048, %.0f MB at T 4096)\n",
(3.0 * HV * S * 4 + HV * 4) / 1024.0, (3.0 * HV * S * 4 + HV * 4) * 2048 / 1e6,
(3.0 * HV * S * 4 + HV * 4) * 4096 / 1e6);
// token range [t0, t0 + n) as buffer offsets
auto pre_d = [&](int t0, int n, std::vector<mtl::Dispatch>& ds, bool first) {
GdnPArgs a{(uint32_t)n, HK, HV, CH, 1e-6f, 1u};
mtl::Dispatch d;
d.p = &pre;
// history: the chunk reads hist (h0 for the first call, hOut afterwards), writes hOut
d.args = {mtl::Arg::B(qkv, (size_t)t0 * CH * 4), mtl::Arg::B(first ? h0 : hOut), mtl::Arg::B(cw),
mtl::Arg::B(al, (size_t)t0 * HV * 4), mtl::Arg::B(be, (size_t)t0 * HV * 4), mtl::Arg::B(dt), mtl::Arg::B(sa),
mtl::Arg::B(qn, (size_t)t0 * HK * S * 4), mtl::Arg::B(kn, (size_t)t0 * HK * S * 4),
mtl::Arg::B(vc, (size_t)t0 * HV * S * 4), mtl::Arg::B(gb, (size_t)t0 * HV * 8), mtl::Arg::B(kq, (size_t)t0 * HK * 4),
mtl::Arg::B(hOut), mtl::Arg::V(a)};
d.grid[0] = HK + HV / 2; d.grid[1] = n; d.tg[0] = 256;
ds.push_back(d);
};
auto scan_d = [&](int which, int t0, int n, const mtl::Buffer& st, const mtl::Buffer& out, std::vector<mtl::Dispatch>& ds) {
GdnPArgs a{(uint32_t)n, HK, HV, CH, 1e-6f, 1u};
mtl::Dispatch d;
d.p = &scan[which];
d.args = {mtl::Arg::B(st), mtl::Arg::B(qn, (size_t)t0 * HK * S * 4), mtl::Arg::B(kn, (size_t)t0 * HK * S * 4),
mtl::Arg::B(vc, (size_t)t0 * HV * S * 4), mtl::Arg::B(gb, (size_t)t0 * HV * 8), mtl::Arg::B(kq, (size_t)t0 * HK * 4),
mtl::Arg::B(out, (size_t)t0 * HV * S * 4), mtl::Arg::V(a)};
d.grid[0] = svs[which].grid; d.tg[0] = svs[which].tg;
ds.push_back(d);
};
auto chunk_d = [&](int t0, int n, const mtl::Buffer& st, const mtl::Buffer& out, std::vector<mtl::Dispatch>& ds) {
GdnPArgs a{(uint32_t)n, HK, HV, CH, 1e-6f, 1u};
const uint32_t nc = (n + GC - 1) / GC;
if (v2) {
mtl::Dispatch f1; f1.p = &pcp2;
f1.args = {mtl::Arg::B(qn, (size_t)t0 * HK * S * 4), mtl::Arg::B(kn, (size_t)t0 * HK * S * 4),
mtl::Arg::B(vc, (size_t)t0 * HV * S * 4), mtl::Arg::B(gb, (size_t)t0 * HV * 8), mtl::Arg::B(cU), mtl::Arg::B(cW),
mtl::Arg::B(clg), mtl::Arg::B(cM), mtl::Arg::V(a)};
f1.grid[0] = HV; f1.grid[1] = nc; f1.tg[0] = 256; ds.push_back(f1);
mtl::Dispatch f2; f2.p = &pcs2;
f2.args = {mtl::Arg::B(st), mtl::Arg::B(qn, (size_t)t0 * HK * S * 4), mtl::Arg::B(kn, (size_t)t0 * HK * S * 4),
mtl::Arg::B(cU), mtl::Arg::B(cW), mtl::Arg::B(clg), mtl::Arg::B(cM), mtl::Arg::B(out, (size_t)t0 * HV * S * 4),
mtl::Arg::V(a)};
f2.grid[0] = HV * 4; f2.tg[0] = 128; ds.push_back(f2);
return;
}
mtl::Dispatch e1; e1.p = &pcp;
e1.args = {mtl::Arg::B(qn, (size_t)t0 * HK * S * 4), mtl::Arg::B(kn, (size_t)t0 * HK * S * 4),
mtl::Arg::B(vc, (size_t)t0 * HV * S * 4), mtl::Arg::B(gb, (size_t)t0 * HV * 8), mtl::Arg::B(cU), mtl::Arg::B(cW),
mtl::Arg::B(clg), mtl::Arg::V(a)};
e1.grid[0] = HV; e1.grid[1] = nc; e1.tg[0] = 256; ds.push_back(e1);
mtl::Dispatch e2; e2.p = &pcs;
e2.args = {mtl::Arg::B(st), mtl::Arg::B(qn, (size_t)t0 * HK * S * 4), mtl::Arg::B(kn, (size_t)t0 * HK * S * 4),
mtl::Arg::B(cU), mtl::Arg::B(cW), mtl::Arg::B(clg), mtl::Arg::B(cD), mtl::Arg::B(out, (size_t)t0 * HV * S * 4),
mtl::Arg::V(a)};
e2.grid[0] = HV * 4; e2.tg[0] = 256; ds.push_back(e2);
mtl::Dispatch e3; e3.p = &pco;
e3.args = {mtl::Arg::B(qn, (size_t)t0 * HK * S * 4), mtl::Arg::B(kn, (size_t)t0 * HK * S * 4), mtl::Arg::B(cD),
mtl::Arg::B(clg), mtl::Arg::B(out, (size_t)t0 * HV * S * 4), mtl::Arg::V(a)};
e3.grid[0] = HV; e3.grid[1] = nc; e3.tg[0] = 128; ds.push_back(e3);
};
auto post_d = [&](int t0, int n, const mtl::Buffer& out, std::vector<mtl::Dispatch>& ds) {
GdnPArgs a{(uint32_t)n, HK, HV, CH, 1e-6f, 1u};
mtl::Dispatch d;
d.p = &post;
d.args = {mtl::Arg::B(out, (size_t)t0 * HV * S * 4), mtl::Arg::B(z, (size_t)t0 * HV * S * 4), mtl::Arg::B(gam),
mtl::Arg::B(yh, (size_t)t0 * HV * S * 2), mtl::Arg::B(yB, (size_t)t0 * HV * S * 4), mtl::Arg::V(a)};
d.grid[0] = HV; d.grid[1] = n; d.tg[0] = 128;
ds.push_back(d);
};
mtl::Pipeline pf[3] = {cx.pipeline(lib, "gdn_scanf_4"), cx.pipeline(lib, "gdn_scanf_8"), cx.pipeline(lib, "gdn_scanf_2")};
std::printf("fused scan+post pipelines (max threads / static tg mem; 512 asked): TB4 %d/%d TB8 %d/%d TB2 %d/%d\n", pf[0].max_threads,
pf[0].static_tg_mem, pf[1].max_threads, pf[1].static_tg_mem, pf[2].max_threads, pf[2].static_tg_mem);
auto fused_d = [&](int which, int t0, int n, const mtl::Buffer& st, std::vector<mtl::Dispatch>& ds) {
GdnPArgs a{(uint32_t)n, HK, HV, CH, 1e-6f, 1u};
mtl::Dispatch d;
d.p = &pf[which];
d.args = {mtl::Arg::B(st), mtl::Arg::B(qn, (size_t)t0 * HK * S * 4), mtl::Arg::B(kn, (size_t)t0 * HK * S * 4),
mtl::Arg::B(vc, (size_t)t0 * HV * S * 4), mtl::Arg::B(gb, (size_t)t0 * HV * 8), mtl::Arg::B(kq, (size_t)t0 * HK * 4),
mtl::Arg::B(z, (size_t)t0 * HV * S * 4), mtl::Arg::B(gam), mtl::Arg::B(yh, (size_t)t0 * HV * S * 2),
mtl::Arg::B(yB, (size_t)t0 * HV * S * 4), mtl::Arg::V(a)};
d.grid[0] = HV; d.tg[0] = 512;
ds.push_back(d);
};
auto timeit = [&](const std::vector<mtl::Dispatch>& ds) {
cx.warm(ds, 0.3);
std::vector<double> t;
for (int i = 0; i < reps; ++i) t.push_back(cx.run(ds));
return median(t) * 1e3;
};
auto copy_st0 = [&](mtl::Buffer& dst) { std::memcpy(dst.ptr, st0.ptr, (size_t)S * HV * S * 4); };
// FP64 oracle for head h over tokens [t0, t0 + n) from the prepared tensors; state in/out [S][S] (i, j)
auto oracle = [&](int h, int t0, int n, std::vector<double>& St, std::vector<double>& out) {
const int hk = h % HK;
out.assign((size_t)n * S, 0.0);
std::vector<double> a(S), b(S);
for (int t = t0; t < t0 + n; ++t) {
const float* k = kn.as<float>() + ((size_t)t * HK + hk) * S;
const float* q = qn.as<float>() + ((size_t)t * HK + hk) * S;
const float* v = vc.as<float>() + ((size_t)t * HV + h) * S;
const double g = gb.as<float>()[((size_t)t * HV + h) * 2], bt = gb.as<float>()[((size_t)t * HV + h) * 2 + 1];
double kqv = 0;
for (int i = 0; i < S; ++i) kqv += (double)k[i] * q[i];
std::fill(a.begin(), a.end(), 0.0); std::fill(b.begin(), b.end(), 0.0);
for (int i = 0; i < S; ++i) {
const double* row = &St[(size_t)i * S];
const double ki = k[i], qi = q[i];
for (int j = 0; j < S; ++j) { a[j] += row[j] * ki; b[j] += row[j] * qi; }
}
std::vector<double> dl(S);
for (int j = 0; j < S; ++j) {
dl[j] = (v[j] - g * a[j]) * bt;
out[(size_t)(t - t0) * S + j] = (g * b[j] + dl[j] * kqv) / std::sqrt((double)S);
}
for (int i = 0; i < S; ++i) {
double* row = &St[(size_t)i * S];
for (int j = 0; j < S; ++j) row[j] = g * row[j] + (double)k[i] * dl[j];
}
}
};
auto initial = [&](int h, std::vector<double>& St) {
St.resize((size_t)S * S);
for (int i = 0; i < S; ++i) for (int j = 0; j < S; ++j) St[(size_t)i * S + j] = st0.as<float>()[((size_t)i * HV + h) * S + j];
};
struct Err { double l2, mx; int nonfinite; };
auto cmp_o = [&](const mtl::Buffer& ob, int h, int t0, int n, const std::vector<double>& ro) {
double se = 0, sr = 0, mx = 0, rmax = 0; int nf = 0;
for (int t = 0; t < n; ++t) for (int j = 0; j < S; ++j) {
const float x = ob.as<float>()[((size_t)(t0 + t) * HV + h) * S + j];
if (!std::isfinite(x)) { ++nf; continue; }
const double e = x - ro[(size_t)t * S + j];
se += e * e; sr += ro[(size_t)t * S + j] * ro[(size_t)t * S + j]; mx = std::max(mx, std::fabs(e)); rmax = std::max(rmax, std::fabs(ro[(size_t)t * S + j]));
}
return Err{std::sqrt(se / std::max(sr, 1e-30)), mx / std::max(rmax, 1e-30), nf};
};
auto cmp_s = [&](const mtl::Buffer& sb, int h, const std::vector<double>& rs) {
double se = 0, sr = 0, mx = 0, rmax = 0; int nf = 0;
for (int i = 0; i < S; ++i) for (int j = 0; j < S; ++j) {
const float x = sb.as<float>()[((size_t)i * HV + h) * S + j];
if (!std::isfinite(x)) { ++nf; continue; }
const double e = x - rs[(size_t)i * S + j];
se += e * e; sr += rs[(size_t)i * S + j] * rs[(size_t)i * S + j]; mx = std::max(mx, std::fabs(e)); rmax = std::max(rmax, std::fabs(rs[(size_t)i * S + j]));
}
return Err{std::sqrt(se / std::max(sr, 1e-30)), mx / std::max(rmax, 1e-30), nf};
};
// prepared tensors once for all TMAX tokens (gdn_pre per call carries the history; here one call, the tokens independent
// of the scan variant). For the continuation check the pre runs per split below.
{
std::vector<mtl::Dispatch> ds;
// gdn_pre on a very long T reads hist only for the first 3 tokens; one call over TMAX is the same as pieces
pre_d(0, TMAX, ds, true);
cx.run(ds);
}
int fails = 0;
if (check) {
std::printf("== numerical checks vs FP64 oracle (rel L2 / max-abs over max|ref|, per head)\n");
for (int T : Ts) {
// the variants, each from st0, over tokens [0, T)
struct V { const char* name; int kind; }; // kind: 0..2 scanr IB 2/4/8, 10 chunked
for (V v : {V{"scanr_4", 1}, V{"chunked", 10}, V{"chunked2", 11}}) {
copy_st0(stX);
std::vector<mtl::Dispatch> ds;
v2 = v.kind == 11; if (v.kind >= 10) chunk_d(0, T, stX, o, ds); else scan_d(v.kind, 0, T, stX, o, ds); v2 = false;
cx.run(ds);
for (int h : heads) {
std::vector<double> St, ro; initial(h, St); oracle(h, 0, T, St, ro);
Err eo = cmp_o(o, h, 0, T, ro), es = cmp_s(stX, h, St);
const bool ok = eo.l2 < 3e-3 && es.l2 < 3e-3 && !eo.nonfinite && !es.nonfinite;
fails += !ok;
std::printf("T %5d %-8s head %2d: o rel %.2e (max %.2e) state rel %.2e (max %.2e) nonfinite %d %s\n", T, v.name,
h, eo.l2, eo.mx, es.l2, es.mx, eo.nonfinite + es.nonfinite, ok ? "PASS" : "FAIL");
if (T > 8192) break; // one head at the largest sizes (oracle cost)
}
}
}
// continuation: T = A + B, second call on offsets and the first call's state; every pairing
std::printf("== continuation (state carried across two calls, 2nd call on buffer offsets) vs oracle over the whole range\n");
const std::pair<int, int> splits[] = {{37, 963}, {1024, 1024}, {2048, 2047}, {31, 33}, {1, 4095}};
for (auto [A, B] : splits) {
if (A + B > TMAX) continue;
const char* nm[8] = {"chunk+chunk", "scan+chunk", "chunk+scan", "scan+scan", "chunk2+chunk2", "scan+chunk2", "chunk2+scan", "-"};
for (int m = 0; m < 8; ++m) {
v2 = m >= 4;
if (v2 && m == 7) continue; // scan+scan has no chunked part
copy_st0(stX);
std::vector<mtl::Dispatch> d1, d2;
if (m % 4 == 0 || m % 4 == 2) chunk_d(0, A, stX, o, d1); else scan_d(1, 0, A, stX, o, d1);
if (m % 4 == 0 || m % 4 == 1) chunk_d(A, B, stX, o, d2); else scan_d(1, A, B, stX, o, d2);
cx.run(d1); cx.run(d2); v2 = false;
for (int h : {heads[0], heads.back()}) {
std::vector<double> St, ro; initial(h, St); oracle(h, 0, A + B, St, ro);
Err eo = cmp_o(o, h, 0, A + B, ro), es = cmp_s(stX, h, St);
const bool ok = eo.l2 < 3e-3 && es.l2 < 3e-3 && !eo.nonfinite && !es.nonfinite;
fails += !ok;
std::printf("split %5d + %5d %-14s head %2d: o rel %.2e state rel %.2e %s\n", A, B, nm[m], h, eo.l2, es.l2, ok ? "PASS" : "FAIL");
}
}
}
std::printf("== bitwise: gdn_scanr_4_8 / _4_4 / _4_2 / _8_8 / _2_8 o and state vs gdn_scanr_4\n");
for (int T : Ts) {
if (T > 16384) continue;
copy_st0(stX);
std::vector<mtl::Dispatch> d0; scan_d(1, 0, T, stX, o, d0); cx.run(d0);
std::vector<float> oref(o.as<float>(), o.as<float>() + (size_t)T * HV * S), sref(stX.as<float>(), stX.as<float>() + (size_t)S * HV * S);
for (int w : {3, 8, 9, 10, 12}) {
copy_st0(stX);
std::memset(o.ptr, 0, (size_t)T * HV * S * 4);
std::vector<mtl::Dispatch> d1; scan_d(w, 0, T, stX, o, d1); cx.run(d1);
const bool eq = std::memcmp(o.ptr, oref.data(), oref.size() * 4) == 0 && std::memcmp(stX.ptr, sref.data(), sref.size() * 4) == 0;
fails += !eq;
std::printf("T %5d %-14s %s\n", T, svs[w].nm, eq ? "BITWISE EQUAL" : "DIFFERS");
}
}
std::printf("== fused scan+post vs pre/scanr_4/post: y rel L2, fp16 y mismatches, state rel L2\n");
for (int T : Ts) {
if (T > 8192) continue;
copy_st0(stX);
std::vector<mtl::Dispatch> d0; scan_d(1, 0, T, stX, o, d0); post_d(0, T, o, d0); cx.run(d0);
std::vector<float> yref(yB.as<float>(), yB.as<float>() + (size_t)T * HV * S), sref(stX.as<float>(), stX.as<float>() + (size_t)S * HV * S);
std::vector<uint16_t> href(yh.as<uint16_t>(), yh.as<uint16_t>() + (size_t)T * HV * S);
for (int w = 0; w < 3; ++w) {
copy_st0(stX);
std::memset(yB.ptr, 0, (size_t)T * HV * S * 4);
std::vector<mtl::Dispatch> d1; fused_d(w, 0, T, stX, d1); cx.run(d1);
double se = 0, sr = 0, ss = 0, ssr = 0; size_t hm = 0;
for (size_t i = 0; i < (size_t)T * HV * S; ++i) { const double e = yB.as<float>()[i] - yref[i]; se += e * e; sr += (double)yref[i] * yref[i]; hm += yh.as<uint16_t>()[i] != href[i]; }
for (size_t i = 0; i < (size_t)S * HV * S; ++i) { const double e = stX.as<float>()[i] - sref[i]; ss += e * e; ssr += (double)sref[i] * sref[i]; }
const bool ok = std::sqrt(se / sr) < 1e-5 && std::sqrt(ss / ssr) < 1e-5;
fails += !ok;
std::printf("T %5d fused TB%-2d: y rel %.2e, fp16 mismatches %zu, state rel %.2e %s\n", T, w == 0 ? 4 : w == 1 ? 8 : 2, std::sqrt(se / sr), hm, std::sqrt(ss / ssr), ok ? "PASS" : "FAIL");
}
}
std::printf("%s\n", fails ? "SOME FAILED" : "ALL PASS");
}
// timing: gdn_pre, the scan variants, chunked parts, gdn_post; the stage = pre + scan + post
std::printf("== timing (GPU ms per call, median of %d; stage = pre + scan + post)\n", reps);
std::printf("%6s | %7s %7s |", "T", "pre", "post");
for (const Sv& v : svs) std::printf(" %s", v.nm + 4);
std::printf(" | chunked (cprep cstate cout) | stage scanr_4 / chunked / x\n");
for (int T : Ts) {
std::vector<mtl::Dispatch> dp, dpo, dc;
pre_d(0, T, dp, true);
post_d(0, T, o, dpo);
const double tp = timeit(dp), tpo = timeit(dpo);
std::printf("%6d | %7.3f %7.3f |", T, tp, tpo);
double ts1 = 0, tbest = 1e30;
int wbest = 1;
for (int w = 0; w < NSC; ++w) {
std::vector<mtl::Dispatch> d1;
double t = 0;
if (!noscan) { scan_d(w, 0, T, stX, o, d1); t = timeit(d1); }
if (w == 1) ts1 = t;
if (t > 0 && t < tbest) { tbest = t; wbest = w; }
std::printf(" %8.3f", t);
}
chunk_d(0, T, stX, o, dc);
const double tc = timeit(dc), tc1 = timeit({dc[0]}), tc2 = timeit({dc[1]}), tc3 = timeit({dc[2]});
const double base = tp + ts1 + tpo, ch = tp + tc + tpo;
std::printf(" | %8.3f (%7.3f %7.3f %7.3f) | %8.3f %8.3f %5.2f | best %s stage %.3f = x%.3f (scan x%.3f)\n", tc, tc1, tc2, tc3, base, ch, base / ch, svs[wbest].nm, tp + tbest + tpo, base / (tp + tbest + tpo), ts1 / tbest);
{
v2 = true;
std::vector<mtl::Dispatch> dc2;
chunk_d(0, T, stX, o, dc2);
const double t2 = timeit(dc2), t2a = timeit({dc2[0]}), t2b = timeit({dc2[1]});
v2 = false;
std::printf(" chunked v2 %.3f (cprep2 %.3f cstate2 %.3f); stage pre+v2+post %.3f = x%.3f vs scanr_4 stage (scan x%.3f)\n", t2, t2a, t2b,
tp + t2 + tpo, base / (tp + t2 + tpo), ts1 / t2);
}
if (std::getenv("GDN_PROBE_RUN")) {
v2 = true;
std::vector<mtl::Dispatch> dd;
chunk_d(0, T, stX, o, dd);
for (int pr = 1; pr <= 3; ++pr) {
mtl::Pipeline a1 = cx.pipeline(lib, "gdn_cprep2", mtl::Constants().i(6, pr)), a2 = cx.pipeline(lib, "gdn_cstate2", mtl::Constants().i(6, pr));
mtl::Dispatch d1 = dd[0], d2 = dd[1];
d1.p = &a1; d2.p = &a2;
const double x1 = timeit({d1}), x2 = timeit({d2});
std::printf(" probe %d: cprep2 %.3f cstate2 %.3f ms\n", pr, x1, x2);
}
v2 = false;
}
{
double tf[3];
for (int w = 0; w < 3; ++w) { std::vector<mtl::Dispatch> d1; fused_d(w, 0, T, stX, d1); tf[w] = timeit(d1); }
const double fb = tp + std::min(tf[0], std::min(tf[1], tf[2]));
std::printf(" fused scan+post TB4 %.3f TB8 %.3f TB2 %.3f ms; stage pre+fused best %.3f = x%.3f vs scanr_4 stage\n", tf[0], tf[1], tf[2], fb, base / fb);
}
std::fflush(stdout);
}
return fails ? 1 : 0;
}
+194
View File
@@ -0,0 +1,194 @@
// metal/tests/gdn_layer_bench.cpp - one Flash-Next GDN layer's prompt cost, piece by piece (30 Oct GDN-M phase 2).
// Real shapes (N 2560, qkv 10240, z 6144, alpha/beta 48, out 2560 x 6144), a chunk of T rows (the engine's prompt chunk is
// 2048): dq (Q6_K -> fp16 weights), the four input GEMMs separate and fused into one, tile choices, gdn_pre, the scan, gdn_post,
// the out GEMM; each alone (GPU time) and the layer in order, plus the TFLOP/s of every GEMM. Random weights: a timing
// of the byte and FLOP stream, not a numerical check.
// gdn_layer_bench [--T 2048] [--reps 7] [--scan gdn_scanr_4]
#include "mtl_rt.h"
#include "ref_quant.h"
#include <algorithm>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <string>
#include <vector>
namespace {
constexpr int N = 2560, HK = 16, HV = 48, CH = 10240, ZD = 6144, S = 128, TQ6K = 14;
struct GemmArgs { int M, N, K, pad; };
struct DqArgs { uint32_t n_in, n_out, row_bytes, perm; };
struct GdnPArgs { uint32_t n_tok, h_k, h_v, channels; float eps; uint32_t f32; };
double median(std::vector<double> v) { std::sort(v.begin(), v.end()); return v[v.size() / 2]; }
} // namespace
int main(int argc, char** argv) {
int T = 2048, reps = 7;
std::string scan_name = "gdn_scanr_4";
for (int i = 1; i < argc; ++i) {
std::string a = argv[i];
if (a == "--T") T = std::atoi(argv[++i]);
else if (a == "--reps") reps = std::atoi(argv[++i]);
else if (a == "--scan") scan_name = argv[++i];
}
mtl::Context cx;
const int lib = cx.compile({"quant.metal", "units.metal", "mma.metal", "dense_mm.metal"});
const int lgdn = cx.compile({"gdn.metal", "gdn_prompt.metal"}, "#define NO_GGML_COMMON 1\n");
std::printf("device %s, chunk T %d, scan %s\n", cx.device_name().c_str(), T, scan_name.c_str());
ref::Rng r(5);
auto fill_half = [&](mtl::Buffer& b, size_t n, float sc) {
for (size_t i = 0; i < n; ++i) b.as<uint16_t>()[i] = ref::f32_to_f16(r.sym() * sc);
};
const int rows[5] = {CH, ZD, 48, 48, N}; // qkv, z, alpha, beta, out
const int kin[5] = {N, N, N, N, ZD};
mtl::Buffer mixh = cx.alloc(2 * (size_t)T * N), ygh = cx.alloc(2 * (size_t)T * ZD);
fill_half(mixh, (size_t)T * N, 1.0f);
fill_half(ygh, (size_t)T * ZD, 1.0f);
mtl::Buffer Wq[5], Wh[5], Y[5];
mtl::Buffer qsrc[5];
for (int i = 0; i < 5; ++i) {
Wh[i] = cx.alloc(2 * (size_t)rows[i] * kin[i]);
fill_half(Wh[i], (size_t)rows[i] * kin[i], 0.03f);
Y[i] = cx.alloc(4 * (size_t)T * rows[i]);
const size_t rb = ref::row_bytes(TQ6K, kin[i]);
qsrc[i] = cx.alloc(rb * rows[i]);
for (size_t j = 0; j < rb * rows[i]; ++j) qsrc[i].as<uint8_t>()[j] = (uint8_t)(r.uni() * 255.0f);
// keep the fp16 scale of each Q6_K block sane (the last two bytes of a 210 byte block)
for (size_t j = 0; j + 210 <= rb * rows[i]; j += 210) { qsrc[i].as<uint16_t>()[(j + 208) / 2] = ref::f32_to_f16(0.01f); }
}
const int NF = CH + ZD + 48 + 48;
mtl::Buffer WF = cx.alloc(2 * (size_t)NF * N), YF = cx.alloc(4 * (size_t)T * NF);
fill_half(WF, (size_t)NF * N, 0.03f);
auto timeit = [&](const std::vector<mtl::Dispatch>& ds) {
cx.warm(ds, 0.3);
std::vector<double> t;
for (int i = 0; i < reps; ++i) t.push_back(cx.run(ds));
return median(t) * 1e3;
};
auto dq_d = [&](int i) {
DqArgs a{(uint32_t)kin[i], (uint32_t)rows[i], (uint32_t)ref::row_bytes(TQ6K, kin[i]), 0};
static mtl::Pipeline p = cx.pipeline(lib, "dq_f16_14");
mtl::Dispatch d; d.p = &p;
d.args = {mtl::Arg::B(qsrc[i]), mtl::Arg::B(Wh[i]), mtl::Arg::V(a)};
d.grid[0] = (kin[i] / 32 + 63) / 64; d.grid[1] = rows[i]; d.tg[0] = 64;
return d;
};
auto gemm_d = [&](const mtl::Buffer& A, const mtl::Buffer& B, const mtl::Buffer& Yb, int M, int Nn, int K, int tm, int tn, int sgn = 4) {
static std::vector<std::pair<std::string, mtl::Pipeline*>> cache;
const std::string fn = "gemm_f16_" + std::to_string(tm) + "_" + std::to_string(tn) + "_" + std::to_string(sgn);
mtl::Pipeline* p = nullptr;
for (auto& c : cache) if (c.first == fn) p = c.second;
if (!p) { p = new mtl::Pipeline(cx.pipeline(lib, fn)); cache.push_back({fn, p}); }
GemmArgs g{M, Nn, K, 0};
mtl::Dispatch d; d.p = p;
d.args = {mtl::Arg::B(A), mtl::Arg::B(B), mtl::Arg::B(Yb), mtl::Arg::V(g)};
d.grid[0] = (Nn + tn - 1) / tn; d.grid[1] = (M + tm - 1) / tm; d.tg[0] = 32 * sgn;
return d;
};
auto tfl = [&](int Nn, int K, double ms) { return 2.0 * T * Nn * K / (ms * 1e-3) / 1e12; };
std::printf("== dq (Q6_K -> fp16), ms: qkv %.3f z %.3f alpha %.3f beta %.3f out %.3f\n", timeit({dq_d(0)}), timeit({dq_d(1)}),
timeit({dq_d(2)}), timeit({dq_d(3)}), timeit({dq_d(4)}));
std::printf("== GEMMs (tile TMxTN, 4 simdgroups): ms and TFLOP/s\n");
struct Sh { const char* nm; int i; };
struct Tl { int tm, tn, sg; };
const std::vector<Tl> tiles = {{64, 32, 4}, {64, 64, 4}, {128, 64, 4}, {64, 128, 4}, {128, 128, 4}, {128, 128, 8}, {128, 64, 8},
{64, 64, 8}, {256, 64, 8}, {64, 256, 8}, {128, 32, 4}, {32, 128, 4}};
for (Sh s : {Sh{"qkv", 0}, Sh{"z", 1}, Sh{"alpha", 2}, Sh{"out", 4}}) {
std::printf("%-6s N %5d K %4d:", s.nm, rows[s.i], kin[s.i]);
const mtl::Buffer& A = s.i == 4 ? ygh : mixh;
for (const Tl& tl : tiles) {
if (rows[s.i] <= 64 && tl.tn > 32) continue;
const double ms = timeit({gemm_d(A, Wh[s.i], Y[s.i], T, rows[s.i], kin[s.i], tl.tm, tl.tn, tl.sg)});
std::printf(" %dx%d/%d %.3f (%.0f)", tl.tm, tl.tn, tl.sg, ms, tfl(rows[s.i], kin[s.i], ms));
}
std::printf("\n");
}
std::printf("fused qkv+z+alpha+beta N %d:", NF);
double best_f = 1e9;
for (const Tl& tl : tiles) {
const double ms = timeit({gemm_d(mixh, WF, YF, T, NF, N, tl.tm, tl.tn, tl.sg)});
best_f = std::min(best_f, ms);
std::printf(" %dx%d/%d %.3f (%.0f)", tl.tm, tl.tn, tl.sg, ms, tfl(NF, N, ms));
}
std::printf("\n");
// the four input GEMMs as the engine runs them, in order
const double sep = timeit({gemm_d(mixh, Wh[0], Y[0], T, CH, N, 64, 64), gemm_d(mixh, Wh[1], Y[1], T, ZD, N, 64, 64),
gemm_d(mixh, Wh[2], Y[2], T, 48, N, 64, 32), gemm_d(mixh, Wh[3], Y[3], T, 48, N, 64, 32)});
std::printf("== input projections: separate (engine tiles) %.3f ms, fused best %.3f ms\n", sep, best_f);
// pre / scan / post
mtl::Buffer qkv = Y[0], z = Y[1], al = Y[2], be = Y[3], cw = cx.alloc(4 * CH * 4), dt = cx.alloc(4 * HV), sa = cx.alloc(4 * HV),
gam = cx.alloc(4 * S), st = cx.alloc(4 * (size_t)S * HV * S), h0 = cx.alloc(4 * CH * 3), h1 = cx.alloc(4 * CH * 3),
qn = cx.alloc(4 * (size_t)T * HK * S), kn = cx.alloc(4 * (size_t)T * HK * S), vc = cx.alloc(4 * (size_t)T * HV * S),
gb = cx.alloc(8 * (size_t)T * HV), kq = cx.alloc(4 * (size_t)T * HK), o = cx.alloc(4 * (size_t)T * HV * S),
yh = cx.alloc(2 * (size_t)T * HV * S), y32 = cx.alloc(4);
for (int i = 0; i < CH * 4; ++i) cw.as<float>()[i] = r.sym() * 0.6f;
for (int i = 0; i < HV; ++i) { dt.as<float>()[i] = r.sym(); sa.as<float>()[i] = -0.2f - r.uni() * 2.0f; }
for (int i = 0; i < S; ++i) gam.as<float>()[i] = 0.5f + r.uni();
for (size_t i = 0; i < (size_t)T * CH; ++i) qkv.as<float>()[i] = r.sym() * 2.0f;
for (size_t i = 0; i < (size_t)T * HV; ++i) { al.as<float>()[i] = r.sym() * 3.0f; be.as<float>()[i] = r.sym() * 2.0f; }
for (size_t i = 0; i < (size_t)T * ZD; ++i) z.as<float>()[i] = r.sym() * 2.0f;
mtl::Pipeline pre = cx.pipeline(lgdn, "gdn_pre"), post = cx.pipeline(lgdn, "gdn_post"),
scan = cx.pipeline(lgdn, scan_name, mtl::Constants().i(6, 0));
GdnPArgs ga{(uint32_t)T, HK, HV, CH, 1e-6f, 0};
mtl::Dispatch dpre; dpre.p = &pre;
dpre.args = {mtl::Arg::B(qkv), mtl::Arg::B(h0), mtl::Arg::B(cw), mtl::Arg::B(al), mtl::Arg::B(be), mtl::Arg::B(dt), mtl::Arg::B(sa),
mtl::Arg::B(qn), mtl::Arg::B(kn), mtl::Arg::B(vc), mtl::Arg::B(gb), mtl::Arg::B(kq), mtl::Arg::B(h1), mtl::Arg::V(ga)};
dpre.grid[0] = HK + HV / 2; dpre.grid[1] = T; dpre.tg[0] = 256;
mtl::Dispatch dscan; dscan.p = &scan;
dscan.args = {mtl::Arg::B(st), mtl::Arg::B(qn), mtl::Arg::B(kn), mtl::Arg::B(vc), mtl::Arg::B(gb), mtl::Arg::B(kq), mtl::Arg::B(o),
mtl::Arg::V(ga)};
dscan.grid[0] = HV * 4; dscan.tg[0] = 128;
mtl::Dispatch dpost; dpost.p = &post;
dpost.args = {mtl::Arg::B(o), mtl::Arg::B(z), mtl::Arg::B(gam), mtl::Arg::B(yh), mtl::Arg::B(y32), mtl::Arg::V(ga)};
dpost.grid[0] = HV; dpost.grid[1] = T; dpost.tg[0] = 128;
const double tpre = timeit({dpre}), tscan = timeit({dscan}), tpost = timeit({dpost});
std::printf("== gdn_pre %.3f, scan %.3f, gdn_post %.3f ms (stage %.3f)\n", tpre, tscan, tpost, tpre + tscan + tpost);
double tF = 0;
{ // the fused-stride kernels (function constant 7) against the separate ones: bit for bit
for (int t = 0; t < T; ++t) {
std::memcpy(YF.as<float>() + (size_t)t * NF, qkv.as<float>() + (size_t)t * CH, 4 * CH);
std::memcpy(YF.as<float>() + (size_t)t * NF + CH, z.as<float>() + (size_t)t * ZD, 4 * ZD);
std::memcpy(YF.as<float>() + (size_t)t * NF + CH + ZD, al.as<float>() + (size_t)t * 48, 4 * 48);
std::memcpy(YF.as<float>() + (size_t)t * NF + CH + ZD + 48, be.as<float>() + (size_t)t * 48, 4 * 48);
}
std::memset(h1.ptr, 0, 4 * CH * 3);
cx.run({dpre, dscan, dpost});
std::vector<char> rqn((char*)qn.ptr, (char*)qn.ptr + 4 * (size_t)T * HK * S), rvc((char*)vc.ptr, (char*)vc.ptr + 4 * (size_t)T * HV * S),
rgb((char*)gb.ptr, (char*)gb.ptr + 8 * (size_t)T * HV), ryh((char*)yh.ptr, (char*)yh.ptr + 2 * (size_t)T * HV * S),
rh((char*)h1.ptr, (char*)h1.ptr + 4 * CH * 3);
mtl::Pipeline preF = cx.pipeline(lgdn, "gdn_pre", mtl::Constants().i(7, NF)), postF = cx.pipeline(lgdn, "gdn_post", mtl::Constants().i(7, NF));
mtl::Dispatch fpre = dpre, fpost = dpost;
fpre.p = &preF; fpost.p = &postF;
fpre.args[0] = mtl::Arg::B(YF, 0);
fpre.args[3] = mtl::Arg::B(YF, 4 * (size_t)(CH + ZD));
fpre.args[4] = mtl::Arg::B(YF, 4 * (size_t)(CH + ZD + 48));
fpost.args[1] = mtl::Arg::B(YF, 4 * (size_t)CH);
std::memset(qn.ptr, 0, rqn.size()); std::memset(vc.ptr, 0, rvc.size()); std::memset(gb.ptr, 0, rgb.size());
std::memset(yh.ptr, 0, ryh.size()); std::memset(h1.ptr, 0, rh.size());
cx.run({fpre, dscan, fpost});
std::printf("fused-stride gdn_pre/gdn_post vs separate: qn %s, vc %s, gate/beta %s, history %s, fp16 y %s\n",
std::memcmp(qn.ptr, rqn.data(), rqn.size()) == 0 ? "equal" : "DIFFER", std::memcmp(vc.ptr, rvc.data(), rvc.size()) == 0 ? "equal" : "DIFFER",
std::memcmp(gb.ptr, rgb.data(), rgb.size()) == 0 ? "equal" : "DIFFER", std::memcmp(h1.ptr, rh.data(), rh.size()) == 0 ? "equal" : "DIFFER",
std::memcmp(yh.ptr, ryh.data(), ryh.size()) == 0 ? "equal" : "DIFFER");
tF = timeit({fpre}) + timeit({fpost});
std::printf("fused-stride pre + post %.3f ms (separate %.3f)\n", tF, tpre + tpost);
}
const double tout = timeit({gemm_d(ygh, Wh[4], Y[4], T, N, ZD, 64, 64)});
double dqall = 0;
for (int i = 0; i < 5; ++i) dqall += timeit({dq_d(i)});
std::printf("== layer in engine order (GPU ms): dq %.3f + input GEMMs %.3f + pre/scan/post %.3f + out GEMM %.3f = %.3f (sum of parts)\n",
dqall, sep, tpre + tscan + tpost, tout, dqall + sep + tpre + tscan + tpost + tout);
{ // the layer as the engine now runs it on the M5 family: fused input GEMM, fused-stride pre/post, tiled out GEMM
double tout_best = 1e9;
for (auto [tm, tn] : std::vector<std::pair<int, int>>{{64, 64}, {128, 64}, {64, 128}})
tout_best = std::min(tout_best, timeit({gemm_d(ygh, Wh[4], Y[4], T, N, ZD, tm, tn)}));
const double base_l = dqall + sep + tpre + tscan + tpost + tout;
const double new_l = dqall + best_f + tF + tscan + tout_best;
std::printf("== layer: before %.3f ms, after (fused input GEMM %.3f, fused-stride pre+post %.3f, scan %.3f, out GEMM %.3f) %.3f ms = x%.3f\n",
base_l, best_f, tF, tscan, tout_best, new_l, base_l / new_l);
}
return 0;
}
+36
View File
@@ -0,0 +1,36 @@
// metal/tests/gdn_tprobe.cpp - times kernels/gdn_tprobe.metal: the five MPP matmul2d products of a GDN chunk step, NC chunk steps a
// threadgroup, 192 threadgroups (h_v * 4). Compare with gdn_scanr_4's stage (chunks of 32 -> T = 32 * NC).
// gdn_tprobe [NC]
#include "mtl_rt.h"
#include "ref_quant.h"
#include <algorithm>
#include <cstdio>
#include <cstdlib>
#include <vector>
int main(int argc, char** argv) {
const int NC = argc > 1 ? std::atoi(argv[1]) : 64;
mtl::Context cx;
const int lib = cx.compile({"gdn_tprobe.metal"}, "#define NO_GGML_COMMON 1\n");
std::printf("device %s, NC %d (T %d)\n", cx.device_name().c_str(), NC, NC * 32);
const size_t NB = (size_t)48 * NC;
mtl::Buffer out = cx.alloc(4 * 192 * 4096), W = cx.alloc(2 * NB * 32 * 128), Q = cx.alloc(2 * NB * 32 * 128),
K = cx.alloc(2 * NB * 32 * 128), M = cx.alloc(2 * NB * 32 * 32);
ref::Rng r(3);
for (auto* b : {&W, &Q, &K}) for (size_t i = 0; i < NB * 32 * 128; ++i) b->as<uint16_t>()[i] = ref::f32_to_f16(r.sym() * 0.1f);
for (size_t i = 0; i < NB * 32 * 32; ++i) M.as<uint16_t>()[i] = ref::f32_to_f16(r.sym() * 0.1f);
for (int pr : {0, 1}) {
mtl::Pipeline p = cx.pipeline(lib, "gdn_tprobe", pr ? mtl::Constants().i(0, pr) : mtl::Constants());
mtl::Dispatch d; d.p = &p;
d.args = {mtl::Arg::B(out), mtl::Arg::B(W), mtl::Arg::B(Q), mtl::Arg::B(K), mtl::Arg::B(M), mtl::Arg::V(NC)};
d.grid[0] = 192; d.tg[0] = 128;
cx.warm({d}, 0.3);
std::vector<double> t;
for (int i = 0; i < 7; ++i) t.push_back(cx.run({d}) * 1e3);
std::sort(t.begin(), t.end());
std::printf("probe %d (%s): %.3f ms for %d chunk steps x 192 threadgroups (max threads %d, tg mem %d)\n", pr,
pr ? "SIMT parts only" : "with the five products", t[3], NC, p.max_threads, p.static_tg_mem);
}
return 0;
}
+4 -4
View File
@@ -37,7 +37,7 @@ Comments are for this document only; actual feed files must be strict JSON.
"generated_at": "2026-10-20T10:00:00Z", "expires_at": "2027-10-20T10:00:00Z", // expires_at may be null
"app": { // exactly Tauri's static updater JSON, plus sha256/size per platform
"version": "0.2.5", "notes": "...", "pub_date": "2026-10-20T10:00:00Z",
"platforms": {"windows-x86_64": {"url": "app/Strata-0.2.8-windows-x64-setup.exe", "signature": "<minisign, base64>", "sha256": "...", "size": 71000000}}
"platforms": {"windows-x86_64": {"url": "app/Strata-0.2.9-windows-x64-setup.exe", "signature": "<minisign, base64>", "sha256": "...", "size": 71000000}}
},
"control": {"version": "0.1.50", "packages": [{"platform": "linux-x86_64", "format": "tar.gz", "url": "control/...tar.gz",
"size": 0, "sha256": "...", "installed_bytes": 0, "entry": "strata.py"}]},
@@ -131,7 +131,7 @@ All signing happens on the release PC, on the finished files.
**What is built (per OS)**
* *The installer*, one per OS (0.2.5). The Windows and Linux ones have no engine inside: built with `--config src-tauri/tauri.update.conf.json`, which drops `engine-bundle`. The Mac dmg keeps its small Metal engine (one Mac variant).
Windows: `Strata-0.2.8-windows-x64-setup.exe`; Linux: `Strata-0.2.8-linux-x86_64.AppImage`; Mac: `Strata-0.2.8-macos-arm64.dmg` for people and
Windows: `Strata-0.2.9-windows-x64-setup.exe`; Linux: `Strata-0.2.9-linux-x86_64.AppImage`; Mac: `Strata-0.2.9-macos-arm64.dmg` for people and
`Strata.app.tar.gz` as the updater archive (the updater cannot install from a dmg). The Windows and Linux files are both the first download and the
file `latest.json` names; the first start of the app downloads the engine for the card (the first-run card), or installs a file the person already has.
Why: an app update must not carry an engine, and the right engine depends on the card. Engines come per card from the signed feed
@@ -149,8 +149,8 @@ All signing happens on the release PC, on the finished files.
# Linux: tauri build --bundles appimage (no engine inside) is the AppImage
# on the release PC, per updater artifact (Windows setup .exe, Linux .AppImage, Mac Strata.app.tar.gz):
npx tauri signer sign -f <path to tauri-updater.key> <artifact> # writes <artifact>.sig
python packaging/feed_tool.py set-app-platform --app-json RELEASE/tauri-latest.json --target windows-x86_64 --version 0.2.5 --archive Strata-0.2.8-windows-x64-setup.exe --sig Strata-0.2.8-windows-x64-setup.exe.sig --notes "..."
python packaging/feed_tool.py set-app-platform --app-json RELEASE/tauri-latest.json --target linux-x86_64 --version 0.2.5 --archive Strata-0.2.8-linux-x86_64.AppImage --sig Strata-0.2.8-linux-x86_64.AppImage.sig
python packaging/feed_tool.py set-app-platform --app-json RELEASE/tauri-latest.json --target windows-x86_64 --version 0.2.5 --archive Strata-0.2.9-windows-x64-setup.exe --sig Strata-0.2.9-windows-x64-setup.exe.sig --notes "..."
python packaging/feed_tool.py set-app-platform --app-json RELEASE/tauri-latest.json --target linux-x86_64 --version 0.2.5 --archive Strata-0.2.9-linux-x86_64.AppImage --sig Strata-0.2.9-linux-x86_64.AppImage.sig
python packaging/feed_tool.py set-app-platform --app-json RELEASE/tauri-latest.json --target darwin-aarch64 --version 0.2.5 --archive Strata.app.tar.gz --sig Strata.app.tar.gz.sig
python packaging/feed_tool.py build-feed --out RELEASE --sequence N --key <feed-key.json> --engine RELEASE/engine --control RELEASE/control --app-json RELEASE/tauri-latest.json [--dsv41 RELEASE/dsv41]
python packaging/feed_tool.py verify-feed RELEASE --pub <feed-key.pub.json>
+2 -2
View File
@@ -9,7 +9,7 @@
# SRC=<this checkout> the Strata source (default: the checkout this script is in)
# LLAMA=<llama.cpp checkout> at the pinned commit (setup.py LLAMA_CPP_COMMIT); mtmd and ggml come from it
# HP=<ROCm devel root> CORE=<ROCm core root> LIBW=<ROCm libraries root> (TheRock: _rocm_sdk_devel, _rocm_sdk_core, _rocm_sdk_libraries_*)
# OUT=<work folder> PKGVER=0.1.54 COMMIT=<source commit for the build id> JOBS=16
# OUT=<work folder> PKGVER=0.1.55 COMMIT=<source commit for the build id> JOBS=16
# VISION_ONLY=1 build only the two readers (no engine, no package)
# DSV41=1|0 also build strata-dsv41 (the DeepSeek-V4.1 engine, tools/dsv41/build_hip_linux.sh's flags) and ship it beside
# strata: default 1 for the rx9070 variant (gfx1200 gfx1201; built and run on an R9700), 0 for the others
@@ -26,7 +26,7 @@ HP=${HP:?HP=<ROCm devel root>}
CORE=${CORE:-$HP}
LIBW=${LIBW:-$HP}
OUT=${OUT:-$PWD/work-$V}
PKGVER=${PKGVER:-0.1.54}
PKGVER=${PKGVER:-0.1.55}
COMMIT=${COMMIT:-unknown}
JOBS=${JOBS:-16}
case "$V" in
+2 -2
View File
@@ -1,7 +1,7 @@
#!/bin/bash
# Build the DeepSeek-V4.1-Flash engine component for Apple Silicon (strata-dsv41, backend metal).
#
# bash packaging/mac/build_dsv41_engine.sh [--version 0.1.54] [--build DIR] [--out DIR] [--build-id ID]
# bash packaging/mac/build_dsv41_engine.sh [--version 0.1.55] [--build DIR] [--out DIR] [--build-id ID]
#
# Run it in the source tree on the Mac. It configures and builds the `strata_ds4` target (metal/CMakeLists.txt) when the binary is
# missing, stages it as `strata-dsv41` beside the kernel sources the engine compiles at start-up, packages them with
@@ -18,7 +18,7 @@
# <exe>/metal/ggml-common.h
set -u
ROOT=$(cd "$(dirname "$0")/../.." && pwd)
VERSION=0.1.54
VERSION=0.1.55
BUILD="$ROOT/build-metal"
OUT="$ROOT/pkg/engine"
BUILD_ID=""
+40 -11
View File
@@ -1143,6 +1143,7 @@ class OutputParser:
self.stray_think = 0 # ... and the strays of that kind dropped from the answer
self.refused = 0 # declared calls kept as reasoning text (quoted, or the turn was cut)
self.fence, self.line, self.ticks = "", "", 0
self._prev_blank = self._indented = False # #4703: the line before was blank / this is inside an indented (4-space) code block
# recover (on in the server since 0.2.8, off for a bare OutputParser; the config's "tool_call_recovery": false turns it off): a declared tool's call written next to the template's
# form is the call - `<parameter=NAME>` as the opener, JSON inside <tool_call>, a bare <function=NAME> at the
# start of a line outside code, or a second call in the same <tool_call> (which otherwise merges into one).
@@ -1159,10 +1160,10 @@ class OutputParser:
def _ok_at(self, p: int) -> bool:
"""self.buf[p] would open a call: at the start of a line, outside a code fence and inline code."""
snap = (self.fence, self.line, self.ticks)
snap = self._snap()
self._track(self.buf[:p])
ok = self._opener_ok()
self.fence, self.line, self.ticks = snap
self._restore(snap)
return ok
def _callable(self, name: str) -> bool:
@@ -1270,15 +1271,29 @@ class OutputParser:
self._line_parts.clear()
self._line_blocks = None
s = line.lstrip()
was_fenced = bool(self.fence)
if self.fence:
if s.startswith(self.fence * 3):
# #4702: a fence is closed only by a line of at least as many of the same character as opened it (CommonMark), so a
# ``` example inside a ```` file stays inside it
run = len(s) - len(s.lstrip(self.fence[0]))
if run >= len(self.fence):
self.fence = ""
elif s[:3] in ("```", "~~~") and (s[0] * 3) not in s[3:]:
self.fence = s[0]
self.fence = s[0] * (len(s) - len(s.lstrip(s[0])))
elif not s:
self.ticks = 0
else:
self.ticks += line.count("`")
# #4703: an indented (4 spaces or a tab) code block starts after a blank line and goes on until a line that is not indented
if was_fenced or self.fence:
self._prev_blank = self._indented = False
elif not s:
self._prev_blank = True
elif line.startswith((" ", "\t")):
self._indented = self._prev_blank or self._indented
self._prev_blank = False
else:
self._prev_blank = self._indented = False
else:
if part:
self._line_parts.append(part)
@@ -1287,11 +1302,25 @@ class OutputParser:
def _opener_ok(self) -> bool:
"""The reasoning text so far puts a `<tool_call>` at the start of a line, outside a code fence and outside
inline code."""
return not self.fence and not self.line.strip() and self.ticks % 2 == 0
return not self.fence and not self.line.strip() and self.ticks % 2 == 0 and not self._in_indented_block()
def _in_indented_block(self) -> bool:
"""#4703: the current line is part of an indented code block: four spaces or a tab, after a blank line or another such line."""
return (self._prev_blank or self._indented) and self.line.startswith((" ", "\t"))
def _snap(self) -> tuple:
return (self.fence, self.line, self.ticks, self._prev_blank, self._indented)
def _restore(self, snap: tuple) -> None:
self.fence, self.line, self.ticks, self._prev_blank, self._indented = snap
def _reset_track(self) -> None:
self.fence, self.line, self.ticks = "", "", 0
self._prev_blank = self._indented = False
def _in_code(self) -> bool:
"""The text so far leaves the next character inside a code fence or inline code."""
return bool(self.fence) or (self.ticks + self.line.count("`")) % 2 == 1
"""The text so far leaves the next character inside a code fence, inline code or an indented code block."""
return bool(self.fence) or (self.ticks + self.line.count("`")) % 2 == 1 or self._in_indented_block()
def _quoted_think_end(self, i: int) -> bool | None:
"""#537: the `</think>` at self.buf[i] is the model quoting the tag while it thinks, not the end of the
@@ -1576,7 +1605,7 @@ class OutputParser:
out.append(Event("reasoning", self._track(self.buf[:i])))
self.buf = self.buf[i + len(THINK_END):]
self.state, self.lead, self.after_think = "content", True, True
self.fence, self.line, self.ticks = "", "", 0 # the answer's own text starts here
self._reset_track() # the answer's own text starts here
elif self.state == "content":
if self.lead: # newlines right after </think> or a call
stripped = self.buf.lstrip("\n")
@@ -1594,7 +1623,7 @@ class OutputParser:
if stray:
out.append(Event("content", self._track(self.buf[:stray])))
self.buf = self.buf[stray + len(THINK_END):]
self.fence, self.line, self.ticks = "", "", 0
self._reset_track()
self.lead = True
self.stray_think += 1
continue
@@ -1630,10 +1659,10 @@ class OutputParser:
return out
# #1058: an opener inside a code fence or inline code is text (a quoted example), never a call. The
# text before it decides, so streamed and whole outputs agree. Mid-sentence openers still count.
snap = (self.fence, self.line, self.ticks)
snap = self._snap()
self._track(self.buf[:i])
in_code = self._in_code()
self.fence, self.line, self.ticks = snap
self._restore(snap)
if in_code:
out.append(Event("content", self._track(self.buf[:i + len(CALL_START)])))
self.buf = self.buf[i + len(CALL_START):]
+162
View File
@@ -18,6 +18,18 @@ conversation's next turn therefore goes back to its replica, until that replica
reading the prompt again is cheaper than waiting. What each replica holds is tracked as hashes of 64-token blocks of
the prompts and answers it served (the engine itself checks the real prefix, as it always does).
Host RAM. Every engine keeps its own copy of the experts in RAM (about 47 GiB for Qwen3.8-Flash-Next IQ3_S), so two
replicas need two copies and four need four. "replica_shared_arena": true (or STRATA_REPLICA_SHARED_ARENA=1; opt-in)
backs the experts' arena of all replicas with ONE shared memory object (the engine's --shared-expert-arena, MAP_SHARED):
the experts are in RAM once, whatever the number of replicas. On Linux the object is a memfd the server creates
(os.memfd_create): it is not a file on a mount, so the size of /dev/shm does not limit it; the engines open it as
/proc/<server pid>/fd/<n>. On Windows it is a named section (CreateFileMapping) the engines open by name. Without memfd (an old kernel, a sandbox that refuses it) a file in /dev/shm is used instead.
The pack hash in the header refuses another model; the first engine to start reads the experts into it and the others
wait for it and skip the read (a state word in the header: nobody / a pid reading / filled; an engine whose reader died
takes the read over); the server closes the memfd (or unlinks the file) when it ends - an engine that still maps it
keeps its pages, the kernel frees them with the last mapping - and a /dev/shm file whose server is gone is removed at
the next start. See shared_arena_plan.
CPU cores. On Linux the replicas get disjoint physical cores (serve/cpu_affinity.py, from PR #1436 by agorevski), so
the host threads of the engines (the expert workers) do not compete. "replica_cpus": false turns it off.
"""
@@ -26,6 +38,7 @@ from __future__ import annotations
import collections
import copy
import os
import sys
import threading
import time
@@ -81,6 +94,154 @@ def _cards(g) -> list[int]:
raise ReplicaConfigError(f"cards must be numbers, not {g!r}") from None
ARENA_PREFIX = "strata-replicas-"
_MEMFDS: dict = {} # the memfds this server created: path (/proc/<pid>/fd/<n>) -> fd
ARENA_MIN_RAM = 8 << 30 # a memfd arena: at least this much free RAM to try (the engine reserves the real size)
ARENA_MIN_FREE = 8 << 30 # /dev/shm must have at least this much free to try (the engine reserves the real size)
ARENA_INCOMPATIBLE = ("--mmap-experts", "--resident-experts", "--shared-expert-arena", "--resident-budget-gib")
def _pid_alive(pid: int) -> bool:
try:
os.kill(pid, 0)
except ProcessLookupError:
return False
except PermissionError:
return True
return True
def sweep_stale_arenas(shm: str = "/dev/shm", uid: int | None = None) -> list[str]:
"""Remove the arena files of servers that no longer run (a killed server cannot unlink its own). A file of a living
server, or of another user, is never touched; a mapping an engine still holds stays valid either way."""
if uid is None:
uid = os.getuid() if hasattr(os, "getuid") else 0
gone = []
try:
names = os.listdir(shm)
except OSError:
return gone
head = f"{ARENA_PREFIX}{uid}-"
for n in names:
if not (n.startswith(head) and n.endswith(".bin")):
continue
try:
pid = int(n[len(head):-4])
except ValueError:
continue
if _pid_alive(pid):
continue
try:
os.unlink(os.path.join(shm, n))
gone.append(n)
except OSError:
pass
return gone
def _free(path: str) -> int:
try:
st = os.statvfs(path)
return st.f_bavail * st.f_frsize
except OSError:
return 0
def _mem_available() -> int:
try:
with open("/proc/meminfo") as f:
for line in f:
if line.startswith("MemAvailable:"):
return int(line.split()[1]) * 1024
except (OSError, ValueError, IndexError):
pass
return 0
def shared_arena_plan(cfg: dict, n_replicas: int, *, shm: str = "/dev/shm", platform: str | None = None,
environ=None, free_bytes=None, use_memfd: bool | None = None) -> tuple[str | None, str]:
"""(the file for --shared-expert-arena or None, a line for the log). Opt-in: "replica_shared_arena": true (or
STRATA_REPLICA_SHARED_ARENA=1). Only on Linux (the engine's MAP_SHARED backing), for Qwen's arena (not DeepSeek,
not the mapped or budgeted RAM modes, which have no arena), and with room in /dev/shm. The engine checks the real
size and reserves it before it loads (a tmpfs that is too small is an error at start, not a bus error later)."""
environ = os.environ if environ is None else environ
platform = sys.platform if platform is None else platform
want = cfg.get("replica_shared_arena")
if want is None:
want = str(environ.get("STRATA_REPLICA_SHARED_ARENA", "")).strip().lower() in ("1", "true", "on", "yes")
if not want or n_replicas < 2:
return None, ""
if platform.startswith("win"):
# a named pagefile-backed section the engines open by name (src/core/pinned.cu); it lives while any engine holds it
name = f"Local{chr(92)}strata-replicas-{os.getpid()}"
return f"win:{name}", (f"one shared expert arena for {n_replicas} replicas (a Windows section, {name}): "
"the experts are in RAM once")
if not platform.startswith("linux"):
return None, "shared expert arena: Linux and Windows only; each replica keeps its own copy of the experts in RAM"
args = [str(a) for a in cfg.get("args") or []]
if isinstance(cfg.get("replicas"), list):
for e in cfg["replicas"]:
if isinstance(e, dict):
args += [str(a) for a in e.get("args") or []]
if cfg.get("arch") == "deepseek41" or any(a in args for a in ARENA_INCOMPATIBLE):
return None, "shared expert arena: this model/config has no private expert arena to share; each replica as before"
if use_memfd is None:
use_memfd = hasattr(os, "memfd_create") and str(cfg.get("replica_shared_arena")).lower() != "devshm"
if use_memfd:
avail = free_bytes if free_bytes is not None else _mem_available()
if avail < ARENA_MIN_RAM:
return None, (f"shared expert arena: only {avail / 2**30:.1f} GiB of RAM is free; each replica keeps its own "
"copy of the experts in RAM")
try:
fd = os.memfd_create("strata-replicas-arena", os.MFD_CLOEXEC)
except OSError:
fd = -1
if fd >= 0:
path = f"/proc/{os.getpid()}/fd/{fd}"
_MEMFDS[path] = fd
warn = ""
try: # the arena once plus about 7 GB of private RAM per engine (measured); a warning, never a refusal
from strata_control import replica_profile as _rp
q = _rp.quant_of(cfg)
if q:
need = _rp.ARENA_GB[q] + n_replicas * _rp.PER_REPLICA_GB
if avail / 2**30 < need + 4:
warn = (f" WARNING: about {need:.0f} GB are needed for {n_replicas} engines and {avail / 2**30:.0f} GB "
"are free: the last engine may not fit (fewer replicas, or the layer split, is safer)")
except Exception: # noqa: BLE001
pass
return path, (f"one shared expert arena for {n_replicas} replicas (memory object of the server, not limited by "
f"the size of /dev/shm): the experts are in RAM once.{warn}")
if not (os.path.isdir(shm) and os.access(shm, os.W_OK)):
return None, f"shared expert arena: {shm} is not usable; each replica keeps its own copy of the experts in RAM"
free = free_bytes if free_bytes is not None else _free(shm)
if free < ARENA_MIN_FREE:
return None, (f"shared expert arena: {shm} has only {free / 2**30:.1f} GiB free (a tmpfs is half the RAM by "
"default); each replica keeps its own copy of the experts in RAM")
sweep_stale_arenas(shm)
uid = os.getuid() if hasattr(os, "getuid") else 0
path = os.path.join(shm, f"{ARENA_PREFIX}{uid}-{os.getpid()}.bin")
return path, f"one shared expert arena for {n_replicas} replicas ({path}): the experts are in RAM once"
def remove_arena(path: str | None) -> None:
"""Close the memfd / unlink the arena file (the server's end, or a start that fell back): engines that map it keep
their pages."""
if not path or path.startswith("win:"): # (a Windows section goes with its last handle)
return
fd = _MEMFDS.pop(path, None)
if fd is not None:
try:
os.close(fd)
except OSError:
pass
return
try:
os.unlink(path)
except OSError:
pass
def _suffixed(path, i: int):
if not isinstance(path, str) or not path:
return path
@@ -337,6 +498,7 @@ class ReplicaEngine:
e.close()
except BaseException as x: # noqa: BLE001
err = err or x
remove_arena(self.__dict__.get("arena_file")) # (an engine that still maps it keeps its pages)
if err:
raise err
+263 -9
View File
@@ -327,6 +327,7 @@ def _timeout_env(name: str, default: float) -> float | None:
# still working is never ended by this, only reported once (the recommendation: engine_silence_s, or this variable).
# STRATA_ENGINE_STALL_S overrides it (0 = off). Needs psutil (setup installs it); without it nothing is ended.
ENGINE_STALL_S = _timeout_env("STRATA_ENGINE_STALL_S", 90.0)
PAR_COUNTERS = os.environ.get("STRATA_PAR_COUNTERS") == "1" # research counters (queue wait for a slot), shown in /v1/status
# --batch: new requests take the control lines (one prompt read at a time) short prompts first: a prompt of at most
# STRATA_ADMIT_SHORT_FIRST tokens goes before longer ones (opt-in; unset or 0: whoever gets the lock first), and a
# longer one that waited STRATA_ADMIT_MAX_WAIT_S (default 30) counts as short. A chat question then waits for the one
@@ -336,6 +337,19 @@ try:
except ValueError:
ADMIT_SHORT_FIRST = 0
ADMIT_MAX_WAIT_S = _timeout_env("STRATA_ADMIT_MAX_WAIT_S", 30.0) or 0.0
# STRATA_COALESCE_PREFILL=N (research, PAR-H; needs the engine's BCO / BCOK lines): a request that takes the control lines
# for its admission also sends the prompts of up to N-1 requests still waiting for them (BCO <slot> <ids>, into free slots
# it reserves for them); the engine reads all the prompts in ONE layer-major pass, so the experts a layer streams are
# read once for all of them, and each waiting request then finds its prompt in its slot. 0 / unset: off.
COALESCE_PREFILL = max(0, int(os.environ.get("STRATA_COALESCE_PREFILL") or 0))
COALESCE_TOKENS = max(256, int(os.environ.get("STRATA_COALESCE_TOKENS") or 16384)) # prompt tokens read together at most
COALESCE_WAIT_MS = max(0, int(os.environ.get("STRATA_COALESCE_WAIT_MS") or 0)) # a burst's later requests register within this long
# Adaptive: a read is shared (and the leader waits for its followers) only when this engine has seen at least
# COALESCE_MIN_REQS requests arrive within COALESCE_BURST_MS (the leader's own included). Fewer than that - one or two
# requests per engine, as 8 requests on 4 replicas - read plainly, with no wait: nothing to share, and the wait and the
# longer pass only cost (measured 4 x 1 replicas at 8 requests: 168 against 196 tok/s with an unconditional wait).
COALESCE_MIN_REQS = max(2, int(os.environ.get("STRATA_COALESCE_MIN_REQS") or 3))
COALESCE_BURST_MS = max(10, int(os.environ.get("STRATA_COALESCE_BURST_MS") or 250))
STALL_CPU_EPS_S = 0.5 # CPU seconds the engine may use in the whole stall window and still count as frozen
STALL_IO_EPS_B = 1 << 20 # bytes read or written in that window
@@ -1193,7 +1207,9 @@ class StrataEngine:
self.slot_cv = threading.Condition()
self.waiting = 0 # requests waiting for the control lines (ctl)
self.admitting = 0 # #1603: ... and those that gave the lines back to wait for a slot
self.par_counters = {"queued": 0, "queue_wait_s": 0.0}
self.wait_lens: list[list[int]] = [] # ... their prompt lengths (a long read gives way to short ones)
self.arrivals = collections.deque(maxlen=64) # monotonic times of the last requests that came for the control lines (STRATA_COALESCE_*)
self.ctl_epoch = 0 # how often the control lines were taken
self.ctl = threading.Lock() # one admission or solo request on the control lines at a time
self.gen = self.__dict__.get("gen", 0) + 1 # which engine process this is (a request notes its own)
@@ -1575,6 +1591,8 @@ class StrataEngine:
f = line.split()
self._ctl_result = ("badm", len(f) >= 3 and f[2] == "1")
return
elif line.startswith("BCOK "): # STRATA_COALESCE_PREFILL: <slot> <tokens>: the slot holds a waiting prompt
pass
elif line.startswith("YIELDED "): # the read gave way (BYIELD): <slot> <tokens read>
f = line.split()
if len(f) >= 3 and f[1].lstrip("-").isdigit() and f[2].isdigit():
@@ -1726,14 +1744,15 @@ class StrataEngine:
YIELDS_MAX = 2 # #656: how often one request's prompt read gives way to a shorter waiting one
def _take_control(self, cancel, plen: int, after_epoch: int | None = None, new: bool = False):
def _take_control(self, cancel, plen: int, after_epoch: int | None = None, new: bool = False, prompt=None):
"""Waits for the control lines (one prompt read at a time), yielding None heartbeats; False when cancelled.
`after_epoch`: a request whose read gave way lets the requests waiting then go first.
`new`: the request's first admission (STRATA_ADMIT_SHORT_FIRST orders only these)."""
entry = [plen, time.monotonic(), new] # (length, arrival, ordered: STRATA_ADMIT_SHORT_FIRST)
entry = [plen, time.monotonic(), new, prompt, cancel] # (length, arrival, ordered: STRATA_ADMIT_SHORT_FIRST; the prompt and request, for STRATA_COALESCE_PREFILL)
with self.slot_cv:
self.waiting += 1
self.wait_lens.append(entry)
self.arrivals.append(entry[1])
beat = time.monotonic()
try:
while True:
@@ -1784,6 +1803,61 @@ class StrataEngine:
self.slot_cv.notify_all()
return True
def _release_preslot(self, cancel):
"""STRATA_COALESCE_PREFILL: the request that was given a slot ahead of its turn left before using it."""
slot = getattr(cancel, "_strata_preslot", None)
if slot is None:
return
cancel._strata_preslot = None
with self.slot_cv:
self.slot_busy[slot] = False
self.slot_claims.discard(slot)
self.slot_cv.notify_all()
def _coalesce(self, cancel, lead_prompt):
"""STRATA_COALESCE_PREFILL (the caller holds the control lines and sends its BGEN right after): reserve free slots
for requests still waiting for the control lines and send their prompts as BCO lines; the engine reads them in the
same layer-major pass as the BGEN's prompt. Each follower finds its slot in cancel._strata_preslot."""
picked = []
total = len(lead_prompt)
def seen() -> int:
now = time.monotonic()
with self.slot_cv:
return max(sum(1 for t in self.arrivals if now - t <= COALESCE_BURST_MS / 1000.0), 1 + len(self.wait_lens))
recent = seen()
if recent < COALESCE_MIN_REQS and COALESCE_WAIT_MS > 0:
time.sleep(0.010) # (the first request of a burst holds the lines before its neighbours have arrived)
recent = seen()
if recent < COALESCE_MIN_REQS:
return # one or two requests on this engine: a plain read, at most 10 ms lost
if COALESCE_WAIT_MS > 0:
time.sleep(COALESCE_WAIT_MS / 1000.0) # the requests of a burst reach the server a few ms apart
with self.slot_cv:
for e in list(self.wait_lens):
if len(picked) >= COALESCE_PREFILL - 1:
break
prompt = e[3] if len(e) > 4 else None
rq = e[4] if len(e) > 4 else None
if prompt is None or rq is None or rq is cancel or getattr(rq, "_strata_preslot", None) is not None:
continue
if getattr(rq, "_strata_co", False) or len(prompt) < 32 or total + len(prompt) > COALESCE_TOKENS:
continue
slot = self.pick_slot(prompt)
if slot is None:
break
self.slot_busy[slot] = True
self.slot_claims.add(slot)
self.slot_held[slot] = []
rq._strata_preslot = slot
rq._strata_co = True
total += len(prompt)
picked.append((slot, prompt))
for slot, prompt in picked:
self._send(f"BCO {slot} {','.join(str(int(t)) for t in prompt)}")
if picked:
print(f"[strata] coalesce: {len(picked)} waiting prompt(s) ({total - len(lead_prompt)} tokens) read with this "
f"request's {len(lead_prompt)}", flush=True)
def _admit_next(self):
"""STRATA_ADMIT_SHORT_FIRST=<tokens> (the caller holds slot_cv): the new request that takes the control lines
next - one whose prompt is at most that long (it reads little) before a longer one, in arrival order within
@@ -1839,12 +1913,17 @@ class StrataEngine:
prompt, left = list(ids), int(max_new)
reused0 = None # the first read's reused tokens: a continued leg's count includes this request's own output
carry = {} # #1869: the figures of the legs that ended in a hand-back to the solo path (the final DONE adds to them)
ok = yield from self._take_control(cancel, len(prompt), new=True)
ok = yield from self._take_control(cancel, len(prompt), new=True,
prompt=prompt if (COALESCE_PREFILL > 1 and not embeddings) else None)
if not ok:
self._release_preslot(cancel)
return
holding, born = True, self.gen # (the engine it now has the control lines of)
btrace("ctl acquired")
slot, gen0, reserved = None, None, None
pre = getattr(cancel, "_strata_preslot", None)
if pre is not None: # STRATA_COALESCE_PREFILL: a request before this one read the prompt into this slot
slot, cancel._strata_preslot = pre, None
phase = "none" # solo -> (admit -> slot) ; "done" once the engine has finished with this request
stop_sent = False
yields, solo_again, refused = 0, 0, 0
@@ -1941,6 +2020,9 @@ class StrataEngine:
if admit_wait:
admit_wait = False
self.admitting = max(0, self.admitting - 1)
if PAR_COUNTERS: # STRATA_PAR_COUNTERS=1: time spent waiting for a free slot
self.par_counters["queued"] += 1
self.par_counters["queue_wait_s"] += time.monotonic() - admit_since
break
if not admit_wait: # #1603: counted while it waits for a slot (it holds no
admit_wait, admit_since = True, time.monotonic() # control lines then)
@@ -1993,6 +2075,8 @@ class StrataEngine:
raise QueueWaitExpired("the inference queue wait deadline expired; retry later")
self.slot_live[slot] = live
self._last_done = None # (#879: this admission's DONE decides below)
if COALESCE_PREFILL > 1 and not embeddings:
self._coalesce(cancel, prompt)
self._send(f"{head} {','.join(str(int(t)) for t in prompt)}")
if deadline is not None:
cancel._strata_queue_deadline = None
@@ -3583,6 +3667,83 @@ def deepseek_slot_env(env: dict, cfg: dict) -> None:
env["STRATA_DS4_SLOT_DIR"] = str(Path(log).parent / "ds4-slots")
# 0.2.9: the exact parallel-request stack, on whenever the engine runs batch slots (--batch / "parallel" 2 or more). Every item
# was qualified bit-exact against the plain slots (QUAL-PAR: greedy identity at c=1, first-token KL 0): the checkpoint pool and the
# slot copies keep a request's running state without a stall, and the BATCH_* / QSA_* switches run all slots of a window in one
# launch. Together +16% sustained throughput at c=8 on two A6000 and +40..65% on one big card, no solo cost. An explicit
# environment variable (or the config's "env") still wins: =0 turns one off.
PARALLEL_STACK_ENV = {
"STRATA_CKPT_POOL": "1", "STRATA_CKPT_POOL_READY": "4", "STRATA_SLOT_CKPT_MOVE": "1", "STRATA_SLOT_COPY_DIRECT": "1",
"STRATA_SLOT_COPY_BATCHSYNC": "1", "STRATA_BATCH_GDN_GROUPS": "1", "STRATA_BATCH_ATTN_ROWS": "1",
"STRATA_BATCH_SELF_COMMIT": "1", "STRATA_QSA_SLOTS": "1",
}
# Only where the stack was run alone: Linux CUDA, all nine. HIP got no measurement without replicas (its only run was inside the
# replica config), Windows none at all (its host-arena / pinned-memory path differs): nothing there.
def parallel_slots(cfg: dict) -> int:
"""How many batch slots the config asks for (0 = none): the args' own --batch/--slots, else "parallel"."""
args = list(cfg.get("args") or [])
for k in ("--batch", "--slots"):
if k in args:
try:
return int(args[args.index(k) + 1])
except (IndexError, ValueError):
return 0
n = cfg.get("parallel")
return n if isinstance(n, int) and not isinstance(n, bool) and n > 1 else 0
def parallel_stack_platform() -> bool:
"""Linux only: the stack was run on Linux CUDA; Windows (own host-arena path) and macOS were not."""
return sys.platform.startswith("linux")
def parallel_stack_env(env: dict, cfg: dict) -> None:
"""Switch the exact parallel stack on (setdefault: a value the user or the config set stays) when the config runs batch
slots on a CUDA or HIP Qwen engine. Other engines (DeepSeek, SYCL, Metal) do not read these."""
if parallel_slots(cfg) < 2 or (cfg.get("arch") or "") == "deepseek41" or cfg.get("backend") in ("sycl", "metal", "vulkan"):
return
if not parallel_stack_platform():
return
if cfg.get("backend") == "hip":
return
for k, v in PARALLEL_STACK_ENV.items():
env.setdefault(k, v)
# 0.2.9: the qualified GDN prompt speedups (QUAL-GDN, Qwen3.8 IQ3_S). Linux only.
# CUDA: STRATA_GDN_TC=1 (the recurrence scan on the tensor cores) + STRATA_GDN_CONVL2=2 (conv, SiLU and q/k norm in one pass):
# KL mean 0.0019 / max 0.0125, 61-task suite 55 vs 54, prompt +4.4% at 4K, +5.5% at 16K, +5.7% at 32K (l620). The engine
# gates TC at run time (compute capability 8 or more, one SM per value head, shared memory) and falls back to the old
# kernels on anything else, so an older card or a CUDA 12 build simply does not use it.
# AMD gfx12 (RDNA4) only: STRATA_GDN_CHUNKED=1 (the chunked recurrence on the matrix cores): KL mean 0.0025, suite 55 vs 56,
# +3.5% at 4K, +5.5% at 16K (R9700). The same variable also turns the gfx11 path on in the engine and only gfx12 was
# qualified, so it is set only when every card the engine will see is gfx12.
# Other engines (Qwen3.6, DeepSeek), SYCL, Metal, Vulkan, Windows: nothing. An explicit variable (or the config's env) wins.
GDN_STACK_CUDA = {"STRATA_GDN_TC": "1", "STRATA_GDN_CONVL2": "2"}
GDN_STACK_GFX12 = {"STRATA_GDN_CHUNKED": "1"}
def gdn_stack_env(env: dict, cfg: dict, archs: dict | None = None) -> None:
if not parallel_stack_platform(): # Linux only (same rule as the parallel stack)
return
if (cfg.get("arch") or "qwen4exp") != "qwen4exp" or cfg.get("backend") in ("sycl", "metal", "vulkan"):
return # only the model the numbers are from
if cfg.get("backend") != "hip":
for k, v in GDN_STACK_CUDA.items():
env.setdefault(k, v)
return
ar = archs if archs is not None else hip_gpu_archs()
if not ar:
return # cannot tell the card: leave AMD out
want = hip_visible(cfg) or list(ar)
seen = [ar.get(i, "") for i in want]
if seen and all(a.startswith("gfx12") for a in seen):
for k, v in GDN_STACK_GFX12.items():
env.setdefault(k, v)
def child_env(cfg: dict, *, offline: bool = False) -> dict:
"""The engine's environment: the CUDA libraries setup installed (pip's nvidia packages, or the toolkit that
compiled it) first on the library search path."""
@@ -3599,6 +3760,8 @@ def child_env(cfg: dict, *, offline: bool = False) -> dict:
env[str(k)] = str(v)
if (cfg.get("arch") or "") == "deepseek41": # (only this engine reads these; Qwen's environment is untouched)
deepseek_slot_env(env, cfg)
parallel_stack_env(env, cfg)
gdn_stack_env(env, cfg)
# Strict offline reaches children through the environment, because a tool subprocess or a model
# loader is code this server does not own. Applied last so a profile's own `env` cannot remove
# the policy by naming the same variable.
@@ -3715,6 +3878,37 @@ def card_note(cfg: dict, env: dict) -> str | None:
f"{', '.join(names)} - the first runs the first layers and the picture reader, the last runs the head and the draft layer")
def engine_arch_note(cfg: dict, cards: dict[int, int] | None = None, built: dict | None = None) -> str | None:
"""#4662: a compiled engine holds code for the architectures it was built for (BUILD.json "archs"); on a card of another one a long prompt
ends in "unspecified launch failure" (the kernels it needs are a trap there). One plain line when a card this run uses is not covered
(NVIDIA; a build with PTX also covers newer cards). Said, never refused: the engine is started as asked. `cards`: {nvidia-smi number:
compute capability x 10}; `built`: the BUILD.json dict - both read here when not given. None when everything is covered or unknown."""
try:
if cfg.get("backend") == "hip":
return None
if built is None:
exe = cfg.get("exe")
if not isinstance(exe, str) or engine_component.is_reference(exe):
return None
built = json.loads((Path(exe).parent / "BUILD.json").read_text(encoding="utf-8-sig"))
archs = sorted({int(re.sub(r"\D", "", str(a))) for a in built.get("archs") or [] if re.sub(r"\D", "", str(a))})
if not archs:
return None
if cards is None:
r = subprocess.run(["nvidia-smi", "--query-gpu=index,compute_cap", "--format=csv,noheader"], capture_output=True, text=True,
errors="replace", timeout=10)
cards = {int(i): int(round(float(c) * 10)) for i, c in (ln.split(",") for ln in (r.stdout or "").strip().splitlines())}
used = gpu_list(cfg) or sorted(cards)
miss = sorted({cards[i] for i in used if i in cards and cards[i] not in archs and not (built.get("ptx") and cards[i] > max(archs))})
if not miss:
return None
names = ", ".join(f"sm_{m}" for m in miss)
return (f"[strata] this engine was built for {', '.join(f'sm_{a}' for a in archs)}, but a graphics card in use is {names}: long prompts "
"can stop with \"unspecified launch failure\". Update the engine (./update.sh, or `strata update`) so it is built for every card in this PC.")
except Exception:
return None
def vision_card_off_igpu(env: dict, archs: dict[int, str] | None = None) -> int | None:
"""AMD: the picture reader is a separate program that sees the engine's HIP_VISIBLE_DEVICES and runs on the FIRST visible card. With a
layer split that starts on a built-in Radeon (Strix Halo beside an R9700) that is the iGPU: the card that draws the desktop and whose
@@ -4795,6 +4989,8 @@ class Service:
slots = self.engine.slots_view() if hasattr(self.engine, "slots_view") else []
# the requests in flight that are not in a slot: the one alone on the solo path, or waiting their turn
in_slots = sum(1 for x in slots if x["state"] != "idle")
if PAR_COUNTERS:
live["par_counters"] = dict(self.par_counters)
live.update(parallel=par, running=running, slots=slots, outside_slots=max(0, running - in_slots),
waiting=int(getattr(self.engine, "waiting", 0) or 0),
admitting=int(getattr(self.engine, "admitting", 0) or 0))
@@ -5311,8 +5507,8 @@ class Service:
print(f"[strata] thinking capped at {max_new - reserve} of max_tokens {max_new} to leave room for "
f"the answer (budget {budget})", flush=True)
budget = max(1, max_new - reserve)
parser = self.parser_cls(thinking=thinking, tools=tools, stream_tools=True, recover=self.tool_call_recovery,
declared_only=declared_only)
parser = self.parser_cls(thinking=thinking, tools=with_history_tools(tools, sampling), stream_tools=True,
recover=self.tool_call_recovery, declared_only=declared_only)
detok, n, finish = Detokenizer(self.tok), 0, "length"
run_tok, run_len, repeated = None, 0, False # #606: the current run of one repeated token
thinking_n = 0 # tokens written while thinking (Responses' reasoning_tokens)
@@ -8501,9 +8697,14 @@ def start_replicas(cfg: dict, groups: list[list[int]], exe: str, effort_end: lis
flush=True)
engines: list = [None] * n
errors: list = []
arena, arena_note = rp.shared_arena_plan(cfg, n) # opt-in: one copy of the experts in RAM for all replicas
if arena_note:
print(f"[strata] {arena_note}", flush=True)
def one(i: int):
rc = rp.replica_cfg(cfg, i, groups[i])
if arena:
rc["args"] = list(rc["args"]) + ["--shared-expert-arena", arena]
try:
engines[i] = StrataEngine(exe, engine_args(rc) + effort_end, cwd=rc.get("cwd"), log=rc.get("log"),
env=child_env(rc, offline=offline), lazy=lazy, cpus=cpus[i] if cpus else None)
@@ -8520,6 +8721,16 @@ def start_replicas(cfg: dict, groups: list[list[int]], exe: str, effort_end: lis
t.start()
for t in ts:
t.join()
if errors and arena and len(errors) < n:
# a replica that died while it was reading the experts into the shared arena (or any other one that failed) while
# another one is up: the living engine took the read over (the state word in the arena), so start the failed ones
# once more on the same arena instead of giving up the sharing
failed = sorted(i for i, _ in errors)
errors.clear()
print(f"[strata] replica(s) {', '.join(map(str, failed))} did not start; starting again on the shared expert arena",
flush=True)
for i in failed:
one(i)
if errors:
for e in engines:
if e is not None:
@@ -8527,10 +8738,20 @@ def start_replicas(cfg: dict, groups: list[list[int]], exe: str, effort_end: lis
e.close()
except Exception: # noqa: BLE001
pass
rp.remove_arena(arena)
i, e = errors[0]
if arena: # a shared arena that could not be made (no room in /dev/shm, a file of another pack): as before
print(f"[strata] replica {i} did not start with the shared expert arena ({e}); starting every replica with "
"its own copy of the experts instead", flush=True)
return start_replicas({**cfg, "replica_shared_arena": False}, groups, exe, effort_end, lazy, offline=offline)
raise SystemExit(f"[strata] replica {i} did not start: {e}")
load = int(cfg.get("replica_load_tokens") or rp.LOAD_TOKENS)
return rp.ReplicaEngine(engines, load_tokens=load)
eng = rp.ReplicaEngine(engines, load_tokens=load)
if arena:
eng.__dict__["arena_file"] = arena
import atexit
atexit.register(rp.remove_arena, arena) # (a killed server leaves the file: the next start sweeps it)
return eng
def start_split(cfg: dict, spec: dict, exe: str, effort_end: list[str], lazy: bool, *, offline: bool = False):
@@ -8653,10 +8874,16 @@ def with_current_reader(cfg: dict, package: Path) -> dict:
fixed = dict(vision)
for key in ("exe", "cpu_exe"):
old = fixed.get(key)
if isinstance(old, str) and old and not engine_component.is_reference(old) and os.path.isabs(old) and not os.path.exists(old):
if isinstance(old, str) and old and not engine_component.is_reference(old) and os.path.isabs(old):
new = package / old.replace("\\", "/").rsplit("/", 1)[-1]
if new.is_file():
print(f"[strata] the picture reader {old} is not there any more; using {new} from the engine package in use", flush=True)
gone = not os.path.exists(old)
# #4658: the old engine folder is kept after `strata update`, so the old reader still exists; one in a SIBLING engine folder
# (components/engine/<version>-...) is the engine's reader from before the update, and the engine in use has its own
older = (not gone and Path(old).parent != package and Path(old).parent.parent == package.parent
and package.parent.name.lower() == "engine")
if (gone or older) and new.is_file():
print(f"[strata] the picture reader {old} " + ("is not there any more" if gone else "belongs to an older engine")
+ f"; using {new} from the engine package in use", flush=True)
fixed[key] = str(new)
if fixed == vision:
return cfg
@@ -8677,6 +8904,30 @@ def with_component_engine(cfg: dict) -> dict:
return cfg
def with_history_tools(tools, req) -> list:
"""#4663: an agent's tool set changes between turns (deferred tools, MCP servers, a compacted history), and the model calls a tool it used
earlier in this conversation that this turn's `tools` no longer lists. That call came back as raw text with finish_reason "stop", and the
client took it for prose and stopped. A name the conversation's own assistant messages called is a tool the client has, so it counts as
declared (the call comes back as a tool call; the client says what it does with it). Nothing is added when the request declared no
tools at all, or for a name that is already declared."""
if not tools or not isinstance(req, dict):
return tools
try:
have = {t.get("name") for t in tools if isinstance(t, dict)}
extra = []
for message in req.get("messages") or []:
if not isinstance(message, dict) or message.get("role") != "assistant":
continue
for call in message.get("tool_calls") or []:
name = (call.get("function") or {}).get("name") if isinstance(call, dict) else None
if isinstance(name, str) and name and name not in have:
have.add(name)
extra.append({"name": name, "parameters": {"type": "object", "properties": {}}})
return [*tools, *extra] if extra else tools
except Exception:
return tools
def _component_exe(reference: str, cfg: dict) -> str:
"""The engine a run config names as "component:strata-engine". No engine for this PC is a clear notice and exit 5;
the server never goes looking on the internet for one (strata_control/engine_component.py)."""
@@ -8977,6 +9228,9 @@ def main(argv=None, *, lifecycle_event=None, interrupt_exit_code: int = 0) -> in
note = card_note(cfg, env)
if note:
print(note, flush=True)
arch_line = engine_arch_note(cfg)
if arch_line:
print(arch_line, flush=True)
igpu_line = igpu_split_warning(cfg)
if igpu_line:
print(igpu_line, flush=True)
+2
View File
@@ -6,6 +6,7 @@
from __future__ import annotations
import sys
import collections
import threading
import time
import unittest
@@ -25,6 +26,7 @@ def engine_without_process(n=2):
e.slot_claims, e.slot_releasing, e._orphan_seen = set(), set(), {}
e.slot_cv = threading.Condition()
e.waiting, e.admitting, e.wait_lens, e.ctl_epoch = 0, 0, [], 0
e.arrivals = collections.deque(maxlen=64)
e.ctl = threading.Lock()
e.gen = 1
e.up = True
+38
View File
@@ -0,0 +1,38 @@
"""#4662: the server says, in one plain line, when a card in use is of an architecture the compiled engine has no code for.
python -m unittest serve.test_engine_arch_note
"""
import unittest
import serve.server as server
CARDS = {0: 75, 1: 86}
class ArchNote(unittest.TestCase):
def test_a_card_the_build_does_not_cover_is_said(self):
line = server.engine_arch_note({"gpu": [0, 1]}, CARDS, {"archs": [75]})
self.assertIn("sm_75", line)
self.assertIn("sm_86", line)
self.assertIn("update", line)
def test_a_covered_pair_says_nothing(self):
self.assertIsNone(server.engine_arch_note({"gpu": [0, 1]}, CARDS, {"archs": [75, 86]}))
self.assertIsNone(server.engine_arch_note({"gpu": [0, 1]}, CARDS, {"archs": ["sm_75", "sm_86"]}))
def test_only_the_cards_the_run_uses_count(self):
self.assertIsNone(server.engine_arch_note({"gpu": [0]}, CARDS, {"archs": [75]}))
self.assertIsNotNone(server.engine_arch_note({}, CARDS, {"archs": [75]})) # no "gpu": every card
def test_a_build_with_ptx_covers_newer_cards(self):
self.assertIsNone(server.engine_arch_note({"gpu": [0, 1]}, CARDS, {"archs": [75], "ptx": True}))
def test_unknown_things_say_nothing(self):
self.assertIsNone(server.engine_arch_note({"backend": "hip"}, CARDS, {"archs": [75]}))
self.assertIsNone(server.engine_arch_note({"gpu": [0, 1]}, CARDS, {}))
self.assertIsNone(server.engine_arch_note({"gpu": [0, 1]}, CARDS, {"archs": ["x"]}))
self.assertIsNone(server.engine_arch_note({"exe": "component:strata-engine"}))
if __name__ == "__main__":
unittest.main()
+49
View File
@@ -0,0 +1,49 @@
"""#4663: a call to a tool that this turn's `tools` no longer lists, but that the conversation already called, is a tool call (not raw text).
python -m unittest serve.test_history_tools
"""
import unittest
import serve.server as server
from serve.frontend import OutputParser
TODO = {"name": "TodoWrite", "parameters": {"type": "object", "properties": {"todos": {"type": "array"}}}}
CALL = "<tool_call>\n<function=Bash>\n<parameter=command>\nls -la\n</parameter>\n</function>\n</tool_call>"
def history(*names):
return {"messages": [{"role": "user", "content": "go"},
*[{"role": "assistant", "content": "", "tool_calls": [{"id": f"c{i}", "type": "function",
"function": {"name": n, "arguments": "{}"}}]} for i, n in enumerate(names)]]}
def run(tools, text=CALL):
p = OutputParser(thinking=False, tools=tools, stream_tools=True, recover=True, declared_only=True)
evs = p.feed(text) + p.finish("stop")
return [e.call.name for e in evs if e.kind == "tool_call"], "".join(e.text for e in evs if e.kind == "content")
class HistoryTools(unittest.TestCase):
def test_a_tool_the_conversation_called_is_a_tool_call_again(self):
names, answer = run(server.with_history_tools([TODO], history("Bash")))
self.assertEqual(names, ["Bash"])
self.assertEqual(answer, "")
def test_without_the_history_it_stays_text(self):
names, answer = run(server.with_history_tools([TODO], history("Read")))
self.assertEqual(names, [])
self.assertIn("<tool_call>", answer)
def test_declared_names_are_not_doubled_and_no_tools_stays_no_tools(self):
tools = [TODO, {"name": "Bash", "parameters": {"type": "object", "properties": {"command": {"type": "string"}}}}]
self.assertIs(server.with_history_tools(tools, history("Bash", "TodoWrite")), tools)
self.assertEqual(server.with_history_tools([], history("Bash")), [])
self.assertIsNone(server.with_history_tools(None, history("Bash")))
def test_odd_requests_are_left_alone(self):
self.assertEqual(server.with_history_tools([TODO], None), [TODO])
self.assertEqual(server.with_history_tools([TODO], {"messages": [None, {"role": "assistant", "tool_calls": [None, {}]}]}), [TODO])
if __name__ == "__main__":
unittest.main()
+89
View File
@@ -16,6 +16,7 @@ from pathlib import Path
from unittest import mock
from serve.frontend import ChatTemplate
from serve import server
from serve.server import ByteTokenizer, Service, StrataEngine, engine_args, parallel_args, serve
from strata_cli import bench
from strata_cli.client import Endpoint
@@ -808,5 +809,93 @@ class ParallelService(FakeEngineCase):
self.assertTrue(all(a <= 1 for a in active), active)
class ParallelStackDefaults(unittest.TestCase):
"""The exact parallel stack is the default with batch slots; an explicit value wins."""
def env_for(self, cfg, base=None, linux=True):
import os
from unittest import mock
with mock.patch.dict(os.environ, base or {}, clear=False), mock.patch.object(server, "parallel_stack_platform", return_value=linux):
for k in list(os.environ):
if k in server.PARALLEL_STACK_ENV and not (base and k in base):
del os.environ[k]
return server.child_env(cfg)
def test_on_with_parallel(self):
env = self.env_for({"parallel": 8, "args": []})
hip = self.env_for({"parallel": 8, "args": [], "backend": "hip"})
for k, v in server.PARALLEL_STACK_ENV.items():
self.assertEqual(env.get(k), v, k)
self.assertNotIn(k, hip) # HIP: nothing (no measurement without replicas)
self.assertEqual(self.env_for({"args": ["--batch", "4"]}).get("STRATA_BATCH_SELF_COMMIT"), "1")
def test_off_on_windows_and_mac(self):
self.assertNotIn("STRATA_CKPT_POOL", self.env_for({"parallel": 8, "args": []}, linux=False))
self.assertTrue(server.parallel_stack_platform.__doc__)
def test_off_without_slots(self):
self.assertNotIn("STRATA_CKPT_POOL", self.env_for({"args": []}))
self.assertNotIn("STRATA_CKPT_POOL", self.env_for({"parallel": 1, "args": []}))
def test_explicit_wins(self):
env = self.env_for({"parallel": 8, "args": [], "env": {"STRATA_BATCH_GDN_GROUPS": "0"}}, {"STRATA_CKPT_POOL": "0"})
self.assertEqual(env["STRATA_BATCH_GDN_GROUPS"], "0")
self.assertEqual(env["STRATA_CKPT_POOL"], "0")
self.assertEqual(env["STRATA_QSA_SLOTS"], "1")
def test_other_engines_untouched(self):
self.assertNotIn("STRATA_CKPT_POOL", self.env_for({"parallel": 8, "args": [], "arch": "deepseek41"}))
self.assertNotIn("STRATA_CKPT_POOL", self.env_for({"parallel": 8, "args": [], "backend": "sycl"}))
class GdnStackDefaults(unittest.TestCase):
"""The qualified GDN speedups are on from the server (Linux CUDA; Linux HIP gfx12 only); an explicit value wins."""
def env_for(self, cfg, archs=None, linux=True, base=None):
import os
from unittest import mock
env = {k: v for k, v in (base or {}).items()}
with mock.patch.object(server, "parallel_stack_platform", return_value=linux):
server.gdn_stack_env(env, cfg, archs)
return env
def test_cuda_on(self):
for cfg in ({"backend": "cuda"}, {}, {"arch": "qwen4exp"}):
env = self.env_for(cfg)
self.assertEqual(env.get("STRATA_GDN_TC"), "1")
self.assertEqual(env.get("STRATA_GDN_CONVL2"), "2")
self.assertNotIn("STRATA_GDN_CHUNKED", env)
def test_explicit_wins(self):
env = self.env_for({}, base={"STRATA_GDN_TC": "0"})
self.assertEqual(env["STRATA_GDN_TC"], "0")
self.assertEqual(env["STRATA_GDN_CONVL2"], "2")
def test_other_models_and_backends_off(self):
for cfg in ({"arch": "qwen35moe"}, {"arch": "deepseek41"}, {"backend": "sycl"}, {"backend": "metal"}, {"backend": "vulkan"}):
self.assertEqual(self.env_for(cfg), {}, cfg)
def test_windows_and_mac_off(self):
self.assertEqual(self.env_for({}, linux=False), {})
self.assertEqual(self.env_for({"backend": "hip"}, archs={0: "gfx1201"}, linux=False), {})
def test_hip_gfx12_only(self):
hip = {"backend": "hip", "gpu": [0, 1]}
env = self.env_for(hip, archs={0: "gfx1201", 1: "gfx1200"})
self.assertEqual(env, {"STRATA_GDN_CHUNKED": "1"}) # and nothing of the CUDA pair
for archs in ({0: "gfx1100", 1: "gfx1100"}, {0: "gfx1151"}, {0: "gfx1030", 1: "gfx1030"}, {0: "gfx1201", 1: "gfx1151"}, {}, None):
self.assertEqual(self.env_for(hip, archs=archs), {}, archs)
def test_hip_selected_cards_decide(self):
cfg = {"backend": "hip", "gpu": [1]}
self.assertEqual(self.env_for(cfg, archs={0: "gfx1151", 1: "gfx1201"}), {"STRATA_GDN_CHUNKED": "1"})
cfg = {"backend": "hip", "gpu": [0]}
self.assertEqual(self.env_for(cfg, archs={0: "gfx1151", 1: "gfx1201"}), {})
def test_child_env_wiring(self):
from unittest import mock
with mock.patch.object(server, "parallel_stack_platform", return_value=True):
env = server.child_env({"args": []})
self.assertEqual(env.get("STRATA_GDN_TC"), "1")
if __name__ == "__main__":
unittest.main()
+80
View File
@@ -0,0 +1,80 @@
"""#4702 and #4703: a call quoted in a Markdown code form is text, not a call. A four-backtick fence is closed only by a fence of
at least four backticks (an inner ``` block stays inside it), and an indented (4-space) code block after a blank line is a quote.
A call the model writes plainly, or indents inside a paragraph, is still a call.
python -m unittest serve.test_parser_markdown_code
"""
import unittest
from serve.frontend import OutputParser
def tool(name, **props):
return {"name": name, "parameters": {"type": "object", "properties": props}}
S = {"type": "string"}
TOOLS = [tool("read", path=S), tool("write_file", path=S, content=S)]
def qwen(name, **params):
body = "".join(f"<parameter={k}>\n{v}\n</parameter>\n" for k, v in params.items())
return f"<tool_call>\n<function={name}>\n{body}</function>\n</tool_call>"
def parse(text, piece=0, recover=False):
p = OutputParser(thinking=False, tools=TOOLS, stream_tools=False, recover=recover, declared_only=True)
if piece:
evs = []
for i in range(0, len(text), piece):
evs += p.feed(text[i:i + piece])
evs += p.finish("stop")
else:
evs = p.feed(text) + p.finish("stop")
return ([(e.call.name, e.call.arguments) for e in evs if e.kind == "tool_call"],
"".join(e.text for e in evs if e.kind == "content"))
class MarkdownCode(unittest.TestCase):
def each(self, text):
for recover in (False, True):
for piece in (0, 1, 7):
yield parse(text, piece, recover)
def test_an_inner_fence_does_not_close_a_longer_one(self):
text = ("Save this as docs/TOOLS.md:\n\n````markdown\n# Tool calls\n\nA call:\n\n```xml\n"
+ qwen("read", path="EXAMPLE.txt") + "\n```\n````\n\nThat is the file.")
for calls, answer in self.each(text):
self.assertEqual(calls, [])
self.assertEqual(answer, text)
def test_a_call_after_a_closed_longer_fence_is_a_call(self):
text = "Here:\n\n````md\nexample\n````\n\n" + qwen("read", path="a.txt")
for calls, _ in self.each(text):
self.assertEqual(calls, [("read", {"path": "a.txt"})])
def test_a_tilde_fence_is_not_closed_by_backticks(self):
text = "Quote:\n\n~~~\n```\n" + qwen("read", path="EXAMPLE.txt") + "\n```\n~~~\n"
for calls, _ in self.each(text):
self.assertEqual(calls, [])
def test_an_indented_code_block_is_a_quote(self):
block = "\n".join(" " + line for line in qwen("read", path="EXAMPLE.txt").split("\n"))
text = "The format, indented as code:\n\n" + block + "\n\nIndentation makes it a code block in Markdown."
for calls, answer in self.each(text):
self.assertEqual(calls, [])
self.assertEqual(answer, text)
def test_a_plain_call_after_a_paragraph_is_still_a_call(self):
text = "Let me read it.\n" + qwen("read", path="a.txt")
for calls, _ in self.each(text):
self.assertEqual(calls, [("read", {"path": "a.txt"})])
def test_a_call_after_an_indented_block_ends_it(self):
text = "Example:\n\n x = 1\n\nNow the real one:\n\n" + qwen("read", path="a.txt")
for calls, _ in self.each(text):
self.assertEqual(calls, [("read", {"path": "a.txt"})])
if __name__ == "__main__":
unittest.main()
+15 -5
View File
@@ -16,15 +16,25 @@ class ReferenceParser(OutputParser):
if k < len(parts) - 1:
line, self.line = self.line + part, ""
s = line.lstrip()
was_fenced = bool(self.fence)
if self.fence:
if s.startswith(self.fence * 3):
if len(s) - len(s.lstrip(self.fence[0])) >= len(self.fence): # #4702: as many fence characters as opened it
self.fence = ""
elif s[:3] in ("```", "~~~") and (s[0] * 3) not in s[3:]:
self.fence = s[0]
self.fence = s[0] * (len(s) - len(s.lstrip(s[0])))
elif not s:
self.ticks = 0
else:
self.ticks += line.count("`")
if was_fenced or self.fence: # #4703: indented code blocks
self._prev_blank = self._indented = False
elif not s:
self._prev_blank = True
elif line.startswith((" ", "\t")):
self._indented = self._prev_blank or self._indented
self._prev_blank = False
else:
self._prev_blank = self._indented = False
else:
self.line += part
return text
@@ -32,14 +42,14 @@ class ReferenceParser(OutputParser):
class TrackTests(unittest.TestCase):
def assert_state(self, actual, expected):
self.assertEqual((actual.line, actual.fence, actual.ticks),
(expected.line, expected.fence, expected.ticks))
self.assertEqual((actual.line, actual.fence, actual.ticks, actual._prev_blank, actual._indented),
(expected.line, expected.fence, expected.ticks, expected._prev_blank, expected._indented))
self.assertEqual(actual._opener_ok(), expected._opener_ok())
self.assertEqual(actual._in_code(), expected._in_code())
def test_fragment_state_from_varied_initial_states(self):
fragments = ("", "a", "é🙂", "`", "``", "```", "~~~", "\r", "\n", "\r\n",
"\n\n", " ```python\n", "```quoted```\n", "~\ntext", "\t\nrest")
"\n\n", "\n code\n", " x\n", " ```python\n", "````md\n", "```quoted```\n", "~\ntext", "\t\nrest")
for fence, line, ticks, text in itertools.product(("", "`", "~"),
("", " ", "``", "code `"),
(0, 1, 2, 3), fragments):
+53
View File
@@ -0,0 +1,53 @@
"""#4658: after `strata update` the old engine folder stays, so a picture reader written into it kept starting beside the new engine.
python -m serve.test_reader_follows_engine
"""
import tempfile
import unittest
from pathlib import Path
import serve.server as server
class ReaderFollowsEngine(unittest.TestCase):
def make(self, root: Path, *folders: str):
for name in folders:
d = root / "components" / "engine" / name
d.mkdir(parents=True)
(d / "strata.exe").write_bytes(b"x")
(d / "strata-vision.exe").write_bytes(b"x")
return root / "components" / "engine"
def test_a_reader_in_the_older_engine_folder_moves_to_the_engine_in_use(self):
with tempfile.TemporaryDirectory() as tmp:
engine = self.make(Path(tmp), "0.1.52-x", "0.1.54-x")
cfg = {"vision": {"exe": str(engine / "0.1.52-x" / "strata-vision.exe"), "mmproj": "m.gguf"}}
fixed = server.with_current_reader(cfg, engine / "0.1.54-x")
self.assertEqual(fixed["vision"]["exe"], str(engine / "0.1.54-x" / "strata-vision.exe"))
self.assertEqual(fixed["vision"]["mmproj"], "m.gguf")
def test_a_reader_already_in_the_engine_folder_stays(self):
with tempfile.TemporaryDirectory() as tmp:
engine = self.make(Path(tmp), "0.1.54-x")
cfg = {"vision": {"exe": str(engine / "0.1.54-x" / "strata-vision.exe")}}
self.assertIs(server.with_current_reader(cfg, engine / "0.1.54-x"), cfg)
def test_a_reader_somewhere_else_is_the_persons_choice_and_stays(self):
with tempfile.TemporaryDirectory() as tmp:
engine = self.make(Path(tmp), "0.1.54-x")
mine = Path(tmp) / "mine"
mine.mkdir()
(mine / "strata-vision.exe").write_bytes(b"x")
cfg = {"vision": {"exe": str(mine / "strata-vision.exe")}}
self.assertIs(server.with_current_reader(cfg, engine / "0.1.54-x"), cfg)
def test_a_missing_reader_is_still_replaced(self):
with tempfile.TemporaryDirectory() as tmp:
engine = self.make(Path(tmp), "0.1.54-x")
cfg = {"vision": {"exe": str(engine / "0.1.40-x" / "strata-vision.exe")}}
fixed = server.with_current_reader(cfg, engine / "0.1.54-x")
self.assertEqual(fixed["vision"]["exe"], str(engine / "0.1.54-x" / "strata-vision.exe"))
if __name__ == "__main__":
unittest.main()
+126 -1
View File
@@ -11,7 +11,8 @@ from pathlib import Path
from unittest import mock
from serve.frontend import ChatTemplate
from serve.replicas import BLOCK, ReplicaConfigError, ReplicaEngine, Router, cpu_sets, gpu_groups, replica_cfg
from serve.replicas import (BLOCK, ReplicaConfigError, ReplicaEngine, Router, cpu_sets, gpu_groups, remove_arena,
replica_cfg, shared_arena_plan, sweep_stale_arenas)
from serve.server import ByteTokenizer, Service, StrataEngine, serve
from serve.test_parallel import FAKE_BATCH
@@ -309,5 +310,129 @@ class ReplicaService(unittest.TestCase):
self.assertEqual(self.served()[1], 3)
class SharedArena(unittest.TestCase):
"""The opt-in shared expert arena of the replicas (shared_arena_plan): when it is used, when it is not, what a stale
file means."""
CFG = {"replica_shared_arena": True, "args": ["--pack", "/p"], "gpu": [0, 1]}
def plan(self, cfg=None, n=2, tmp=None, **kw):
return shared_arena_plan(cfg or self.CFG, n, shm=tmp, platform=kw.pop("platform", "linux"),
environ=kw.pop("environ", {}), free_bytes=kw.pop("free_bytes", 100 << 30),
use_memfd=kw.pop("use_memfd", False), **kw)
def test_off_by_default_and_for_one_engine(self):
with tempfile.TemporaryDirectory() as d:
self.assertEqual(self.plan({"args": []}, tmp=d), (None, ""))
self.assertEqual(self.plan(n=1, tmp=d), (None, ""))
self.assertEqual(self.plan({"args": []}, tmp=d, environ={"STRATA_REPLICA_SHARED_ARENA": "0"}), (None, ""))
def test_on_by_config_or_environment(self):
with tempfile.TemporaryDirectory() as d:
path, note = self.plan(tmp=d)
self.assertTrue(path and path.startswith(d) and path.endswith(".bin"), path)
self.assertIn("once", note)
path2, _ = self.plan({"args": []}, tmp=d, environ={"STRATA_REPLICA_SHARED_ARENA": "1"})
self.assertTrue(path2)
self.assertEqual(path, path2) # (one server: one file)
def test_windows_gets_a_named_section(self):
with tempfile.TemporaryDirectory() as d:
path, note = self.plan(tmp=d, platform="win32")
self.assertTrue(path and path.startswith("win:Local") and "strata-replicas-" in path, path)
self.assertIn("Windows section", note)
remove_arena(path) # (nothing to remove: harmless)
def test_only_where_there_is_an_arena_to_share(self):
with tempfile.TemporaryDirectory() as d:
self.assertIsNone(self.plan(tmp=d, platform="darwin")[0])
for flag in ("--mmap-experts", "--resident-experts", "--resident-budget-gib", "--shared-expert-arena"):
self.assertIsNone(self.plan({**self.CFG, "args": ["--pack", "/p", flag]}, tmp=d)[0], flag)
self.assertIsNone(self.plan({**self.CFG, "replicas": [{"gpus": [0], "args": ["--mmap-experts"]}, {"gpus": [1]}]}, tmp=d)[0])
self.assertIsNone(self.plan({**self.CFG, "arch": "deepseek41"}, tmp=d)[0])
def test_no_room_or_no_shm_says_so_and_falls_back(self):
with tempfile.TemporaryDirectory() as d:
path, note = self.plan(tmp=d, free_bytes=2 << 30)
self.assertIsNone(path)
self.assertIn("2.0 GiB free", note)
path, note = self.plan(tmp=d + "/nope")
self.assertIsNone(path)
self.assertIn("not usable", note)
def test_a_stale_file_goes_a_live_one_stays(self):
with tempfile.TemporaryDirectory() as d:
uid = __import__("os").getuid() if hasattr(__import__("os"), "getuid") else 0
dead = Path(d) / f"strata-replicas-{uid}-2999999.bin"
live = Path(d) / f"strata-replicas-{uid}-{__import__('os').getpid()}.bin"
other = Path(d) / f"strata-replicas-{uid + 1}-2999999.bin"
junk = Path(d) / "strata-replicas-x.bin"
for f in (dead, live, other, junk):
f.write_bytes(b"x")
with mock.patch("serve.replicas._pid_alive", side_effect=lambda pid: pid != 2999999):
gone = sweep_stale_arenas(d, uid)
self.assertEqual(gone, [dead.name])
self.assertFalse(dead.exists())
self.assertTrue(live.exists() and other.exists() and junk.exists())
@unittest.skipUnless(hasattr(__import__("os"), "memfd_create") and sys.platform.startswith("linux"), "Linux memfd")
def test_a_memfd_is_not_limited_by_dev_shm_and_is_closed_at_the_end(self):
import os
path, note = shared_arena_plan(self.CFG, 2, platform="linux", environ={}, free_bytes=100 << 30, use_memfd=True)
self.assertTrue(path and path.startswith(f"/proc/{os.getpid()}/fd/"), path)
fd = int(path.rsplit("/", 1)[1])
os.ftruncate(fd, 1 << 20)
self.assertEqual(os.fstat(fd).st_size, 1 << 20)
with open(path, "r+b") as other: # (how an engine opens it)
other.seek(100)
other.write(b"x")
remove_arena(path)
with self.assertRaises(OSError):
os.fstat(fd)
remove_arena(path) # (twice: harmless)
@unittest.skipUnless(hasattr(__import__("os"), "memfd_create") and sys.platform.startswith("linux"), "Linux memfd")
def test_a_memfd_arena_warns_when_the_engines_will_not_all_fit(self):
cfg = {**self.CFG, "model_name": "qwen3.8-flash-next-iq3_s"}
path, note = shared_arena_plan(cfg, 2, platform="linux", environ={}, free_bytes=57 << 30, use_memfd=True)
try:
self.assertTrue(path)
self.assertIn("WARNING", note)
self.assertIn("may not fit", note)
finally:
remove_arena(path)
path, note = shared_arena_plan(cfg, 2, platform="linux", environ={}, free_bytes=120 << 30, use_memfd=True)
try:
self.assertNotIn("WARNING", note)
finally:
remove_arena(path)
def test_a_memfd_needs_free_ram(self):
path, note = shared_arena_plan(self.CFG, 2, platform="linux", environ={}, free_bytes=2 << 30, use_memfd=True)
self.assertIsNone(path)
self.assertIn("2.0 GiB of RAM", note)
def test_removing_twice_or_nothing_is_harmless(self):
with tempfile.TemporaryDirectory() as d:
f = Path(d) / "a.bin"
f.write_bytes(b"x")
remove_arena(str(f))
remove_arena(str(f))
remove_arena(None)
self.assertFalse(f.exists())
def test_the_replica_engine_unlinks_the_file_at_close(self):
with tempfile.TemporaryDirectory() as d:
f = Path(d) / "a.bin"
f.write_bytes(b"x")
class E:
def close(self):
pass
r = ReplicaEngine([E(), E()])
r.__dict__["arena_file"] = str(f)
r.close()
self.assertFalse(f.exists())
if __name__ == "__main__":
unittest.main()
+303 -38
View File
@@ -100,7 +100,7 @@ HF_REVISIONS = {
"unsloth/Qwen3.8-Flash-Next-GGUF": "38bb39ee97821de2c9009abb7e93950eec396e66", # 2026-09-30
"unsloth/Qwen3.6-35B-A3B-MTP-GGUF": "5bc3e238d916f48a861bac2f8a1990a0e9b7e98d", # 2026-10-12
"agentionai/Qwen3.8-Flash-Next-Gyro-GGUF": "1ada08422e404aca51ab9df665f089f34c419c6b", # 2026-10-13
"unsloth/DeepSeek-V4.1-Flash-1017-GGUF": "f924b94f19c1fea02c06a65c8475b0a45e2b88b1", # 2026-10-20 (the port's reference)
"unsloth/DeepSeek-V4.1-Flash-1017-GGUF": "8a6ec9cf118e785a362ea94ee123c61c70553be6", # 2026-10-30 (adds UD-IQ1_S / UD-IQ1_M; every file pinned from the 2026-10-20 commit has the same size and SHA-256 here)
"ISTA-DASLab/DeepSeek-V4.1-Flash-1017-GSQ-RCO-GGUF": "cb02422c900df489cea5833e8a92171224059b3c", # 2026-10-24 (ISTA-DASLab's final 1.75 / 2.00 / 2.25 bpw sets and Engram)
}
@@ -735,6 +735,36 @@ DSV41_LEGACY_NAMES = {
**{DSV41_ISTA_SHARD.format(quant="IQ2_XS", i=i): "model-2.25bpw-%05d-of-00004.gguf" % i for i in range(1, 5)},
DSV41_ISTA_ENGRAM: "engram-IQ4_NL.gguf",
}
# The SIX-shard layout (0.2.8.1). On 2026-10-27 (commit a419d74c, llama.cpp b11870 and newer) ISTA-DASLab re-split the three sets: five shards per set
# (shard 1 is 5.8 MB of metadata and the tokenizer, no tensors) and the Engram as the sixth shard of the split. The files are not the pinned ones, so they
# are a second layout beside the four-shard one: setup downloads the pinned four-shard files as before (HF_REVISIONS stays at cb02422c, the owner decides
# when the default moves), and takes this layout from "files already here" (--gguf-dir / --model-path, the app's "Use a local GGUF") by size and SHA-256.
# Same model: the experts and dense weights have the same types, shapes and sizes in both layouts; the converter folded the Engram gate (q_weight *
# k_weight -> engram_gate), merged the two Engram tables into one tensor (per_layer_token_embd, the same 110,595,290,400 bytes) and added the image tokens'
# routing bias (exp_probs_b_vl, used only for picture tokens). The engine reads both (engram.gate.weight or q_weight/k_weight; ds4_model.cu).
DSV41_ISTA6_SHARD = "DeepSeek-V4.1-Flash-1017-GSQ-RCO-{quant}-{i:05d}-of-00006.gguf"
DSV41_ISTA6_SHARDS = 5 # the files of a set itself; the sixth shard of its split is the Engram
DSV41_ISTA6_FILES = {
"DeepSeek-V4.1-Flash-1017-GSQ-RCO-IQ1_M-00001-of-00006.gguf": (5780704, "3e8ddd11441edd4bc19bacaef5dbe38818af2cbdf0820fb70f1d2d851fcc192a"),
"DeepSeek-V4.1-Flash-1017-GSQ-RCO-IQ1_M-00002-of-00006.gguf": (39340012896, "be4ceb1caf1bed28e886860cf6a52adc8f5ca9eb0d2098ade39bb4179d48b323"),
"DeepSeek-V4.1-Flash-1017-GSQ-RCO-IQ1_M-00003-of-00006.gguf": (38698805888, "758c66ba65236e8ce0820c21a7b6f9b2947681df8cc509f672a77722e8191b9a"),
"DeepSeek-V4.1-Flash-1017-GSQ-RCO-IQ1_M-00004-of-00006.gguf": (39707055008, "04a5bea2b38eb576f49efc19c0a677768d6e2a8ea5772d72fb6cb79e1733a212"),
"DeepSeek-V4.1-Flash-1017-GSQ-RCO-IQ1_M-00005-of-00006.gguf": (2969539840, "7c8dbee99890f05dbc420eba6f44393f5228d06248fe61e1ae8ed002449c7549"),
"DeepSeek-V4.1-Flash-1017-GSQ-RCO-IQ2_XS-00001-of-00006.gguf": (5780704, "0182a2a1f42d319a0dbc6e34aa82a063880c069fe3aafbbea86018f386e64e5f"),
"DeepSeek-V4.1-Flash-1017-GSQ-RCO-IQ2_XS-00002-of-00006.gguf": (39581533600, "5c88039fa640460a9fc3465a6dfcd162a4083dd738d55e5b52e025be14555a08"),
"DeepSeek-V4.1-Flash-1017-GSQ-RCO-IQ2_XS-00003-of-00006.gguf": (39417972640, "0fddebfd05f0a36cfd107ba5dfd2547e64df61adbab39560a5a5f5521ca099a5"),
"DeepSeek-V4.1-Flash-1017-GSQ-RCO-IQ2_XS-00004-of-00006.gguf": (39079187648, "c577a43c6e6e615838a1655a0b162541edeac6f36542f08c4a9593a70dcaf500"),
"DeepSeek-V4.1-Flash-1017-GSQ-RCO-IQ2_XS-00005-of-00006.gguf": (37141423776, "851d8605bf504962054d17ea8a0d9e2e0a228d085d396bd865a4de9f16d90fbd"),
"DeepSeek-V4.1-Flash-1017-GSQ-RCO-IQ2_XXS-00001-of-00006.gguf": (5780704, "f356f198561f56df36f7ab92d7238a926646d76de992c34f15bcb939a5f529b9"),
"DeepSeek-V4.1-Flash-1017-GSQ-RCO-IQ2_XXS-00002-of-00006.gguf": (39535781184, "25af3785ccf925f54137e1ead4c481a8952315dc0b1af6519a99407f67bce8f3"),
"DeepSeek-V4.1-Flash-1017-GSQ-RCO-IQ2_XXS-00003-of-00006.gguf": (39736712000, "8168d0998fc494def2e1952d83c131425a108660aae9d29190f5a8670ab62b48"),
"DeepSeek-V4.1-Flash-1017-GSQ-RCO-IQ2_XXS-00004-of-00006.gguf": (39036908384, "9eb9b4de1968f150dfc207eb31c27be9cd9b5666a9a6246bde8a22b184b4bee0"),
"DeepSeek-V4.1-Flash-1017-GSQ-RCO-IQ2_XXS-00005-of-00006.gguf": (19923784832, "7e35f6b4e6fc46f6c4b152b593d84d683c5ece695ae6ad734f556198c041bec1"),
}
# The Engram of that commit: the same name and size as the pinned one, converted in place (one tensor, split 6 of 6), so a new SHA-256. Either file is the
# Engram of either layout (the pack reads the tables from both); the pin stays the old one, this hash is accepted beside it.
DSV41_ENGRAM_CONVERTED = (110595290816, "3dac4c54893ad7aa89103c0028500eadc78e74112cef3a2930a97e67dc9fb4f3")
DSV41_SHA_ALSO = {DSV41_ISTA_ENGRAM: (DSV41_ENGRAM_CONVERTED[1],)}
# the files every official set shares (name -> where it comes from); they live in models/deepseek-v4.1-flash-shared, once
DSV41_MMPROJ = "mmproj-BF16.gguf" # the vision tower's weights (972 MB, pinned): packed into the model's pack when pictures are on (0.2.7)
DSV41_SHARED = {
@@ -743,7 +773,24 @@ DSV41_SHARED = {
DSV41_SHARD.format(i=1): hf(DSV41_REPO) + "UD-Q8_K_XL/", # only its metadata is read (pack_gguf --engram-meta)
DSV41_DSPARK: hf(DSV41_REPO),
}
DSV41_ALL_FILES = {**DSV41_FILES, **DSV41_ISTA_FILES, **DSV41_LEGACY_FILES}
# Unsloth's own low-bit sets (2026-10-30, unsloth/DeepSeek-V4.1-Flash-1017-GGUF, UD-IQ1_S and UD-IQ1_M): shard 1 = metadata, shards 2-4 = the model, shard 5 = the
# Engram table (IQ4_NL, 110.6 GB; its data is byte-identical to ISTA's Engram file, so an ISTA Engram already here is used instead of fetching shard 5).
# Same tensor names as the IST-DAS six-shard layout; dense weights in Q6_K / Q8_0 / Q5_K, router F32. Opt-in model names, not the download default.
DSV41_UD_SHARD = "DeepSeek-V4.1-Flash-1017-{quant}-{i:05d}-of-00005.gguf"
DSV41_UD_FILES = {
"DeepSeek-V4.1-Flash-1017-UD-IQ1_M-00001-of-00005.gguf": (5780868, "5197c309479c7be06e3612dc696d7f18e13e5942cfe52e70c3c586b7e585a5ac"),
"DeepSeek-V4.1-Flash-1017-UD-IQ1_M-00002-of-00005.gguf": (49707332128, "fbfb546557a33f41332ffbed7f15fbed249b9f7796975d9db571c8cf3870f335"),
"DeepSeek-V4.1-Flash-1017-UD-IQ1_M-00003-of-00005.gguf": (49292777280, "394b7a06e04d4b1b96b6028ec3d4dec49e4716900c26a493e8c2aa38a357ea1a"),
"DeepSeek-V4.1-Flash-1017-UD-IQ1_M-00004-of-00005.gguf": (40803165856, "323a7f59a2938879eaac82b37c443c85151e518071ad44c79b1e4580c91d0ed7"),
"DeepSeek-V4.1-Flash-1017-UD-IQ1_M-00005-of-00005.gguf": (110595290592, "771805301e695d72d268151dfc43043581ba904298574aa879d4349cbe67b020"),
"DeepSeek-V4.1-Flash-1017-UD-IQ1_S-00001-of-00005.gguf": (5780868, "5197c309479c7be06e3612dc696d7f18e13e5942cfe52e70c3c586b7e585a5ac"),
"DeepSeek-V4.1-Flash-1017-UD-IQ1_S-00002-of-00005.gguf": (49120285248, "e83af39db628614d252d49a316af8cd5911d78ab96a9e7a5997b392ece26eaf7"),
"DeepSeek-V4.1-Flash-1017-UD-IQ1_S-00003-of-00005.gguf": (48899707008, "6caa1da6302b532374d55da6510fc92a9031a0ee1bd2bb5bf7defca2477f3d98"),
"DeepSeek-V4.1-Flash-1017-UD-IQ1_S-00004-of-00005.gguf": (24358544704, "71c6c54dd1f20008e2130188cc655742d3738a365de4571d21a56de503b206b7"),
"DeepSeek-V4.1-Flash-1017-UD-IQ1_S-00005-of-00005.gguf": (110595290592, "771805301e695d72d268151dfc43043581ba904298574aa879d4349cbe67b020"),
}
DSV41_FILES.update(DSV41_UD_FILES) # (the repository's pins: REPO_PINS)
DSV41_ALL_FILES = {**DSV41_FILES, **DSV41_ISTA_FILES, **DSV41_ISTA6_FILES, **DSV41_LEGACY_FILES, **DSV41_UD_FILES}
DSV41_PROFILES = ROOT / "data" / "dsv41" # the shipped hot-expert profiles (profile-2.00.bin, profile-2.25.bin, + .sha256)
DSV41_CONTEXT = 8192 # the context the engine was checked at (4096 and 8192; 64K is not tried yet)
DSV41_MAC_VISION = False # an Apple Silicon Mac: the Metal engine has not run DeepSeek's picture tower (0.2.7.1), so pictures stay off there
@@ -768,6 +815,7 @@ MULTI_FILE_FAMILIES["deepseek41"]["models"] = {
"perplexity (code and agent work 5-9%)",
"ram_gb": 40, "recipe": "ista", "subdir": None, "legacy_subdir": "2.00bpw", "profile": "profile-2.00.bin", "hf": hf(DSV41_ISTA_REPO),
"shards": [DSV41_ISTA_SHARD.format(quant="IQ2_XXS", i=i) for i in range(1, 5)],
"shards6": [DSV41_ISTA6_SHARD.format(quant="IQ2_XXS", i=i) for i in range(1, DSV41_ISTA6_SHARDS + 1)],
"shared": [DSV41_ISTA_ENGRAM, DSV41_SHARD.format(i=1), DSV41_DSPARK]},
"IST-DAS-2.25bpw": {
"label": "IST-DAS 2.25 bpw",
@@ -775,6 +823,7 @@ MULTI_FILE_FAMILIES["deepseek41"]["models"] = {
"2.00). About the same speed; quality about 7.3% above the original model's perplexity (code and agent work 2-3%)",
"ram_gb": 44, "recipe": "ista", "subdir": None, "legacy_subdir": "2.25bpw", "profile": "profile-2.25.bin", "hf": hf(DSV41_ISTA_REPO),
"shards": [DSV41_ISTA_SHARD.format(quant="IQ2_XS", i=i) for i in range(1, 5)],
"shards6": [DSV41_ISTA6_SHARD.format(quant="IQ2_XS", i=i) for i in range(1, DSV41_ISTA6_SHARDS + 1)],
"shared": [DSV41_ISTA_ENGRAM, DSV41_SHARD.format(i=1), DSV41_DSPARK]},
"IST-DAS-1.75bpw": {
"label": "IST-DAS 1.75 bpw",
@@ -782,7 +831,22 @@ MULTI_FILE_FAMILIES["deepseek41"]["models"] = {
"where 2.00 is tight (a 12 GB card and 32-40 GB of RAM). Smallest: fine for code and agent work, noticeably weaker on plain prose; pick 2.00 if it fits",
"ram_gb": 34, "recipe": "ista", "subdir": None, "legacy_subdir": None, "profile": "profile-2.00.bin", "hf": hf(DSV41_ISTA_REPO),
"shards": [DSV41_ISTA_SHARD.format(quant="IQ1_M", i=i) for i in range(1, 5)],
"shards6": [DSV41_ISTA6_SHARD.format(quant="IQ1_M", i=i) for i in range(1, DSV41_ISTA6_SHARDS + 1)],
"shared": [DSV41_ISTA_ENGRAM, DSV41_SHARD.format(i=1), DSV41_DSPARK]},
"UD-IQ1_S": {
"label": "Unsloth UD-IQ1_S",
"about": "Unsloth's smallest dynamic version: 122 GB of model (experts in IQ1_S / IQ2_XXS, attention in Q6_K) + the 111 GB n-gram table + 8 GB drafter. Same size class as IST-DAS 1.75 bpw. A 1-bit version: expect weaker answers than the 2.00 and 2.25 versions on long, detailed or careful work (short answers, code and tool calls looked fine in our check; its quality is not measured against the original yet). Pick 2.00 or 2.25 if they fit",
"ram_gb": 36, "recipe": "udlow", "subdir": "UD-IQ1_S", "legacy_subdir": None, "profile": "profile-2.00.bin", "hf": hf(DSV41_REPO) + "UD-IQ1_S/",
"shards": [DSV41_UD_SHARD.format(quant="UD-IQ1_S", i=i) for i in range(1, 5)],
"engram_shard": DSV41_UD_SHARD.format(quant="UD-IQ1_S", i=5),
"shared": [DSV41_ISTA_ENGRAM, DSV41_DSPARK]},
"UD-IQ1_M": {
"label": "Unsloth UD-IQ1_M",
"about": "Unsloth's next dynamic version: 140 GB of model (experts in IQ1_S / IQ2_XXS / IQ2_XS / IQ2_S, attention in Q6_K) + the 111 GB n-gram table + 8 GB drafter. Same size class as IST-DAS 2.00 bpw. Part of its experts are 1-bit: expect somewhat weaker answers than the 2.25 version on long, detailed or careful work (short answers, code and tool calls looked fine in our check; its quality is not measured against the original yet)",
"ram_gb": 40, "recipe": "udlow", "subdir": "UD-IQ1_M", "legacy_subdir": None, "profile": "profile-2.00.bin", "hf": hf(DSV41_REPO) + "UD-IQ1_M/",
"shards": [DSV41_UD_SHARD.format(quant="UD-IQ1_M", i=i) for i in range(1, 5)],
"engram_shard": DSV41_UD_SHARD.format(quant="UD-IQ1_M", i=5),
"shared": [DSV41_ISTA_ENGRAM, DSV41_DSPARK]},
"UD-Q8_K_XL": {
"label": "Unsloth UD-Q8_K_XL",
"about": "Unsloth Dynamic 8-bit: the experts in MXFP4 as DeepSeek made them, the rest in BF16 (the checkpoint's own precision); "
@@ -906,7 +970,7 @@ def run(cmd, cwd=None, env=None, check=True, quiet=False):
env.setdefault("STRATA_EXIT_WITH_PARENT", "1") # tools/mtp_fetch.py ends when setup ends (#3257: a cancel left it writing)
r = subprocess.run([str(c) for c in cmd], cwd=cwd, env=env,
stdout=subprocess.PIPE if quiet else None, stderr=subprocess.STDOUT if quiet else None,
text=True)
text=True, errors="replace")
if check and r.returncode != 0:
if quiet and r.stdout:
say(r.stdout[-4000:])
@@ -935,7 +999,7 @@ def fetch_mtp(mtp: Path, env: dict, tries: int = 3):
def out(cmd):
try:
return subprocess.run(cmd, capture_output=True, text=True, timeout=60).stdout
return subprocess.run(cmd, capture_output=True, text=True, errors="replace", timeout=60).stdout or ""
except (OSError, subprocess.TimeoutExpired):
return ""
@@ -975,7 +1039,7 @@ def ram_gb():
if sys.platform == "darwin":
try:
value = subprocess.run(["sysctl", "-n", "hw.memsize"], capture_output=True,
text=True, timeout=3, check=True).stdout.strip()
text=True, errors="replace", timeout=3, check=True).stdout.strip()
return int(value) / 2**30
except (OSError, ValueError, subprocess.CalledProcessError, subprocess.TimeoutExpired):
return 0.0
@@ -1899,6 +1963,10 @@ def choose_gpus(a, found) -> list:
fail("none of your GPUs can run Strata", "it needs an NVIDIA RTX 20 series or newer (compute capability 7.5+)")
can = together_ok(found)
if not can:
for g in found: # said, not asked: a card that works alone but cannot share a model beside a bigger one
if g["index"] != single[0]["index"] and gpu_problem(g) is None:
say(f" GPU {g['index']} ({g['name']}, {g['vram_gb']:.0f} GB) is not used: a card that shares a model needs {SPLIT_MIN_VRAM_GB} GB or more, "
f"so the model runs on GPU {single[0]['index']} alone.")
return [single[0]["index"]]
return choose_roles(a, found)
@@ -2215,7 +2283,7 @@ CMAKE_MIN = (3, 24) # CMakeLists.txt: cmake_minimum_required(
def _tool_version(path, name):
"""(ok, version tuple) of `path --version`: ok False when it does not run (a pip launcher whose Python is gone)."""
try:
done = subprocess.run([path, "--version"], capture_output=True, text=True, timeout=30)
done = subprocess.run([path, "--version"], capture_output=True, text=True, errors="replace", timeout=30)
except (OSError, subprocess.SubprocessError):
return False, ()
if done.returncode != 0:
@@ -3146,13 +3214,14 @@ def own_gguf_is_whole(path: Path) -> bool:
return False
def verify_sha256(s: Path, size: int, sha: str, own: bool = False, repair=None) -> None:
def verify_sha256(s: Path, size: int, sha: str, own: bool = False, repair=None, also: tuple = ()) -> None:
"""A shard's size and SHA-256 against the pinned values (the Unsloth file); the result is kept in its finish mark,
so the ~5 minutes of hashing 111 GB happen once. A wrong file is deleted, so the next run downloads it again.
`repair` (a downloaded file's own re-reader, see repair_download): the wrong file is first compared with the server
piece by piece and only the damaged pieces are written again, then it is checked a second time."""
piece by piece and only the damaged pieces are written again, then it is checked a second time.
`also`: other SHA-256 values that are the same file for Strata's purposes (the Engram ISTA-DASLab converted in place on 2026-10-27, DSV41_SHA_ALSO)."""
m = s.with_name(s.name + ".done")
if m.exists() and f"sha256 {sha}" in m.read_text(encoding="utf-8", errors="replace"):
if m.exists() and any(f"sha256 {x}" in m.read_text(encoding="utf-8", errors="replace") for x in (sha, *also)):
return
have = s.stat().st_size if s.exists() else -1
if own and have > 0 and have != size and own_gguf_is_whole(s):
@@ -3175,6 +3244,10 @@ def verify_sha256(s: Path, size: int, sha: str, own: bool = False, repair=None)
digest = _hash_file(s, size)
else:
say(" The damaged parts could not be found this way.")
if digest != sha and digest in also:
say(f" {s.name} is the converted version ISTA-DASLab published on 2026-10-27 (the same tables in one tensor); it is used as it is")
mark(s, f"sha256 {digest}")
return
if digest != sha:
if own and own_gguf_is_whole(s):
warn(f"{s.name} is not the published file (SHA-256 {digest[:12]}..., expected {sha[:12]}...) but it is a complete GGUF file, so "
@@ -4209,11 +4282,12 @@ _DISPLAY_CLASS = r"SYSTEM\CurrentControlSet\Control\Class\{4d36e968-e325-11ce-bf
def _win_display_adapters() -> list[dict]:
"""The display adapters present ({"name", "pnp"}), from Win32_VideoController."""
ps = ("Get-CimInstance Win32_VideoController | ForEach-Object { $_.Name + '|' + $_.PNPDeviceID + '|' + "
ps = ("[Console]::OutputEncoding = [System.Text.Encoding]::UTF8; " # the pipe is read as UTF-8 (#4675)
"Get-CimInstance Win32_VideoController | ForEach-Object { $_.Name + '|' + $_.PNPDeviceID + '|' + "
"$_.AdapterRAM }")
text = out(["powershell", "-NoProfile", "-NonInteractive", "-Command", ps])
found = []
for line in text.splitlines():
for line in (text or "").splitlines():
parts = line.strip().split("|")
if len(parts) >= 2 and parts[1]:
ram = parts[2] if len(parts) > 2 else ""
@@ -4285,7 +4359,8 @@ def amd_gpus_windows(adapters=None, registry=None) -> list[dict]:
for k, r in enumerate(registry): # the same card's driver instance: its 64-bit VRAM size
if k in used or _pci_device_id(r.get("MatchingDeviceId")) != did:
continue
if r.get("DriverDesc") and ad.get("name") and r["DriverDesc"].strip() != ad["name"].strip():
if (r.get("DriverDesc") and ad.get("name") and "\ufffd" not in ad["name"]
and r["DriverDesc"].strip() != ad["name"].strip()):
continue
used.add(k)
mem = r.get("HardwareInformation.qwMemorySize")
@@ -4353,7 +4428,7 @@ def hip_devices(probe: Path | None = None, text: str | None = None) -> list[dict
return None
try:
hip_runtime_beside_exe(probe.parent) # #468 #461: not the driver's System32 copy
r = subprocess.run([str(probe), "--list-devices"], capture_output=True, text=True, timeout=120,
r = subprocess.run([str(probe), "--list-devices"], capture_output=True, text=True, errors="replace", timeout=120,
cwd=str(probe.parent), env=engine_env(probe.parent))
except (OSError, subprocess.TimeoutExpired):
return None
@@ -4479,7 +4554,7 @@ def engine_from_feed(eng: Path, updating: bool = False) -> Path | None:
say(" Fetching the ready-made engine from the signed update feed ...")
try:
run = subprocess.run([sys.executable, str(ROOT / "strata.py"), "update", "--no-control", "--output", "json"],
env=env, cwd=str(ROOT), capture_output=True, text=True, timeout=3600)
env=env, cwd=str(ROOT), capture_output=True, text=True, errors="replace", timeout=3600)
except (OSError, subprocess.SubprocessError) as e:
warn(f"the update feed could not be run ({e})")
return None
@@ -5815,6 +5890,18 @@ def prebuilt_vision(meta: dict, gpu: dict, vision: str) -> str:
return vision
def local_build_archs(archs, found) -> list:
"""#4662: the architectures a compiled engine gets code for: the cards this install uses AND every other card Strata can run on in the PC.
A PC with a TITAN RTX (sm_75) and an RTX 3080 (sm_86) was updated with archs [75] (the pair was not picked as a split by setup), and the
layer-split run on both cards then launched kernels the sm_75 build holds only a trap for: "unspecified launch failure" on every long prompt.
One more architecture is a few more minutes of compile, once; a card Strata cannot use (older than sm_75) is left out."""
try:
present = {int(g["arch"]) for g in found or [] if gpu_problem(g) is None}
except (KeyError, TypeError, ValueError):
present = set()
return sorted({int(x) for x in archs} | present)
def build_engine(gpu, vision, yes, llama, toolkit=None) -> Path:
"""Compile the engine (and, for images, the encoder) for this GPU; the results go to engine/. A compiled
engine whose source files changed since (a `git pull`) is compiled again: only the changed files, a few minutes.
@@ -5832,6 +5919,7 @@ def build_engine(gpu, vision, yes, llama, toolkit=None) -> Path:
local = meta.get("source") == "local"
src, vsrc = source_hash(ENGINE_SOURCES), source_hash(VISION_SOURCES)
archs = sorted({int(x) for x in gpu.get("archs", [gpu["arch"]])}) # every card the model runs on
archs = local_build_archs(archs, gpus()) # #4662: and every other card in the PC
built = set(arch_numbers(meta.get("archs")))
# a card the engine has no code for (a GPU added with --gpus, #128) needs a compile even when the source is the
# same; the compile keeps the generations it was built for
@@ -6412,7 +6500,7 @@ def write_config(path: Path, cfg: dict):
# "args_written" (strata_control/user_args.py) is setup's own; "user_args" is not in SETUP_KEYS on purpose: carry_over keeps it,
# then applies the person's options over the generated ones
SETUP_KEYS = frozenset({"exe", "args", "cwd", "tokenizer", "model_name", "log", "lib_dirs", "port", "backend", "env",
"gpu", "gpus_asked", "layer_split", "host", "api_key", "draft_vocab", "vision", "args_written"})
"gpu", "gpus_asked", "gpus_by", "layer_split", "host", "api_key", "draft_vocab", "vision", "args_written"})
SETUP_ENV = frozenset({"STRATA_HIPBLASLT_TUNING", "STRATA_ROCBLAS_TUNING", "STRATA_RESIDENT_PIN", "STRATA_NO_ARENA_THP", "STRATA_MMQ_KQ", "STRATA_HC_Q8",
"STRATA_ROUTE_TAIL_SKIP"}) | frozenset(GFX1103_SWITCHES) | frozenset(GFX1150_SWITCHES) # the "env" entries setup writes (the gfx1103 switches are setup's too: a re-run refreshes them, they do not linger)
# 0.1.44 added these three, written for Unsloth's files only (unsloth_default_env). In any other model's config they are the
@@ -6994,6 +7082,31 @@ def settings_summary(cfg: dict, port=None) -> str:
return " ".join(out) + ("; " if out else "") + "server " + ", ".join(srv)
def widen_setups_one_card(cfg_path: Path, cfg: dict, amd: list) -> dict:
"""A DeepSeek run config on AMD that names one card and was written by setup 0.2.8 or earlier (the card was setup's own pick, marked "asked"
by mistake) moves to every card that counts, here at the start, with its pack and files untouched. A card the person named
(strata_control/gpu_pick.py) stays. Linux only: several AMD cards sharing a model is not supported on Windows."""
if WIN or not isinstance(cfg, dict) or cfg.get("gpus_by") or not cfg.get("gpus_asked"):
return cfg
try:
from strata_control import gpu_pick
usable = amd_usable_cards(amd)
if not gpu_pick.looks_like_setups_pick(cfg, usable):
return cfg
cards = [c["index"] for c in dsv41_auto_cards(amd, ROOT / "engine")]
if len(cards) < 2:
return cfg
was = gpu_pick.single_card(cfg)
new = gpu_pick.widened(cfg, cards)
write_config(cfg_path, new)
ok(f"DeepSeek now uses all {len(cards)} of your AMD cards (it was set up on GPU {was} alone by an earlier Strata). Nothing is rebuilt. "
f"To keep it on one card: strata launch --gpu {was} (it is remembered from then on)")
return new
except Exception as e: # noqa: BLE001 - never stop a start over this
warn(f"could not check which cards this model should use ({e})")
return cfg
def start(cfg_path: Path, port: int | None, gpu: int | list | None = None, open_browser=True, yes=False,
layer_split=None, keep=None) -> int:
"""Start the model. Nothing is installed, downloaded or compiled while it starts (strata_control/acquisition.py):
@@ -7048,13 +7161,21 @@ def _start(cfg_path: Path, port: int | None, gpu: int | list | None = None, open
if miss:
fail("the installed engine has no code for " + ", ".join(f"{x['name']} ({x['arch']})" for x in miss),
"set it up for these cards: ./setup.sh --setup --backend hip --gpus " + ",".join(map(str, gpu)))
cfg["gpu"], cfg["gpus_asked"] = gpu, True
cfg["gpu"], cfg["gpus_asked"], cfg["gpus_by"] = gpu, True, "person"
cfg["layer_split"] = layer_split or cfg.get("layer_split") or "auto"
split_budget(cfg, yes, True) # #498: before it is saved (asks when the RAM is short, #737)
write_config(cfg_path, cfg)
gpu = None
elif gpu is not None:
cmd += ["--gpu", str(gpu)]
if not isinstance(gpu, list): # a DeepSeek model on AMD keeps a card the person names (the others: this start only)
from strata_control import gpu_pick
saved = gpu_pick.remember(cfg_path, [gpu])
if saved is not None:
cfg = saved
ok(f"saved for this model: it runs on GPU {gpu} from now on")
else:
cfg = widen_setups_one_card(cfg_path, cfg, amd) # a DeepSeek config from setup 0.2.8 and earlier: setup's own one-card pick
found = []
use = gpu if gpu is not None else cfg.get("gpu")
if isinstance(use, list):
@@ -7406,7 +7527,7 @@ def mtp_corrupt(mtp: Path, env=None) -> bool:
if not (mtp / "tensors").is_dir():
return False
r = subprocess.run([sys.executable, str(ROOT / "tools" / "mtp_fetch.py"), "verify", "--out", str(mtp)], env=env,
stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True)
stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True, errors="replace")
return r.returncode == 3
@@ -7686,7 +7807,7 @@ def engine_step(a, gpu, hip, amd, cuda_tk, vision):
def engine_families(eng: Path) -> set:
"""The model families an engine runs (`strata --families`; an engine from before it: Flash-Next's only)."""
try:
text = subprocess.run([str(eng / EXE), "--families"], capture_output=True, text=True, timeout=60,
text = subprocess.run([str(eng / EXE), "--families"], capture_output=True, text=True, errors="replace", timeout=60,
env=engine_env(eng)).stdout # #2372: with the engine's rocm/bin on PATH
except (OSError, subprocess.SubprocessError):
return {"qwen4exp"}
@@ -8049,7 +8170,7 @@ def dsv41_manual_steps(eng: Path) -> str:
"and run it again. Strata checks it before it is used.")
def get_dsv41_engine(eng: Path, hip: bool, package: str | None = None, feed: str | None = None) -> Path | None:
def get_dsv41_engine(eng: Path, hip: bool, package: str | None = None, feed: str | None = None, archs=None) -> Path | None:
"""The DeepSeek engine package next to the engine `eng`: one the person gave (--dsv41-package) or put in engine-dsv41/ by hand, else downloaded from
the signed feed `feed` (--feed; else STRATA_UPDATE_FEED, a remembered https feed, or the release's). Checked first either way; its
strata-dsv41, or None when it cannot be had."""
@@ -8073,6 +8194,8 @@ def get_dsv41_engine(eng: Path, hip: bool, package: str | None = None, feed: str
if hip:
from strata_control import updater
hw = {**updater.detect_hardware(), "backend": "hip", "backend_source": "requested"}
if archs: # the package is chosen for the cards this model runs on, not for every card in the PC
hw["gfx"] = sorted({str(x) for x in archs})
exe = dsv41_engine.install(root, source, hardware=hw, emit=lambda m: say(" " + m))
if exe is None:
warn("no DeepSeek engine package could be fetched (see above)")
@@ -8119,7 +8242,7 @@ def dsv41_folder(a, fam: dict, tag: str, model: str = "UD-Q8_K_XL") -> Path:
if a.gguf_dir:
folder = Path(a.gguf_dir)
d = fam["models"][model]
names = [n for s in d["shards"] for n in (s, DSV41_LEGACY_NAMES.get(s)) if n]
names = [n for s in d["shards"] for n in (s, DSV41_LEGACY_NAMES.get(s)) if n] + list(d.get("shards6") or [])
if any((folder / n).exists() for n in names):
return folder
for sub in (d.get("subdir"), d.get("legacy_subdir"), model): # the set's own folder inside (the first release used 2.00bpw/, 2.25bpw/)
@@ -8140,17 +8263,51 @@ def dsv41_pick(folder: Path, name: str) -> Path:
return folder / name
def dsv41_layout_of(md: dict, present) -> str:
"""Which layout of a set the files are: "four" (the pinned files, and the first release's names) or "six" (ISTA-DASLab's 2026-10-27 re-split,
DSV41_ISTA6_FILES). `present(name)` says whether a file is there. Six only when its files are there and the four-shard ones are not (or are
not whole while the six are): a folder holding both whole sets is used as the pinned four-shard one, as before."""
four = [bool(present(n) or (DSV41_LEGACY_NAMES.get(n) and present(DSV41_LEGACY_NAMES[n]))) for n in md["shards"]]
six = [bool(present(n)) for n in md.get("shards6") or []]
return "six" if six and any(six) and (not any(four) or (all(six) and not all(four))) else "four"
def dsv41_parts(md: dict, present) -> tuple:
"""(layout, the names of its files, the ones that are not there) for a set's files seen through `present(name)`."""
layout = dsv41_layout_of(md, present)
names = list(md["shards6"] if layout == "six" else md["shards"])
missing = [n for n in names if not present(n) and not (layout == "four" and DSV41_LEGACY_NAMES.get(n) and present(DSV41_LEGACY_NAMES[n]))]
return layout, names, missing
def dsv41_pack_layout(pack: Path) -> str | None:
""""four" or "six" when the pack names the files of that layout (tensors.txt's `file N <name>` lines), else None. The pack folder is the same for
both layouts of a set, and a pack points into the files it was made from: one made from the other layout's files must be built again."""
try:
with (Path(pack) / "tensors.txt").open(encoding="utf-8", errors="replace") as f:
for line in f:
if not line.startswith("file "):
break
if "-of-00006" in line:
return "six"
if "-of-00004" in line:
return "four"
except OSError:
pass
return None
def dsv41_shared_dir(a, fam: dict) -> Path:
"""Where the files the official sets share (the Engram, the DSpark drafter, the metadata shard) are downloaded to: one folder, so
installing the second set does not download the 111 GB Engram again."""
return Path(a.models_dir) / (fam["tag"] + "shared")
def dsv41_find_shared(a, name: str, folder: Path, shared: Path) -> Path:
def dsv41_find_shared(a, name: str, folder: Path, shared: Path, alt: tuple = ()) -> Path:
"""The file `name` (an Engram, the drafter, ...) the person already has: in the shared folder, beside the shards, or (--gguf-dir) in the
folder above them or one folder level down from either; else where it will be downloaded to (the shared folder). The size must be the
pinned one, so a half-copied file is not taken for the file."""
names = [n for n in (name, DSV41_LEGACY_NAMES.get(name)) if n] # today's name, and the first release's (the same weights)
names = [n for n in (name, DSV41_LEGACY_NAMES.get(name), *alt) if n] # today's name, the first release's (the same weights), and `alt` (an Unsloth set's own shard 5: the same Engram data)
roots = [shared, folder]
if a.gguf_dir:
base = Path(a.gguf_dir)
@@ -8266,6 +8423,64 @@ def dsv41_smoke(exe: Path, pack: Path, lib_dirs, gpus=None, timeout: int = 5400)
return r.returncode == 0, text[-1500:].encode(enc, "replace").decode(enc, "replace")
def earlier_card_kept(prev: Path) -> bool:
"""A new copy of Strata set up like an earlier install carries over the card that install was set to. A card setup picked itself on a
DeepSeek model on AMD (strata_control/gpu_pick.py) is not a choice to carry over; every other config keeps its card exactly as before
(no AMD listing is read for them, and nothing here can stop a setup)."""
try:
earlier = json.loads(Path(prev).read_text(encoding="utf-8-sig"))
from strata_control import gpu_pick
if WIN or not gpu_pick.is_deepseek_amd(earlier):
return True
return gpu_pick.chosen_by_person(earlier, amd_usable_cards(amd_gpus()))
except Exception: # noqa: BLE001 - an unreadable file or listing: keep the card, as before
return True
def cards_named(a, hip: bool = False) -> bool:
"""Did the person name the cards (--gpu / --gpus)? On AMD setup's own pick is written into a.gpu further on (other steps rely on it), so
the AMD branch records the answer first (a._cards_named); NVIDIA keeps its way (choose_gpus sets a.gpu to the cards in use)."""
named = getattr(a, "_cards_named", None)
return bool(named) if (hip and named is not None) else bool(a.gpu is not None or a.gpus)
def amd_usable_cards(amd) -> list:
"""The AMD cards that count for a model (gpu_fit.usable_cards: no integrated chip beside a card with memory of its own, none under 8 GB)."""
from strata_control import gpu_fit
return gpu_fit.usable_cards([{**g, "vendor": "amd", "usable": amd_problem(g) is None} for g in amd])[0]
def dsv41_covered_archs(eng):
"""The AMD architectures the installed DeepSeek engine package holds code for (its manifest); None when it names none (no package yet, or one
without a list): the caller then uses the release's own rule (dsv41_fit.run_cards) and says nothing about a card it cannot know."""
try:
from strata_control import engine_component as ec
exe = dsv41_exe(Path(eng))
if exe is not None and (exe.parent / ec.MANIFEST).is_file():
archs = ec.read_manifest(exe.parent).hip_archs
if archs:
return tuple(archs)
except Exception: # noqa: BLE001 - no list: None
pass
return None
def dsv41_auto_cards(amd, eng) -> list:
"""DeepSeek on AMD with no card named: every card that counts and the engine package can run (one card on Windows: several AMD cards sharing
a model is Linux-only). The same rule as `strata launch --plan` (strata_control/dsv41_fit.run_cards). What is left out is said."""
from strata_control import dsv41_fit
# only real AMD cards (gfx...): sycl/setup_intel.py runs this same branch with its Intel cards in `amd` (arch "xe" / "i915"), and the DeepSeek
# engine has no Intel package: those keep the one card setup picked
pool = [{**g, "vendor": "amd", "usable": amd_problem(g) is None} for g in amd
if amd_problem(g) is None and str(g.get("arch") or "").startswith("gfx")]
keep, left = dsv41_fit.run_cards(pool, "amd", windows=WIN, covered=dsv41_covered_archs(eng))
byid = {g["index"]: g for g in amd}
for c in left:
if c.get("why_ignored") and not str(c["why_ignored"]).startswith("it shares the system memory"):
say(f" Not used for DeepSeek: GPU {c.get('index')} ({c.get('name')}): {c['why_ignored']}.")
return [byid[c["index"]] for c in keep]
def dsv41_cards(gpu: dict, all_cards: list | None, asked: bool) -> list:
"""The cards the model runs on: the ones asked for (--gpu / --gpus), else every card setup can use (recommend: the model is
larger than any one card, so the more graphics memory the more of its experts stay off the CPU)."""
@@ -8298,23 +8513,33 @@ def install_dsv41(a, family, gpu, hip, amd, cuda_tk, multi, port, ram, adopted,
except Exception: # noqa: BLE001 - the disk is only a hint here
room = None
rec = dsv41_fit.recommend(ram, room) # the best version this computer's memory and disk suit; warn-never-refuse
names = [m for m in names if not dsv41_fit.not_here(m)] # the Unsloth 1-bit sets: no Metal kernel yet on a Mac, untested on a DGX Spark
for i, m in enumerate(names, 1):
say(f" {i}) {fam['models'][m].get('label', m)}: {fam['models'][m]['about']}" + (" (recommended)" if m == rec else ""))
model = names[int(ask("Which version?", [str(i) for i in range(1, len(names) + 1)], str(names.index(rec) + 1), a.yes)) - 1]
if dsv41_fit.not_here(model): # Mac: IQ1_S experts have no Metal kernel yet; DGX Spark: never run (strata_control/dsv41_fit.not_here); said before anything is fetched or packed
fail(dsv41_fit.not_here(model), "choose --model IST-DAS-2.00bpw or --model IST-DAS-2.25bpw (their files work with --gguf-dir too); nothing has been downloaded")
ok(f"size: {model}")
md = fam["models"][model]
say(" Its GGUF files: " + ("IST-DAS / ISTA-DASLab (huggingface.co/ISTA-DASLab/DeepSeek-V4.1-Flash-1017-GSQ-RCO-GGUF), with the DSpark drafter and a small metadata file from Unsloth"
if md.get("recipe") == "ista" else "Unsloth (huggingface.co/unsloth/DeepSeek-V4.1-Flash-1017-GGUF)"))
ista = md.get("recipe") == "ista"
if md.get("recipe") == "ista" else
"Unsloth (huggingface.co/unsloth/DeepSeek-V4.1-Flash-1017-GGUF); the Engram table is the same data as IST-DAS's, so a copy of it already here is used"
if md.get("recipe") == "udlow" else "Unsloth (huggingface.co/unsloth/DeepSeek-V4.1-Flash-1017-GGUF)"))
udlow = md.get("recipe") == "udlow" # Unsloth's UD-IQ1_S / UD-IQ1_M: packed and checked like the IST-DAS sets; shard 5 is the Engram
ista = md.get("recipe") in ("ista", "udlow")
tag = fam["tag"] + model
cards = dsv41_cards(gpu, all_cards, bool(a.gpu is not None or a.gpus))
named = cards_named(a, hip)
cards = dsv41_cards(gpu, all_cards, named)
ctx = a.context or DSV41_CONTEXT
if dsv41_context.problem(ctx): # 8K to 1M: the model reads 1,048,576 positions (Project Maya #442); a size it cannot address stops here, before any download
fail(dsv41_context.problem(ctx), "--context takes 8192 up to 1048576 (8K, 32K, 64K, 128K, 256K, 512K, 1M); the default is 8192")
if dsv41_context.note(ctx):
warn(f"context {dsv41_context.label(ctx)}: {dsv41_context.note(ctx)}")
folder = dsv41_folder(a, fam, tag, model)
shards = [dsv41_pick(folder, n) for n in md["shards"]]
layout = dsv41_layout_of(md, lambda n: (folder / n).exists()) if (ista and a.gguf_dir) else "four"
shards = [dsv41_pick(folder, n) for n in (md["shards6"] if layout == "six" else md["shards"])]
if layout == "six":
say(f" {folder} holds ISTA-DASLab's six-shard files of 2026-10-27 ({len(shards)} files and the Engram as the sixth): they are used where they are.")
shared_dir = dsv41_shared_dir(a, fam)
# Pictures are on by default (owner rule): the engine reads them with its own vision tower, which the pack carries when the tower's weights
# (mmproj-BF16.gguf, a shared file) were there when it was made. --vision no leaves the file out and the pack text-only.
@@ -8322,7 +8547,7 @@ def install_dsv41(a, family, gpu, hip, amd, cuda_tk, multi, port, ram, adopted,
if mac and not DSV41_MAC_VISION and a.vision not in ("no", "off"):
warn("pictures are off for DeepSeek on a Mac: its picture reader has not been run on the Metal engine yet, so it reads text only")
shared_names = list(md.get("shared", [])) + ([DSV41_MMPROJ] if ds_vision == "on" else [])
shared = [dsv41_find_shared(a, n, folder, shared_dir) for n in shared_names]
shared = [dsv41_find_shared(a, n, folder, shared_dir, (md["engram_shard"],) if (udlow and n == DSV41_ISTA_ENGRAM) else ()) for n in shared_names]
mmproj_file = shared[len(md.get("shared", []))] if ds_vision == "on" else None
own_files = {f: bool(a.gguf_dir) or f.parent != shared_dir for f in shared} # a file found outside setup's own folder is the person's
@@ -8334,7 +8559,8 @@ def install_dsv41(a, family, gpu, hip, amd, cuda_tk, multi, port, ram, adopted,
for f in [*shards, *shared]) / 1e9
probe = folder if folder.exists() else (folder.parent if folder.parent.exists() else Path(a.models_dir))
free = free_gb(probe)
plan = dsv41_fit.plan([{**c, "vram_gb": float(c.get("vram_gb") or 0)} for c in cards], ram, free if left else None, ctx, model=model)
plan = dsv41_fit.plan([{**c, "vram_gb": float(c.get("vram_gb") or 0)} for c in cards], ram, free if left else None, ctx, model=model,
named=True) # `cards` are decided above (dsv41_auto_cards / the person's): the plan describes them
say()
for line in plan["lines"]:
say(" " + line)
@@ -8343,8 +8569,9 @@ def install_dsv41(a, family, gpu, hip, amd, cuda_tk, multi, port, ram, adopted,
f"is {left * 1e9 / 50e6 / 3600:.1f} hours. Files you already have: --gguf-dir DIR.")
else:
say(" Nothing to download: the files are already here.")
unchecked = [f for f in [*shards, *shared] if f.exists() and f"sha256 {fam['sha256'][f.name][1]}" not in
(f.with_name(f.name + ".done").read_text(encoding="utf-8", errors="replace") if done(f) else "")]
unchecked = [f for f in [*shards, *shared] if f.exists() and not any(
f"sha256 {x}" in (f.with_name(f.name + ".done").read_text(encoding="utf-8", errors="replace") if done(f) else "")
for x in (fam['sha256'][f.name][1], *DSV41_SHA_ALSO.get(f.name, ())))]
if unchecked:
gb = sum(fam["sha256"][f.name][0] for f in unchecked) / 1e9
say(f" The files already here ({gb:.0f} GB) are checked against their SHA-256 once, now: about {max(1, round(gb / 55))} minutes on a fast "
@@ -8395,7 +8622,8 @@ def install_dsv41(a, family, gpu, hip, amd, cuda_tk, multi, port, ram, adopted,
fail("Strata can't reach GitHub to download the DeepSeek engine.",
"Check the internet connection and run the same command again. " + dsv41_manual_steps(eng))
if exe is None and not getattr(a, "build", False):
get_dsv41_engine(eng, hip, getattr(a, "dsv41_package", None), getattr(a, "feed", None))
get_dsv41_engine(eng, hip, getattr(a, "dsv41_package", None), getattr(a, "feed", None),
archs=[c.get("arch") for c in cards if c.get("arch")] if hip else None)
exe = dsv41_exe(eng)
if (exe is not None and not getattr(a, "dsv41_package", None) and not getattr(a, "build", False)
and not (os.environ.get("STRATA_DSV41_EXE") or "").strip() and exe.parent.parent == dsv41_engine_dir(eng)):
@@ -8403,6 +8631,12 @@ def install_dsv41(a, family, gpu, hip, amd, cuda_tk, multi, port, ram, adopted,
if exe is not None and exe.parent != Path(eng):
from strata_control import dsv41_engine
lib_dirs = [*lib_dirs, *dsv41_engine.lib_dirs(exe)] # its own package, and the rocm/bin an AMD package keeps the ROCm DLLs in
if hip and exe is not None and named: # cards the person named that this package holds no code for: said, never refused
covered = dsv41_covered_archs(eng)
for c in cards if covered else []:
if c.get("arch") and c["arch"] not in covered:
warn(f"GPU {c['index']} ({c['name']}, {c['arch']}): this DeepSeek engine package holds code for {', '.join(covered)} only, so it may "
"not start on that card. Leave it out (--gpus ...) to be sure.")
if exe is None:
if os.environ.get("STRATA_VERBOSE") == "1" or "--verbose" in sys.argv:
say(" Details for developers: build it with cmake -DSTRATA_BUILD_DSV41=ON -DCMAKE_CUDA_ARCHITECTURES=<your card: 86 RTX 30, 89 RTX 40, 120 RTX 50> "
@@ -8439,7 +8673,7 @@ def install_dsv41(a, family, gpu, hip, amd, cuda_tk, multi, port, ram, adopted,
ok(f"{f.name} already here ({f.parent})")
else:
download(DSV41_SHARED[f.name] + f.name, f)
verify_sha256(f, *fam["sha256"][f.name], own=own_files[f])
verify_sha256(f, *fam["sha256"][f.name], own=own_files[f], also=DSV41_SHA_ALSO.get(f.name, ()))
ok(f"model files: {folder} ({len(shards)} shards" + (f"; shared files: {shared_dir if any(not own_files[f] for f in shared) else shared[0].parent}" if shared else "") + ")")
step(6, "preparing the model for Strata")
@@ -8450,6 +8684,10 @@ def install_dsv41(a, family, gpu, hip, amd, cuda_tk, multi, port, ram, adopted,
warn("the pack from an earlier run was made without the picture reader: building it again with it (a few minutes)")
shutil.rmtree(pack, ignore_errors=True)
have_pack = False
if have_pack and ista and dsv41_pack_layout(pack) not in (None, layout):
warn(f"the pack from an earlier run was made from the {'four' if layout == 'six' else 'six'}-shard files of this set; these are the {layout}-shard files: building it again")
shutil.rmtree(pack, ignore_errors=True)
have_pack = False
if have_pack and ista: # a pack from an earlier run is kept only when the engine finds all of its files
good, why = dsv41_pack_info(exe, pack, lib_dirs)
if not good:
@@ -8458,7 +8696,10 @@ def install_dsv41(a, family, gpu, hip, amd, cuda_tk, multi, port, ram, adopted,
have_pack = False
if not have_pack:
if ista:
run(dsv41_pack_cmd(folder, [f.name for f in shards], shared[0], shared[1], shared[2], pack, mmproj_file))
if udlow: # the set's own shard 1 carries the metadata (and the drafter's geometry); shared = [Engram, drafter]
run(dsv41_pack_cmd(folder, [f.name for f in shards], shared[0], shards[0], shared[1], pack, mmproj_file))
else:
run(dsv41_pack_cmd(folder, [f.name for f in shards], shared[0], shared[1], shared[2], pack, mmproj_file))
else:
run([sys.executable, str(ROOT / "tools" / "dsv41" / "pack_gguf.py"), "--model", str(folder), "--out", str(pack),
*(["--mmproj", str(mmproj_file)] if mmproj_file else [])])
@@ -8477,7 +8718,7 @@ def install_dsv41(a, family, gpu, hip, amd, cuda_tk, multi, port, ram, adopted,
warn("the 16-word test run was skipped (STRATA_DSV41_NO_SMOKE=1)")
else:
say(" Testing the model with a short question (a few minutes: the engine reads the first experts from the disk) ...")
good, why = dsv41_smoke(exe, pack, lib_dirs, [c["index"] for c in cards] if (a.gpu is not None or a.gpus) else None)
good, why = dsv41_smoke(exe, pack, lib_dirs, [c["index"] for c in cards] if (a.gpu is not None or a.gpus or (hip and len(cards) > 1)) else None)
if not good:
fail("the model does not answer sensibly after the preparation: " + (why.splitlines()[-1] if why else "no answer"),
f"delete {pack} and run setup again so the pack is built fresh; if the test fails again, "
@@ -8514,9 +8755,11 @@ def install_dsv41(a, family, gpu, hip, amd, cuda_tk, multi, port, ram, adopted,
if mac:
cfg["backend"] = "metal"
idx = [c["index"] for c in cards]
if len(idx) > 1 or a.gpu is not None or a.gpus:
if len(idx) > 1 or a.gpu is not None or a.gpus: # (a.gpu is also setup's own pick: one card pinned when several devices are in the PC)
cfg["gpu"] = idx if len(idx) > 1 else idx[0]
cfg["gpus_asked"] = True
if hip: # who chose them (strata_control/gpu_pick.py): the person, or setup itself
cfg["gpus_by"] = "person" if named else "setup"
if a.host:
cfg["host"] = a.host
if a.api_key:
@@ -8953,6 +9196,11 @@ def main() -> int:
help="several GPUs: run N independent engines, each on its own equal group of the chosen cards (2 "
"replicas on 4 cards = two 2-card engines), requests spread over them (opt-in; more total "
"throughput for a model that fits fewer cards; each replica holds its own copy of the model)")
ap.add_argument("--replica-profile", action="store_true", default=False,
help="two or more GPUs, Qwen3.8 IQ3_S/IQ3_XXS/Q2_0: one engine per card behind the server instead of one "
"layer split, the experts shared in RAM, the batch switches on (opt-in; about 1.8 times the "
"throughput at 8-16 requests at once on 4 Radeon AI PRO R9700; needs about 61 GB of RAM for 2 "
"engines and 66 GB for 4; a layer split you chose, replicas or --parallel always win)")
ap.add_argument("--kv-streaming", choices=["auto", "on", "off"], default="auto",
help="from a 64K context: keep the KV cache in RAM and only the attention's window in VRAM (more "
"experts fit on the GPU); auto: when the RAM has room for it")
@@ -9054,7 +9302,8 @@ def main() -> int:
a.gpus = a.gpus or ",".join(str(g) for g in ch["gpu"])
a.layer_split = a.layer_split or ch.get("layer_split")
else:
a.gpu = a.gpu if a.gpu is not None else ch.get("gpu")
if earlier_card_kept(prev):
a.gpu = a.gpu if a.gpu is not None else ch.get("gpu")
a.yes = True
global GPU_PICK, OLD_GPUS
# starting an installed model: --gpus 0,2 (or all) saves those cards for it and starts on them (it used to start
@@ -9168,6 +9417,7 @@ def main() -> int:
"your user in the render and video groups)")
if not usable:
fail("no AMD GPU Strata can use", f"the AMD backend runs on {AMD_CARDS}")
a._cards_named = bool(a.gpu is not None or a.gpus) # before setup's own pick is written into a.gpu below
if a.gpus: # a layer split across these cards, the first one the main
chosen = amd_parse_gpus(a.gpus, amd)
gpu = chosen[0]
@@ -9363,7 +9613,7 @@ def main() -> int:
if family in OWN_PATH_FAMILIES: # Qwen3.6: its own engine path, its own steps 2-7
return install_own_path(a, family, gpu, hip, amd, cuda_tk, multi, port, ram, adopted)
if family in MULTI_FILE_FAMILIES: # DeepSeek-V4.1-Flash: nine shards, its own engine, several cards
all_cards = amd_ok if hip else [g for g in found if gpu_problem(g) is None]
all_cards = (None if (WIN or getattr(a, "_cards_named", False)) else dsv41_auto_cards(amd, ROOT / "engine")) if hip else [g for g in found if gpu_problem(g) is None]
gpu = {**gpu, "cards": chosen}
return install_dsv41(a, family, gpu, hip, amd, cuda_tk, multi, port, ram, adopted, all_cards)
fam = FAMILIES[family]
@@ -10095,7 +10345,22 @@ def main() -> int:
cfg["gpu_order"] = "as_given"
ok(f"layer split across GPUs {multi} ({cfg['layer_split']})")
recommend_remote_expert_opt(cfg, off=a.no_remote_expert_opt)
if hip and a.layer_split in (None, "auto"): # four Radeon AI PRO R9700 cards and Qwen3.8 IQ3_S: the measured serving profile
replica_done = False
if a.replicas is None and a.layer_split in (None, "auto") and (getattr(a, "replica_profile", False) or
os.environ.get("STRATA_REPLICA_PROFILE")):
try: # the opt-in replica profile (strata_control/replica_profile.py)
from strata_control import replica_profile
if getattr(a, "replica_profile", False):
cfg["replica_profile"] = True
lines = replica_profile.apply([{"name": c.get("name"), "vram_gb": c.get("vram_gb"), "vendor": c.get("vendor"),
"index": c.get("index")} for c in chosen], cfg, float(ram),
parallel_given=a.parallel is not None)
replica_done = bool(cfg.get("replicas"))
for line in lines:
(ok if replica_done else say)(line)
except Exception as e: # noqa: BLE001 - a profile is never a reason to stop setup
say(f" (the replica profile was not applied: {e})")
if hip and a.layer_split in (None, "auto") and not replica_done: # four Radeon AI PRO R9700 cards and Qwen3.8 IQ3_S: the measured serving profile
try: # (strata_control/amd_four_card.py); what the person chose always wins
from strata_control import amd_four_card
for line in amd_four_card.apply([{"name": c.get("name"), "vram_gb": c.get("vram_gb"), "vendor": "amd"} for c in chosen],
+26
View File
@@ -102,6 +102,13 @@ bool igpu_prompt_set_enabled(const char* gcn_arch, bool windows) {
const char* e = std::getenv("STRATA_IGPU_PROMPT_SET");
if (e != nullptr && e[0] == '1') return true;
if (e != nullptr && e[0] == '0') return false;
// 0.2.8: Strix Halo (gfx1151) gets the matrix-core prompt set on Windows by default. Measured on the strix box
// (Qwen3.8-Flash-Next IQ3_S, engine 0.1.52, ids prompts of 4,095 / 8,191 tokens, interleaved pairs, one engine at a
// time): 4K 690 -> 1,090 and 8K 785 -> 1,183 on the f-024-strix-pp branch (0.2.3 engine, the same switches as
// environment variables), i.e. x1.58 / x1.51; the first-token KL against the accepted HC_Q8 bar was 0.0088 mean
// (0.0228 allowed), top-1 the same on every prompt. Linux gfx1151 stays opt-in until it is measured there; gfx1103
// keeps its own rule below.
if (windows && strata::kernels::gfx_arch_is(gcn_arch, "gfx1151")) return true;
return kIgpuPromptSetDefaultOn && !windows && strata::kernels::gfx_arch_is(gcn_arch, "gfx1103");
}
@@ -135,6 +142,25 @@ std::vector<std::pair<std::string, std::string>> igpu_default_env(const char* gc
{"STRATA_SELECT_WMMA", "1"}, // the block scorer on matrix cores
{"STRATA_HC_UPMIX", "1"}, // the hyper-connection up projection with its epilogue
};
// gfx1151 (Strix Halo) on Windows: SELECT_WMMA is in since 0.2.9 (x1.008 at 8K, x1.037 at 32K, x1.077 at 64K), and PF_FUSED (the
// fused int8 gate/up prompt experts, #136) measured x1.12 there and joins the set. The 780M and the 890M keep the
// table above exactly as measured on them.
if (win && strata::kernels::gfx_arch_is(gcn_arch, "gfx1151")) {
t = {
{"STRATA_HIP_WMMA", "1"},
{"STRATA_PF_GEMM", "1"},
{"STRATA_PA_FAST", "1"},
{"STRATA_HC_UPMIX", "1"},
{"STRATA_PF_FUSED", "1"},
{"STRATA_SELECT_WMMA", "1"}, // 0.2.9: the block scorer on matrix cores; not bitwise (about 10 of 2,051 selected cells flip at the cutoff
// for queries past ~5K tokens), x1.008 at 8K, x1.037 at 32K, x1.077 at 64K (STRIX-Q-RESULTS.md)
// 0.2.9 (owner, 10-30: a consistent Strix Halo speed gain goes on by default): the prompt experts of every covered
// format on the F16 matrix-core kernels (the Q8_0 down layers of the Unsloth files included). Windows gfx1151,
// interleaved x3, same 16 greedy ids: IQ3_S 4K 1,187 -> 1,296 and 8K 1,285 -> 1,399 tok/s (x1.09), UD-IQ4_XS 4K 1,119 ->
// 1,221 and 8K 1,180 -> 1,270; decode unchanged. The rounding differs from the int8 kernels (KL is information only).
{"STRATA_PF_F16", "1"},
};
}
return t;
}
+68 -4
View File
@@ -182,7 +182,10 @@ int main() {
for (const Mock& m : mocks) {
const bool want = m.expect_on && std::string(m.gcn).rfind("gfx1103", 0) == 0;
#if defined(_WIN32)
CHECK(igpu_default_env(m.gcn, m.integrated).empty());
// 0.2.8: Strix Halo (gfx1151, integrated) gets the matrix-core prompt set on Windows; the 780M and the 890M are
// not measured there and stay off
const bool win_on = m.expect_on && m.integrated && std::string(m.gcn).rfind("gfx1151", 0) == 0;
CHECK(igpu_default_env(m.gcn, m.integrated).size() == (win_on ? 7u : 0u));
#else
CHECK(igpu_default_env(m.gcn, m.integrated).size() == (want ? 5u : 0u));
#endif
@@ -212,14 +215,21 @@ int main() {
const auto t = igpu_default_env(m.gcn, m.integrated);
CHECK(t.empty() == !m.expect_on);
if (m.expect_on) {
CHECK(t.size() == 5);
const char* want[] = {"STRATA_HIP_WMMA", "STRATA_PF_GEMM", "STRATA_PA_FAST", "STRATA_SELECT_WMMA", "STRATA_HC_UPMIX"};
bool halo_win = false;
#if defined(_WIN32)
halo_win = std::string(m.gcn).rfind("gfx1151", 0) == 0;
#endif
CHECK(t.size() == (halo_win ? 7u : 5u));
std::vector<const char*> want = {"STRATA_HIP_WMMA", "STRATA_PF_GEMM", "STRATA_PA_FAST", "STRATA_HC_UPMIX"};
want.push_back("STRATA_SELECT_WMMA");
if (halo_win) { want.push_back("STRATA_PF_FUSED"); want.push_back("STRATA_PF_F16"); }
for (const char* w : want) {
bool found = false;
for (const auto& kv : t) found = found || (kv.first == w && kv.second == "1");
CHECK(found);
}
for (const auto& kv : t) CHECK(kv.first != "STRATA_PF_FUSED" && kv.first != "STRATA_HC_Q8" && kv.first != "STRATA_PF_HCDOWN");
for (const auto& kv : t) CHECK(kv.first != "STRATA_HC_Q8" && kv.first != "STRATA_PF_HCDOWN");
if (!halo_win) { for (const auto& kv : t) CHECK(kv.first != "STRATA_PF_FUSED"); }
}
if (!m.expect_on) std::printf(" (not on) %s\n", m.name);
}
@@ -260,6 +270,60 @@ int main() {
CHECK(std::getenv("STRATA_HC_UPMIX") != nullptr && std::string(std::getenv("STRATA_HC_UPMIX")) == "0");
for (const char* k : five) unset(k);
unset("STRATA_IGPU_PROMPT_SET");
#if defined(_WIN32)
// 0.2.8: Strix Halo (gfx1151) on Windows gets the matrix-core prompt set with nothing asked. Measured on the strix
// box (Qwen3.8-Flash-Next IQ3_S, engine 0.1.52, 4K / 8K ids prompts): 4K 690 -> 1,090, 8K 785 -> 1,183 (x1.58 /
// x1.51), KL 0.0088 mean against the HC_Q8 bar of 0.0228. The set is HIP_WMMA + PF_GEMM + PA_FAST + HC_UPMIX +
// PF_FUSED (0.2.9 adds SELECT_WMMA and PF_F16 to the Windows gfx1151 set), and the 780M / 890M keep exactly what was measured on them.
unset("STRATA_IGPU_DEFAULTS");
unset("STRATA_GFX1151_DEFAULTS"); // the 19-switch gfx1151 table is live in this block (the checks above turned it off)
CHECK(igpu_prompt_set_enabled("gfx1151", true));
CHECK(igpu_prompt_set_enabled("gfx1151:sramecc-:xnack-", true));
CHECK(!igpu_prompt_set_enabled("gfx1151", false)); // Linux gfx1151: still opt-in
CHECK(!igpu_prompt_set_enabled("gfx1150", true));
CHECK(!igpu_prompt_set_enabled("gfx1103", true));
{
const auto t = igpu_default_env("gfx1151", true);
CHECK(t.size() == 7);
auto has = [&t](const char* k) {
for (const auto& kv : t) if (kv.first == k) return kv.second == "1";
return false;
};
CHECK(has("STRATA_HIP_WMMA") && has("STRATA_PF_GEMM") && has("STRATA_PA_FAST") && has("STRATA_HC_UPMIX") &&
has("STRATA_PF_FUSED") && has("STRATA_SELECT_WMMA") && has("STRATA_PF_F16"));
CHECK(igpu_default_env("gfx1151", false).empty()); // a gfx1151 reported as a discrete card
CHECK(igpu_default_env("gfx1150", true).empty()); // 890M: unchanged
CHECK(igpu_default_env("gfx1103", true).empty()); // 780M on Windows: unchanged
}
{
const char* halo[] = {"STRATA_HIP_WMMA", "STRATA_PF_GEMM", "STRATA_PA_FAST", "STRATA_HC_UPMIX", "STRATA_PF_FUSED",
"STRATA_SELECT_WMMA", "STRATA_PF_F16"};
for (const char* k : halo) unset(k);
const auto set = apply_arch_defaults("gfx1151", true);
for (const char* k : halo) {
bool in = false;
for (const auto& n : set) in = in || n == k;
CHECK(in);
CHECK(std::getenv(k) != nullptr && std::string(std::getenv(k)) == "1");
}
// a switch the user set (even to 0) is kept, and the whole set turns off with either switch
for (const char* k : halo) unset(k);
put("STRATA_HC_UPMIX", "0");
const auto set2 = apply_arch_defaults("gfx1151", true);
CHECK(std::getenv("STRATA_HC_UPMIX") != nullptr && std::string(std::getenv("STRATA_HC_UPMIX")) == "0");
for (const auto& n : set2) CHECK(n != "STRATA_HC_UPMIX");
CHECK(std::getenv("STRATA_PF_FUSED") != nullptr);
unset("STRATA_HC_UPMIX");
for (const char* k : halo) unset(k);
put("STRATA_IGPU_PROMPT_SET", "0");
CHECK(igpu_default_env("gfx1151", true).empty());
unset("STRATA_IGPU_PROMPT_SET");
put("STRATA_IGPU_DEFAULTS", "0");
CHECK(igpu_default_env("gfx1151", true).empty());
unset("STRATA_IGPU_DEFAULTS");
put("STRATA_GFX1151_DEFAULTS", "0"); // back to the state this block found (the 026 checks below rely on it)
}
#endif
// 021-minipc: UMA mode is NOT a default (slower, resets not fixed): off with nothing asked; the logic below runs with on=true
CHECK(!kIgpuUmaArenaDefaultOn);
CHECK(!igpu_uma_arena_default("gfx1103", true, false));
+5 -5
View File
@@ -162,8 +162,8 @@ bool conversation_checkpoint_validate(const ConversationCheckpoint& c, const Ses
} // namespace strata::core
namespace strata::metal_engine {
// the running state lives in the Metal decode window (metal/src/engine/window.cpp), not in the session's buffers
bool window_checkpoint_save(strata::core::ConversationCheckpoint& c, std::string& error);
bool window_checkpoint_restore(const strata::core::ConversationCheckpoint& c, std::string& error);
bool window_checkpoint_save(strata::core::ConversationCheckpoint& c, const strata::core::SessionState* ss, std::string& error);
bool window_checkpoint_restore(const strata::core::ConversationCheckpoint& c, const strata::core::SessionState* ss, std::string& error);
} // namespace strata::metal_engine
namespace strata::core {
#endif
@@ -289,8 +289,8 @@ bool checkpoint_save(ConversationCheckpoint& c, const SessionState& ss, const Mo
if (std::getenv("STRATA_PARK_TRACE")) std::fprintf(stderr, "strata parktrace: save join %.2f ms\n",
std::chrono::duration<double, std::milli>(std::chrono::steady_clock::now() - j0).count());
#if defined(STRATA_GPU_METAL)
(void) ss; (void) g;
return strata::metal_engine::window_checkpoint_save(c, error);
(void) g;
return strata::metal_engine::window_checkpoint_save(c, &ss, error);
#endif
ConversationStateSizes z;
if (!checkpoint_targets(ss, g, c.ids.size(), z, error)) return false;
@@ -401,7 +401,7 @@ bool conversation_checkpoint_restore(const ConversationCheckpoint& c, SessionSta
if (hit == nullptr) conversation_checkpoint_join(c.gdn.data());
#if defined(STRATA_GPU_METAL)
(void) g;
if (!strata::metal_engine::window_checkpoint_restore(c, error)) return false;
if (!strata::metal_engine::window_checkpoint_restore(c, &ss, error)) return false;
ss.ple_prev[0] = c.ids.size() >= 2 ? c.ids[c.ids.size() - 2] : -1;
ss.ple_prev[1] = c.ids.size() >= 1 ? c.ids[c.ids.size() - 1] : -1;
return true;
+71 -1
View File
@@ -20,6 +20,10 @@
#include "strata/kernels/cpu/pool.hpp"
#include <cuda_runtime.h>
#ifndef _WIN32
#include <signal.h>
#include <unistd.h>
#endif
#include <algorithm>
#include <atomic>
@@ -72,6 +76,27 @@
#endif
#endif
// The pid of this process and whether a pid still exists (the shared arena's reader election, both OSes)
static uint64_t arena_this_pid() {
#ifdef _WIN32
return (uint64_t) GetCurrentProcessId();
#else
return (uint64_t) getpid();
#endif
}
static bool arena_pid_alive(uint64_t pid) {
#ifdef _WIN32
HANDLE h = OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, FALSE, (DWORD) pid);
if (h == nullptr) return GetLastError() == ERROR_ACCESS_DENIED; // (denied: it exists; invalid parameter: it does not)
DWORD code = 0;
const bool alive = GetExitCodeProcess(h, &code) && code == STILL_ACTIVE;
CloseHandle(h);
return alive;
#else
return !(kill((pid_t) pid, 0) != 0 && errno == ESRCH);
#endif
}
namespace strata::core {
namespace detail {
@@ -6000,6 +6025,43 @@ bool ArenaExpertSource::open(const std::string& pack_dir, int64_t n_layers, int6
const bool unbuffered = experts_unbuffered(files, want + (uint64_t) blob, why);
const int readers = unbuffered ? std::max(threads, 16) : threads; // 16 keep a PCIe 5 drive's queue full
LoadStats st;
// A shared arena (--shared-expert-arena, the replicas): ONE engine reads the experts into it, the others wait and skip the
// read. The state word in the file's header says who: 0 nobody, a pid loading, ~0 filled. An engine that finds a
// pid that is gone takes the load over (the previous one died mid-read: the arena is read again from the start), so a
// dead loader never leaves the others waiting, and an engine that waits too long loads it itself.
constexpr uint64_t kArenaFilled = ~uint64_t{0};
std::atomic<uint64_t>* arena_state = padded_ ? nullptr : a->shared_state;
bool arena_leader = false, arena_skip = false;
if (arena_state != nullptr) {
const uint64_t me = arena_this_pid();
const auto t_begin = std::chrono::steady_clock::now();
bool said = false;
for (;;) {
uint64_t cur = arena_state->load(std::memory_order_acquire);
if (cur == kArenaFilled) { arena_skip = true; break; }
if (cur == 0 || cur == me) {
if (arena_state->compare_exchange_strong(cur, me, std::memory_order_acq_rel)) { arena_leader = true; break; }
continue;
}
const bool alive = arena_pid_alive(cur);
if (!alive) {
if (arena_state->compare_exchange_strong(cur, me, std::memory_order_acq_rel)) {
std::fprintf(stderr, "strata generate: shared expert arena: engine %llu died while reading it; reading it again\n",
(unsigned long long) cur);
arena_leader = true;
break;
}
continue;
}
if (std::chrono::steady_clock::now() - t_begin > std::chrono::minutes(45)) break; // (loads it on its own)
if (!said) {
said = true;
std::fprintf(stderr, "strata generate: shared expert arena: engine %llu is reading the experts; waiting for it "
"instead of reading them again\n", (unsigned long long) cur);
}
std::this_thread::sleep_for(std::chrono::milliseconds(200));
}
}
std::atomic<int> ready{0};
std::thread reg;
if (deferred) {
@@ -6011,7 +6073,12 @@ bool ArenaExpertSource::open(const std::string& pack_dir, int64_t n_layers, int6
});
}
const std::atomic<int>* rp = deferred ? &ready : nullptr;
if (from_gguf) {
if (arena_skip) {
st.ok = true;
st.bytes = want;
st.seconds = 0.0;
std::fprintf(stderr, "strata generate: shared expert arena: already filled by another engine; the read is skipped\n");
} else if (from_gguf) {
st = load_experts_gguf(gguf_, a->data(), lay, readers, unbuffered, rp, padded_ ? &pad_ : nullptr);
} else {
if (unbuffered && !padded_) st = load_experts_direct(path, a->data(), loff, lbytes, readers, /*chunk=*/8u << 20, rp);
@@ -6022,15 +6089,18 @@ bool ArenaExpertSource::open(const std::string& pack_dir, int64_t n_layers, int6
std::fprintf(stderr, "strata generate: expert arena read %s (%s)\n", unbuffered ? "unbuffered" : "through the file cache",
why.c_str());
if (!st.ok) {
if (arena_leader) arena_state->store(0, std::memory_order_release); // (somebody else may try)
delete a;
err = "ArenaExpertSource: the expert load was refused: " + (st.error.empty() ? std::string("unknown") : st.error);
return false;
}
if (st.bytes != want) {
if (arena_leader) arena_state->store(0, std::memory_order_release);
delete a;
err = "ArenaExpertSource: the load read " + std::to_string(st.bytes) + " B of " + std::to_string(want);
return false;
}
if (arena_leader) arena_state->store(kArenaFilled, std::memory_order_release); // the others may use (and skip reading) it
if (std::getenv("STRATA_VERIFY_ARENA") != nullptr) { // tests: the loaded arena's checksum, any loader
std::fprintf(stderr, "strata generate: expert arena checksum %016llx (%.2f GiB)\n",
(unsigned long long) fnv1a64(a->data(), want), (double) want / (1ull << 30));
+218 -3
View File
@@ -2,6 +2,7 @@
#include "strata/core/pinned.hpp"
#include "strata/platform/memory.hpp"
#include "strata/core/pin_budget.hpp"
#include <cstddef>
#include <cuda_runtime.h>
@@ -32,6 +33,9 @@
#include <fcntl.h>
#include <sys/mman.h>
#include <sys/stat.h>
#ifdef __linux__
#include <sys/vfs.h>
#endif
#include <unistd.h>
#include <linux/mman.h>
#ifndef MAP_HUGE_2MB
@@ -56,6 +60,51 @@ struct SharedArenaHeader {
};
static_assert(sizeof(SharedArenaHeader) <= kSharedArenaHeaderBytes);
#ifdef _WIN32
// Shared-section views: MapViewOfFile address -> the section handle, so release() unmaps and closes instead of VirtualFree.
std::mutex g_views_mu;
std::map<void*, HANDLE> g_views;
#endif
#ifndef _WIN32
// MemAvailable and, in a cgroup v2 group with a limit, the room left under it; ~0 when neither can be read
uint64_t free_ram_room() {
uint64_t room = ~uint64_t{0};
if (std::FILE* f = std::fopen("/proc/meminfo", "r")) {
char line[256];
while (std::fgets(line, sizeof line, f) != nullptr) {
unsigned long long kb = 0;
if (std::sscanf(line, "MemAvailable: %llu kB", &kb) == 1) { room = (uint64_t) kb * 1024u; break; }
}
std::fclose(f);
}
std::string cg;
if (std::FILE* f = std::fopen("/proc/self/cgroup", "r")) {
char line[512];
while (std::fgets(line, sizeof line, f) != nullptr)
if (std::strncmp(line, "0::", 3) == 0) { cg = line + 3; break; }
std::fclose(f);
}
while (!cg.empty() && (cg.back() == '\n' || cg.back() == '\r')) cg.pop_back();
if (!cg.empty()) {
unsigned long long lim = 0, cur = 0;
bool have_lim = false;
if (std::FILE* f = std::fopen(("/sys/fs/cgroup" + cg + "/memory.max").c_str(), "r")) {
have_lim = std::fscanf(f, "%llu", &lim) == 1; // ("max" does not parse: no limit)
std::fclose(f);
}
if (have_lim) {
if (std::FILE* f = std::fopen(("/sys/fs/cgroup" + cg + "/memory.current").c_str(), "r")) {
if (std::fscanf(f, "%llu", &cur) != 1) cur = 0;
std::fclose(f);
}
room = std::min<uint64_t>(room, lim > cur ? (uint64_t) (lim - cur) : 0);
}
}
return room;
}
#endif
#ifndef _WIN32
// bytes in free blocks of 2 MiB or more (/proc/buddyinfo, order >= 9): what MADV_HUGEPAGE can get without compaction
uint64_t free_huge_bytes() {
@@ -96,8 +145,102 @@ void* reserve(uint64_t bytes, PageBacking& got, std::string& note, const std::st
#ifdef _WIN32
if (!shared_file.empty()) {
note = "shared-file arena backing is not implemented on Windows";
return nullptr;
// The shared arena on Windows is a named, pagefile-backed section (CreateFileMappingW) that every engine of the
// replicas opens by name: "win:Local\\strata-replicas-<server pid>" (a name without the prefix is used as it is).
// The first engine creates it at the full size, the others open it; it lives while any process holds a handle or
// a view, so a replica that restarts joins the same pages. The first 4 KiB are the same header as on Linux (the
// pack hash, the size, the load state word); a new section is all zeros, so "no magic" means "nobody has been
// here" - two engines that both find it empty write the same header.
if (shared_pack_hash == 0) {
note = "shared arena requires a nonzero pack hash";
return nullptr;
}
if (bytes > UINT64_MAX - kSharedArenaHeaderBytes) {
note = "shared arena size overflows its header";
return nullptr;
}
const uint64_t file_bytes = bytes + kSharedArenaHeaderBytes;
std::string nm = shared_file.rfind("win:", 0) == 0 ? shared_file.substr(4) : shared_file;
std::wstring wname(nm.begin(), nm.end());
// the section is committed memory (RAM plus pagefile) from the moment it exists: say so before taking it
MEMORYSTATUSEX ms{};
ms.dwLength = sizeof ms;
if (GlobalMemoryStatusEx(&ms) && file_bytes + (2ull << 30) > ms.ullAvailPageFile) {
HANDLE probe = OpenFileMappingW(FILE_MAP_ALL_ACCESS, FALSE, wname.c_str()); // (already there: nothing new is committed)
if (probe == nullptr) {
note = "not enough free memory (RAM + pagefile) for the shared arena " + nm + ": it needs " +
std::to_string((unsigned long long) (file_bytes >> 20)) + " MiB and " +
std::to_string((unsigned long long) (ms.ullAvailPageFile >> 20)) + " MiB are available (2 GiB are kept back)";
return nullptr;
}
CloseHandle(probe);
}
HANDLE h = CreateFileMappingW(INVALID_HANDLE_VALUE, nullptr, PAGE_READWRITE, (DWORD) (file_bytes >> 32),
(DWORD) (file_bytes & 0xffffffffu), wname.c_str());
if (h == nullptr) {
note = "cannot create shared arena section " + nm + ": Windows error " + std::to_string((unsigned long long) GetLastError());
return nullptr;
}
void* view = MapViewOfFile(h, FILE_MAP_ALL_ACCESS, 0, 0, 0); // (0: the whole section, as big as it was created)
if (view == nullptr) {
const DWORD e = GetLastError();
CloseHandle(h);
note = "cannot map shared arena section " + nm + ": Windows error " + std::to_string((unsigned long long) e);
return nullptr;
}
MEMORY_BASIC_INFORMATION mbi{};
if (VirtualQuery(view, &mbi, sizeof mbi) == 0 || (uint64_t) mbi.RegionSize < file_bytes) {
UnmapViewOfFile(view);
CloseHandle(h);
note = "shared arena section " + nm + " is smaller than the " + std::to_string((unsigned long long) file_bytes) +
" B this engine needs (including its header)";
return nullptr;
}
auto* hdr = static_cast<SharedArenaHeader*>(view);
bool empty = true;
for (char c : hdr->magic) empty = empty && c == 0;
if (empty) {
SharedArenaHeader fresh{};
std::memcpy(fresh.magic, kSharedArenaMagic, sizeof fresh.magic);
fresh.version = 1;
fresh.header_bytes = (uint32_t) kSharedArenaHeaderBytes;
fresh.arena_bytes = bytes;
fresh.pack_hash = shared_pack_hash;
// fields after the magic first, the magic last: an engine that sees the magic sees a complete header (the
// state word, reserved[0], stays zero)
std::memcpy(reinterpret_cast<char*>(hdr) + sizeof fresh.magic, reinterpret_cast<char*>(&fresh) + sizeof fresh.magic,
sizeof fresh - sizeof fresh.magic);
MemoryBarrier();
std::memcpy(hdr->magic, fresh.magic, sizeof fresh.magic);
} else {
// (an engine that arrives in the instant between another one's header fields and its magic never sees a
// half header: the magic is written last)
if (std::memcmp(hdr->magic, kSharedArenaMagic, sizeof hdr->magic) != 0 || hdr->version != 1 ||
hdr->header_bytes != kSharedArenaHeaderBytes || hdr->arena_bytes != bytes) {
UnmapViewOfFile(view);
CloseHandle(h);
note = "shared arena section " + nm + " has an incompatible or missing header";
return nullptr;
}
if (hdr->pack_hash != shared_pack_hash) {
char b[256];
std::snprintf(b, sizeof b, "shared arena %s was written for pack hash %016llx, expected %016llx", nm.c_str(),
(unsigned long long) hdr->pack_hash, (unsigned long long) shared_pack_hash);
UnmapViewOfFile(view);
CloseHandle(h);
note = b;
return nullptr;
}
}
{
std::lock_guard<std::mutex> lk(g_views_mu);
g_views[view] = h;
}
mapping_base = view;
mapping_bytes = file_bytes;
got = PageBacking::NormalPages;
note = "shared section " + nm + " (pack hash checked)";
return static_cast<uint8_t*>(view) + kSharedArenaHeaderBytes;
}
// MEM_LARGE_PAGES needs SeLockMemoryPrivilege. Having it assigned to the account is not enough: the
// PROCESS must enable it in its own token (AdjustTokenPrivileges) before VirtualAlloc, or the call fails.
@@ -200,7 +343,27 @@ void* reserve(uint64_t bytes, PageBacking& got, std::string& note, const std::st
return nullptr;
}
SharedArenaHeader hdr{};
const ssize_t got_header = pread(fd, &hdr, sizeof hdr, 0);
ssize_t got_header = pread(fd, &hdr, sizeof hdr, 0);
// the creator sizes the file first and writes the header right after: an engine that starts in that gap waits
// for it (up to 10 s) instead of calling the file broken
for (int w = 0; w < 100 && got_header == (ssize_t) sizeof hdr &&
std::memcmp(hdr.magic, kSharedArenaMagic, sizeof hdr.magic) != 0 &&
hdr.magic[0] == 0 && hdr.magic[1] == 0; ++w) {
usleep(100 * 1000);
got_header = pread(fd, &hdr, sizeof hdr, 0);
}
if (got_header == (ssize_t) sizeof hdr && std::memcmp(hdr.magic, kSharedArenaMagic, sizeof hdr.magic) != 0 &&
hdr.magic[0] == 0 && hdr.magic[1] == 0) {
// still no header after 10 s: the engine that sized the file died before writing it. Every engine
// fills the arena itself, so nothing in the file is worth keeping: write the header as the creator would
std::memset(&hdr, 0, sizeof hdr);
std::memcpy(hdr.magic, kSharedArenaMagic, sizeof hdr.magic);
hdr.version = 1;
hdr.header_bytes = (uint32_t) kSharedArenaHeaderBytes;
hdr.arena_bytes = bytes;
hdr.pack_hash = shared_pack_hash;
got_header = pwrite(fd, &hdr, sizeof hdr, 0);
}
if (got_header != (ssize_t) sizeof hdr ||
std::memcmp(hdr.magic, kSharedArenaMagic, sizeof hdr.magic) != 0 ||
hdr.version != 1 || hdr.header_bytes != kSharedArenaHeaderBytes || hdr.arena_bytes != bytes) {
@@ -220,6 +383,38 @@ void* reserve(uint64_t bytes, PageBacking& got, std::string& note, const std::st
}
}
// tmpfs: take the pages now (ftruncate only sets the length). With /dev/shm too small the load would die with a bus
// error part-way; here a full tmpfs is a plain error at start, for every engine that maps the file (pages already
// taken cost nothing), and the server then starts its replicas with private copies. A file on a disk stays sparse.
#ifdef __linux__
{
struct statfs sf{};
if (fstatfs(fd, &sf) == 0 && (unsigned long) sf.f_type == 0x01021994ul) {
// a memfd (or any tmpfs file not capped by a mount size) would take the RAM and then the OOM killer: check the
// free RAM and the cgroup's room for the pages that are not there yet before taking them
struct stat st2{};
const uint64_t have = fstat(fd, &st2) == 0 ? (uint64_t) st2.st_blocks * 512u : 0;
const uint64_t need = file_bytes > have ? file_bytes - have : 0;
if (need > 0) {
const uint64_t room = free_ram_room();
if (room != ~uint64_t{0} && need + (2ull << 30) > room) {
close(fd);
note = "not enough free RAM for the shared arena " + shared_file + ": it needs " +
std::to_string((unsigned long long) (need >> 20)) + " MiB more and " +
std::to_string((unsigned long long) (room >> 20)) + " MiB are free (2 GiB are kept back)";
return nullptr;
}
}
const int fe = posix_fallocate(fd, 0, (off_t) file_bytes);
if (fe != 0 && fe != EOPNOTSUPP && fe != EINVAL) {
close(fd);
note = "not enough room for the shared arena " + shared_file + " (" +
std::to_string((unsigned long long) (file_bytes >> 20)) + " MiB): " + std::strerror(fe);
return nullptr;
}
}
}
#endif
void* map = mmap(nullptr, (size_t) file_bytes, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
const int e = errno;
close(fd);
@@ -322,6 +517,20 @@ void release(void* p, uint64_t bytes) {
if (!p) return;
#ifdef _WIN32
(void) bytes;
HANDLE section = nullptr;
{
std::lock_guard<std::mutex> lk(g_views_mu);
auto it = g_views.find(p);
if (it != g_views.end()) {
section = it->second;
g_views.erase(it);
}
}
if (section != nullptr) { // a shared-section view: the pages stay while another engine has the section
UnmapViewOfFile(p);
CloseHandle(section);
return;
}
VirtualFree(p, 0, MEM_RELEASE);
#else
munmap(p, bytes);
@@ -410,6 +619,9 @@ PinnedArena::PinnedArena(uint64_t bytes, const std::vector<uint64_t>& bounds,
uint64_t shared_pack_hash) : capacity(bytes) {
if (bytes == 0) return;
base = reserve(bytes, backing, note, shared_file, shared_pack_hash, mapping_base, mapping_bytes);
if (!shared_file.empty() && base != nullptr && mapping_base != nullptr)
shared_state = reinterpret_cast<std::atomic<uint64_t>*>(static_cast<char*>(mapping_base) +
offsetof(SharedArenaHeader, reserved));
if (base != nullptr && mapping_base == nullptr) {
mapping_base = base;
mapping_bytes = bytes;
@@ -529,6 +741,9 @@ PinnedArena::PinnedArena(uint64_t bytes, ByNeed, const std::vector<std::pair<uin
by_need = true;
if (bytes == 0) return;
base = reserve(bytes, backing, note, shared_file, shared_pack_hash, mapping_base, mapping_bytes);
if (!shared_file.empty() && base != nullptr && mapping_base != nullptr)
shared_state = reinterpret_cast<std::atomic<uint64_t>*>(static_cast<char*>(mapping_base) +
offsetof(SharedArenaHeader, reserved));
if (base != nullptr && mapping_base == nullptr) {
mapping_base = base;
mapping_bytes = bytes;
+107
View File
@@ -0,0 +1,107 @@
// Windows selftest for the named-section backing of PinnedArena (the replicas' shared expert arena).
// Parent: two arenas on one section name (same bytes, header refused for another pack / a bigger size, the load state word),
// then a CHILD PROCESS that opens the same section, reads what the parent wrote and writes back; the parent closes its first
// arena while the second still holds the section (the pages must stay), and a third arena joins the live section. After the
// last close, a new arena with the same name starts empty (the section is gone with its last handle).
#ifdef _WIN32
#include "strata/core/pinned.hpp"
#include <windows.h>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <string>
#include <vector>
namespace {
constexpr uint64_t kBytes = 64u << 10;
constexpr uint64_t kPackHash = 0x0123456789abcdefull;
} // namespace
int main(int argc, char** argv) {
const std::vector<uint64_t> no_bounds;
if (argc >= 3 && std::strcmp(argv[1], "child") == 0) {
const std::string name = argv[2];
strata::core::PinnedArena a(kBytes, no_bounds, 0, name, kPackHash);
if (!a.valid()) { std::fprintf(stderr, "child: cannot open: %s\n", a.note.c_str()); return 2; }
if (a.data()[123] != 0x5a) { std::fprintf(stderr, "child: did not see the parent's byte\n"); return 3; }
if (a.shared_state == nullptr || a.shared_state->load() != 4242u) { std::fprintf(stderr, "child: state word is not the parent's\n"); return 4; }
a.data()[456] = 0xa5;
a.shared_state->store(~uint64_t{0}); // (filled)
return 0;
}
int fail = 0;
const std::string name = "win:Local\\strata-pinned-test-" + std::to_string((unsigned long long) GetCurrentProcessId());
{
auto* first = new strata::core::PinnedArena(kBytes, no_bounds, 0, name, kPackHash);
if (!first->valid()) {
std::fprintf(stderr, "first section failed: %s\n", first->note.c_str());
return 1;
}
first->data()[123] = 0x5a;
if (first->shared_state == nullptr) { std::fprintf(stderr, "no state word\n"); fail = 1; }
else if (first->shared_state->load() != 0) { std::fprintf(stderr, "a new section's state word is not 0\n"); fail = 1; }
else first->shared_state->store(4242u);
auto* second = new strata::core::PinnedArena(kBytes, no_bounds, 0, name, kPackHash);
if (!second->valid()) { std::fprintf(stderr, "second section failed: %s\n", second->note.c_str()); fail = 1; }
else {
if (second->data()[123] != 0x5a) { std::fprintf(stderr, "same name did not expose the same bytes\n"); fail = 1; }
if (second->shared_state == nullptr || second->shared_state->load() != 4242u) { std::fprintf(stderr, "state word not shared\n"); fail = 1; }
}
{
strata::core::PinnedArena wrong_pack(kBytes, no_bounds, 0, name, kPackHash + 1);
if (wrong_pack.valid() || wrong_pack.note.find("pack hash") == std::string::npos) {
std::fprintf(stderr, "pack mismatch was not refused: %s\n", wrong_pack.note.c_str());
fail = 1;
}
strata::core::PinnedArena wrong_size(kBytes * 2, no_bounds, 0, name, kPackHash);
if (wrong_size.valid()) { std::fprintf(stderr, "a bigger size was not refused\n"); fail = 1; }
}
// a child process opens the live section
char exe[MAX_PATH];
GetModuleFileNameA(nullptr, exe, MAX_PATH);
std::string cmd = std::string("\"") + exe + "\" child \"" + name + "\"";
STARTUPINFOA si{};
si.cb = sizeof si;
PROCESS_INFORMATION pi{};
std::vector<char> cl(cmd.begin(), cmd.end());
cl.push_back('\0');
if (!CreateProcessA(nullptr, cl.data(), nullptr, nullptr, FALSE, 0, nullptr, nullptr, &si, &pi)) {
std::fprintf(stderr, "cannot start the child (error %lu)\n", GetLastError());
fail = 1;
} else {
WaitForSingleObject(pi.hProcess, 120000);
DWORD code = 99;
GetExitCodeProcess(pi.hProcess, &code);
CloseHandle(pi.hProcess);
CloseHandle(pi.hThread);
if (code != 0) { std::fprintf(stderr, "the child failed with %lu\n", code); fail = 1; }
if (first->data()[456] != 0xa5) { std::fprintf(stderr, "the child's byte did not arrive\n"); fail = 1; }
if (first->shared_state->load() != ~uint64_t{0}) { std::fprintf(stderr, "the child's state word did not arrive\n"); fail = 1; }
}
delete first; // the creator is gone; the second still holds the section
if (second != nullptr && second->valid()) {
if (second->data()[123] != 0x5a || second->data()[456] != 0xa5) { std::fprintf(stderr, "bytes lost when the first arena closed\n"); fail = 1; }
strata::core::PinnedArena third(kBytes, no_bounds, 0, name, kPackHash);
if (!third.valid() || third.data()[123] != 0x5a) { std::fprintf(stderr, "a late arena did not join the live section\n"); fail = 1; }
}
delete second;
}
{
strata::core::PinnedArena again(kBytes, no_bounds, 0, name, kPackHash); // (every handle was closed: a new, empty section)
if (!again.valid() || again.data()[123] != 0 || again.shared_state->load() != 0) {
std::fprintf(stderr, "the section survived its last handle\n");
fail = 1;
}
}
std::printf("pinned_shared_win_test: %s\n", fail ? "FAILED" : "OK");
return fail;
}
#else
int main() { return 77; } // (skipped: the Linux build has pinned_shared_test)
#endif
+2 -2
View File
@@ -136,7 +136,7 @@ void session_release(SessionState& s) {
#if defined(STRATA_GPU_METAL)
} // namespace strata::core
namespace strata::metal_engine {
void window_reset(); // metal/src/engine/window.cpp: the decode window's running state back to a new sequence's
void window_reset(const strata::core::SessionState* ss); // metal/src/engine/window.cpp: that session's running state (the main one or a batch slot's) back to a new sequence's
} // namespace strata::metal_engine
namespace strata::core {
#endif
@@ -145,7 +145,7 @@ void session_zero(SessionState& s, const ModelGeometry& g, const float* R_init,
#if defined(STRATA_GPU_METAL)
// the running state lives in the Metal decode window; the session's own device buffers are not read there
(void) g; (void) R_init; (void) stream;
strata::metal_engine::window_reset();
strata::metal_engine::window_reset(&s);
s.ple_prev[0] = -1;
s.ple_prev[1] = -1;
s.ple_token = -1;
+30 -1
View File
@@ -91,6 +91,8 @@ bool window_init(void* w, const strata::core::ModelGeometry& g, strata::core::Se
bool window_run(void* w, int T, const int32_t* tokens, int64_t pos0, bool head, int32_t* out, std::string& err);
bool window_commit(void* w, int n_keep, std::string& err);
bool window_copy_logits(void* w, int t, float* host);
bool window_init_slots(void* w, const std::vector<strata::core::SessionState*>& slots, std::string& err);
bool window_run_slots(void* w, const int* rows, int S, const int32_t* tokens, const int64_t* pos, int32_t* out, std::string& err);
} // namespace strata::metal_engine
#endif
@@ -1626,10 +1628,11 @@ bool Verifier::record_window(int T, cudaStream_t cs, std::string& err) {
gg.state[gg.n] = sx.gdn_state + (size_t) (gi - sx.gdn_ord0) * gdn_floats;
gg.first[gg.n] = first;
gg.ntok[gg.n] = t - first;
gg.keep[gg.n] = keep_rows ? one_ : nullptr; // STRATA_BATCH_SELF_COMMIT: the window keeps its one-row groups itself
++gg.n;
}
strata::kernels::gdn_step_norm_groups(gg, hb, (int) C, gate, beta, z_, (const float*) wnm->data, EPS,
y_, (int) HK, (int) HV, false, cs);
y_, (int) HK, (int) HV, keep_rows, cs);
} else
if (batch_rec_) { // each slot's recurrence over its own proposed-token group
for (int t = tb; t < te;) {
@@ -3704,6 +3707,17 @@ bool Verifier::init_slots(const std::vector<SessionState*>& slots, std::string&
return false;
}
}
#if defined(STRATA_GPU_METAL)
// the Metal window keeps each slot's running state itself (metal/src/engine/window.cpp): none of the CUDA batch buffers
slots_ = slots;
slot_sp_.assign(slots.size(), sampling_);
if (metal_ == nullptr || !strata::metal_engine::window_init_slots(metal_, slots, err)) {
if (err.empty()) err = "verify: the Metal window is not up";
return false;
}
std::fprintf(stderr, "strata verify: batch windows of up to %d sequences, run slot by slot in the Metal window\n", (int) slots.size());
return true;
#endif
const strata::kernels::QsaShapes s = shapes_of(*g_);
const int64_t S = (int64_t) slots.size(), CB = 2 + max_t_;
const int64_t TS = (s.idx_block - 1) * g_->idx_key_dim, nQ = g_->n_qsa_layers();
@@ -4379,6 +4393,16 @@ bool Verifier::run_slots(int S, const int32_t* tokens, const int64_t* pos, PoolM
bool Verifier::run_slot_rows(const int* rows, int S, const int32_t* tokens, const int64_t* pos, PoolMultiFn pool,
void* user, int32_t* out, std::string& err) {
#if defined(STRATA_GPU_METAL)
// each row runs in its slot's state and commits there (a one-token window); nothing is left for commit_slots
if (S < 1 || S > (int) strata::kernels::kVerifyMaxT) { err = "verify: batch window size out of range"; return false; }
(void) pool; (void) user;
if (!strata::metal_engine::window_run_slots(metal_, rows, S, tokens, pos, out, err)) return false;
last_t_ = S;
for (int t = 0; t < S; ++t) { last_rows_[t] = rows[t]; last_tokens_[t] = tokens[t]; last_pos_b_[t] = pos[t]; }
last_batch_ = true;
return true;
#endif
using namespace strata::kernels;
const OnDevice on_device(device_);
const ModelGeometry& g = *g_;
@@ -4490,6 +4514,11 @@ bool Verifier::commit_slots(std::string& err) {
}
bool Verifier::commit_slot_prefixes(const int* keep, std::string& err) {
#if defined(STRATA_GPU_METAL)
(void) keep; // (greedy one-row windows, committed by window_run_slots)
if (!last_batch_ || last_t_ < 1) { err = "verify: commit_slots without a batch window"; return false; }
return true;
#endif
const OnDevice on_device(device_);
if (!last_batch_ || last_t_ < 1) { err = "verify: commit_slots without a batch window"; return false; }
const int S = last_t_;
+5 -3
View File
@@ -186,10 +186,12 @@ inline bool copy_hugetlb_default(int env, double pool_free_bytes, double copy_by
return pool_free_bytes > 0.0 && pool_free_bytes >= copy_bytes;
}
// DS4_FAST_ATTN (the chunked prompt's attention on the matrix cores): env -1 unset, else 0 / 1; 0.2.6: opt-in; 0.2.7: on for gfx1200 / gfx1201.
inline bool fast_attn_default(int env, bool gfx12, bool v026 = false) {
// DS4_FAST_ATTN (the chunked prompt's attention on the matrix cores): env -1 unset, else 0 / 1; 0.2.6: opt-in; 0.2.7: on for
// gfx1200 / gfx1201, and (this change) on for gfx1151 on Windows, where it measured +43.6% on a 32K prompt (101.6 -> 70.8 s,
// sparse attention 38.3 -> 7.0 s), the same greedy first token and a first-token KL far under the 0.0228 bar.
inline bool fast_attn_default(int env, bool gfx12, bool gfx1151win = false, bool v026 = false) {
if (env >= 0) return env != 0;
return !v026 && gfx12;
return !v026 && (gfx12 || gfx1151win);
}
// The drafter's cumulative confidence cut (STRATA_DS4_DSPARK_CUM, a request's dspark_cum): the variable when set; 0.2.6: 0; 0.2.7: 0.5 on gfx1200 / gfx1201.
+1 -1
View File
@@ -1087,7 +1087,7 @@ bool type_on_chunk(int type) {
#if defined(STRATA_METAL)
// ds4_mm.metal instantiates the tile GEMM for IQ2_XXS / IQ2_XS / IQ2_S only; IQ1_M (the 2.00 bpw pack's type) has
// the matvec kernel but no tile one, so a chunked prompt would stop in "kernel launch with no Metal dispatch".
if (type == iq::kIQ1_S || type == iq::kIQ4_NL) return false; // (IQ1_M: the tile kernels since DS4_MM_INST(29) and iq_bs)
if (type == iq::kIQ4_NL) return false; // (IQ1_M and IQ1_S: the tile kernels, DS4_MM_INST(29) / (19) and iq_bs)
return type_on_device(type);
#else
(void) type;
+1 -1
View File
@@ -1457,7 +1457,7 @@ bool supported(int t) {
// The MSL kernels (metal/src/engine/msl/ds4_iq.metal) implement the 2.0-bit types plus IQ1_M; IQ1_S and IQ4_NL
// have CUDA kernels here but no Metal ones. Saying so makes those layers "cpu only" (ds4_model.cu, expert_layout),
// so their experts stay on the CPU lane instead of stopping in "kernel launch with no Metal dispatch".
if (t == kIQ1_S || t == kIQ4_NL) return false;
if (t == kIQ4_NL) return false; // (IQ1_S: ds4_iq1s_mvn + the tile kernel since the Unsloth sets)
#endif
// the types the kernels below are instantiated for (DS4_IQ_SWITCH + the trellis ones + the dense K-quants)
switch (t) {
+25 -3
View File
@@ -2038,7 +2038,9 @@ bool split_enabled() {
// 0.006 gate, ids identical); gfx11 and gfx1151 (Strix Halo) are untested and keep the scalar kernels. DS4_FAST_ATTN=0 is the
// off switch (exactly the 0.2.6 arithmetic), DS4_FAST_ATTN=1 turns it on elsewhere. Decided per device (the first call there).
// The Marlin stream measured it ON on 4x R9700 (gfx1201), 2.25 bpw, 8K code prompts: 1180 -> 1354 ppts (+14.7%), the same ids,
// KL 0.10833 against 0.10925 for the scalar path. It stays gfx12-only until Strix Halo (gfx1151) is tested.
// KL 0.10833 against 0.10925 for the scalar path. gfx1151 on Windows was the untested part; measured on Strix Halo 2026-10-28
// (32K prompt, 2.00 bpw): 101.6 -> 70.8 s (+43.6%), the sparse-attention stage 38.3 -> 7.0 s, the same greedy first token
// (27408,), so it joins the default there and stays opt-in on every other gfx11 part.
#if defined(STRATA_USE_HIP)
bool device_is_gfx12() {
static std::atomic<int> cache[64]; // 0 unknown, 1 no, 2 yes
@@ -2055,6 +2057,22 @@ bool device_is_gfx12() {
}
return c == 2;
}
#if defined(_WIN32)
bool device_is_gfx1151win() {
static std::atomic<int> cache[64]; // 0 unknown, 1 no, 2 yes
int dev = 0;
cudaGetDevice(&dev);
if (dev < 0 || dev >= 64) return false;
int c = cache[dev].load();
if (c == 0) {
cudaDeviceProp prop{};
const bool yes = cudaGetDeviceProperties(&prop, dev) == cudaSuccess && std::strncmp(prop.gcnArchName, "gfx1151", 7) == 0;
c = yes ? 2 : 1;
cache[dev].store(c);
}
return c == 2;
}
#endif
#endif
bool fast_attn_enabled() {
static const int env = [] {
@@ -2062,9 +2080,13 @@ bool fast_attn_enabled() {
return e == nullptr ? -1 : (std::atoi(e) != 0 ? 1 : 0);
}();
#if defined(STRATA_USE_HIP)
return strata::ds4::fast_attn_default(env, device_is_gfx12(), strata::ds4::defaults_026());
#if defined(_WIN32)
return strata::ds4::fast_attn_default(env, device_is_gfx12(), device_is_gfx1151win(), strata::ds4::defaults_026());
#else
return strata::ds4::fast_attn_default(env, false, strata::ds4::defaults_026());
return strata::ds4::fast_attn_default(env, device_is_gfx12(), false, strata::ds4::defaults_026());
#endif
#else
return strata::ds4::fast_attn_default(env, false, false, strata::ds4::defaults_026());
#endif
}
attnsplit::AttnSplitScratch split_scratch(cudaStream_t s, int heads) {
Loaded 100 of 283 files, more files were not shown because too many files have changed in this diff. Show more