mirror of
https://github.com/Niko1221/Strata.git
synced 2026-11-09 03:59:45 -10:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c62d5e3a15 | ||
|
|
7aa9a584aa | ||
|
|
987ebd442f | ||
|
|
e4031fc4d0 | ||
|
|
dec8f1366c | ||
|
|
6f7f763ce5 | ||
|
|
f93ec8a924 | ||
|
|
5322f1c2bd | ||
|
|
4cf0d8cd38 | ||
|
|
33bbbca442 | ||
|
|
38f56619cc | ||
|
|
1a26492064 | ||
|
|
bee469c462 | ||
|
|
6a7d750578 | ||
|
|
417765b8d8 | ||
|
|
5c25767fcd | ||
|
|
ea5dbffa7f | ||
|
|
a3a1103c13 | ||
|
|
ba3d28b7e3 | ||
|
|
0dba618d34 | ||
|
|
5ff7da4a70 | ||
|
|
1b6c67122a | ||
|
|
7851a8dab6 | ||
|
|
4148c51257 | ||
|
|
bd3c5cd4cb | ||
|
|
99866a47a2 | ||
|
|
73e0a3b6ed | ||
|
|
9144da7b1a | ||
|
|
a833fb46f3 | ||
|
|
32806f82aa | ||
|
|
bc694233bf | ||
|
|
dd3f9ec82f | ||
|
|
47cbb76690 | ||
|
|
ab083a693f | ||
|
|
ad4755c249 | ||
|
|
b60fbb54f7 | ||
|
|
8f13b8c4ad | ||
|
|
0b1808f072 | ||
|
|
cfa7f737cc | ||
|
|
b3de587e03 | ||
|
|
7f6bc33081 | ||
|
|
1ead709790 | ||
|
|
dd63f88fdd | ||
|
|
2a9ccfe3ea | ||
|
|
dd471d3140 | ||
|
|
4a75fa2b62 | ||
|
|
3282c61343 | ||
|
|
209dcadb3f | ||
|
|
e66a445892 | ||
|
|
68718c33fa | ||
|
|
ea52e55cd7 | ||
|
|
08e8770c6e | ||
|
|
7146d6983f | ||
|
|
01ab6b51bb | ||
|
|
957038a1cd | ||
|
|
d340dc8dd1 | ||
|
|
ffaa6ad78c | ||
|
|
4006305476 | ||
|
|
97e6f1fe19 | ||
|
|
33bd74182b | ||
|
|
4d12701181 | ||
|
|
aed382885e | ||
|
|
274257630b | ||
|
|
d96873fc56 | ||
|
|
86bed1c92f | ||
|
|
203df526f1 | ||
|
|
b6fc410ee5 | ||
|
|
0a76d62e68 | ||
|
|
c0e245c8be | ||
|
|
16e08b50cd | ||
|
|
1fb3e9a326 | ||
|
|
1f8f2fa8ad | ||
|
|
3a76d5eaf6 | ||
|
|
01ef876562 | ||
|
|
4746bfb165 | ||
|
|
0b1fbf4012 | ||
|
|
6923421a61 | ||
|
|
09dc5c52b3 | ||
|
|
5eaee81a43 | ||
|
|
d936db6547 | ||
|
|
ac3e0e17b5 | ||
|
|
b3bc477512 | ||
|
|
db729163ce | ||
|
|
19b5f873f2 | ||
|
|
0122eceb4c | ||
|
|
8401c21f9d | ||
|
|
9c2121cf5c | ||
|
|
1cfffa2ede | ||
|
|
a83252a7b7 | ||
|
|
d8056fbe00 | ||
|
|
6fb110e557 | ||
|
|
28c4543e48 | ||
|
|
e1e7e32680 | ||
|
|
94fc01c6fd | ||
|
|
b0ba459514 | ||
|
|
2fef430202 | ||
|
|
7a9245b6c0 | ||
|
|
368bcb7a26 | ||
|
|
5ad0135c00 | ||
|
|
cdb5470c70 | ||
|
|
87970c8ae0 | ||
|
|
777d11f8a3 | ||
|
|
dcc79c5230 | ||
|
|
db0606b4f8 | ||
|
|
4bfd66686e | ||
|
|
5c00af8a1f | ||
|
|
78859bbcb1 | ||
|
|
9b5965c3c1 | ||
|
|
836dd2d86e | ||
|
|
6d7255f205 | ||
|
|
97bbd76c92 | ||
|
|
36e798f4c5 | ||
|
|
1510e6d6d2 | ||
|
|
4772f0cd9a | ||
|
|
7aafac422c | ||
|
|
62b042e843 | ||
|
|
9d8337844e | ||
|
|
714438ec19 | ||
|
|
d3af2d42ad | ||
|
|
1a865fb3db | ||
|
|
a0e7f5160d | ||
|
|
dc149ef003 | ||
|
|
9f1d0ad041 | ||
|
|
1064ff7783 | ||
|
|
301a09f100 | ||
|
|
4f88a2fa8b | ||
|
|
1f05fdc283 | ||
|
|
a8fba1f5b7 | ||
|
|
2f0d6a1c15 | ||
|
|
42aee7a349 | ||
|
|
3782008944 | ||
|
|
05cd12e7a2 | ||
|
|
0c94851835 | ||
|
|
21f950b4b4 | ||
|
|
3ccfd5aed3 | ||
|
|
51567de1a6 | ||
|
|
f0a34fa9bf | ||
|
|
84aa2d7d57 | ||
|
|
1d9b4c48e9 | ||
|
|
2072510a47 | ||
|
|
0ea3e455fd | ||
|
|
a9f154e82b | ||
|
|
766b54afb1 | ||
|
|
faa1be268c | ||
|
|
47a1047750 | ||
|
|
ee05114846 | ||
|
|
d8c94b45c8 | ||
|
|
236adb3987 | ||
|
|
3484572236 | ||
|
|
e0cc4e859e | ||
|
|
cea3057e2c | ||
|
|
5e9995d296 | ||
|
|
095464152c | ||
|
|
2cf3fd181f | ||
|
|
06f212aefe | ||
|
|
4d9d9cefd3 | ||
|
|
f34ab1c8f5 | ||
|
|
280885fda0 | ||
|
|
ae2657a19d | ||
|
|
d4f15adf87 | ||
|
|
dbdec0bb9e | ||
|
|
d7cb41ba87 | ||
|
|
c4e975cc7a | ||
|
|
c15a90b28c | ||
|
|
b2dbd9420d | ||
|
|
c697e859e9 | ||
|
|
2ea46ded73 | ||
|
|
20bba22af7 | ||
|
|
1493781e91 |
No files matched your search
@@ -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
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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 デスクトップアプリ">
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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,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": {
|
||||
|
||||
Generated
+1
-1
@@ -3625,7 +3625,7 @@ checksum = "ff7589a1859b09232522bb9edf076ca7fcf14ded96685d3fa629db73023ffbb7"
|
||||
|
||||
[[package]]
|
||||
name = "strata-desktop"
|
||||
version = "0.2.8"
|
||||
version = "0.2.9"
|
||||
dependencies = [
|
||||
"gtk",
|
||||
"serde",
|
||||
|
||||
@@ -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"
|
||||
|
||||
Generated
+2
-2
@@ -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,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": {
|
||||
|
||||
@@ -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));
|
||||
@@ -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();
|
||||
@@ -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';
|
||||
|
||||
@@ -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,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; }
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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\)/);
|
||||
});
|
||||
@@ -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\(\);/);
|
||||
});
|
||||
@@ -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 => {
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
@@ -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);}
|
||||
});
|
||||
@@ -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');
|
||||
});
|
||||
@@ -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 });
|
||||
});
|
||||
@@ -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
@@ -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
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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
@@ -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). |
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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.**
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
@@ -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()
|
||||
@@ -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
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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];
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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;
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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); }
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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]]) {
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
|
||||
|
||||
@@ -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
|
||||
};
|
||||
|
||||
|
||||
@@ -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
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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 = ⪯
|
||||
// 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;
|
||||
}
|
||||
@@ -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 = ⪯
|
||||
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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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):]
|
||||
|
||||
@@ -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
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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):
|
||||
|
||||
@@ -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
@@ -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()
|
||||
@@ -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],
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
@@ -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;
|
||||
|
||||
@@ -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
|
||||
@@ -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
@@ -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_;
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
@@ -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) {
|
||||
|
||||
@@ -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
Reference in new issue
Block a user