mirror of
https://github.com/Niko1221/Strata.git
synced 2026-11-07 14:21:06 -10:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
870aedd9e1 | ||
|
|
36f6663c25 | ||
|
|
1bf946eafa | ||
|
|
5e8f47cfff | ||
|
|
18200cffaf | ||
|
|
cf059188ca | ||
|
|
b119f40f08 | ||
|
|
8d5be82566 | ||
|
|
74e1f86442 | ||
|
|
a6fd788567 | ||
|
|
32f0c44f08 | ||
|
|
93dd9b421d | ||
|
|
92a867be17 | ||
|
|
a3d6298297 | ||
|
|
b82d1d4567 | ||
|
|
3b8dc8fa35 | ||
|
|
d76b50681a | ||
|
|
77287f13ec | ||
|
|
95563ff292 | ||
|
|
5c8bcf4307 | ||
|
|
4fdb563a34 | ||
|
|
9d3c8c3664 | ||
|
|
061c542010 | ||
|
|
f2bf7a3ce3 | ||
|
|
6124c35e39 | ||
|
|
8475011ac1 | ||
|
|
89253935db | ||
|
|
e318f71e75 | ||
|
|
bf8d88b9dd | ||
|
|
7356aa1404 | ||
|
|
11d337537c | ||
|
|
478964ecaa | ||
|
|
09524eb479 | ||
|
|
949f77c0a8 | ||
|
|
7e0f719903 | ||
|
|
f7570e23e2 | ||
|
|
d56565ce39 | ||
|
|
da3df90eb8 | ||
|
|
16ccaf6150 | ||
|
|
11a72289d2 | ||
|
|
43f32b3046 | ||
|
|
3fcb2106d0 | ||
|
|
20a63cf0f0 | ||
|
|
72a2b77057 | ||
|
|
cfa197e21a | ||
|
|
22fb183cf6 | ||
|
|
01d6f7b995 | ||
|
|
07d5ecfacb | ||
|
|
b18292d702 | ||
|
|
d71132738e | ||
|
|
84c7f43358 | ||
|
|
012bea51f8 | ||
|
|
5596cb2c5b | ||
|
|
587a474433 | ||
|
|
35c0c9a8ae | ||
|
|
c7a0cef13a | ||
|
|
ea73eab442 | ||
|
|
5ef90cb96b | ||
|
|
3e1141edf0 | ||
|
|
293044e561 | ||
|
|
5af539691d | ||
|
|
0e72aff563 | ||
|
|
b72f56e02a | ||
|
|
77794e3400 | ||
|
|
b5aa1d94e0 | ||
|
|
2f9c6fc35c | ||
|
|
c3326979fc | ||
|
|
7a80b3a96b | ||
|
|
d52b73dc65 | ||
|
|
0c5b157fbd | ||
|
|
46c152a131 | ||
|
|
78b99a6605 | ||
|
|
55a557bb6d | ||
|
|
ed1e5546c2 | ||
|
|
bc6c32555b | ||
|
|
a9686a900a | ||
|
|
b90c8a2172 | ||
|
|
979639e9cd | ||
|
|
756c00fbf6 | ||
|
|
9258e4086e | ||
|
|
09c18d3e98 | ||
|
|
58f244ab1e | ||
|
|
39c5534ed3 | ||
|
|
2a220c1d1c | ||
|
|
87a755bf1e | ||
|
|
3939193dd3 | ||
|
|
864afc7421 | ||
|
|
d50f6a77ed | ||
|
|
5729a6e50a | ||
|
|
48c8e85843 | ||
|
|
4daa83e75f | ||
|
|
5cfe96ee6a | ||
|
|
845941f522 | ||
|
|
e3ed743f21 | ||
|
|
c7980a3fe4 | ||
|
|
1a937829a5 | ||
|
|
773fc4e228 | ||
|
|
732e4bfb60 | ||
|
|
52ee842dbf | ||
|
|
b0e897b41f | ||
|
|
6a51355051 | ||
|
|
8f7d53ccd5 | ||
|
|
2cb47c34c2 | ||
|
|
232fa357fd | ||
|
|
828fc80004 | ||
|
|
1fca685d6b | ||
|
|
cb32d8758d | ||
|
|
fa370e46d1 | ||
|
|
2e7af09a86 | ||
|
|
b4814a9fb1 | ||
|
|
d551ab0282 | ||
|
|
43a328d8b6 | ||
|
|
e6fd914616 | ||
|
|
694e711708 | ||
|
|
8544420180 | ||
|
|
f436dba81b | ||
|
|
552c56f7a7 | ||
|
|
e3d8823e6c | ||
|
|
9a398bd5bf | ||
|
|
4e06c84b91 | ||
|
|
f262b81b28 | ||
|
|
56ab127c87 | ||
|
|
5ed8ae03be | ||
|
|
756380301b | ||
|
|
314a6ecf69 | ||
|
|
8477811197 | ||
|
|
1b667c7abb | ||
|
|
4acede655a | ||
|
|
1ece7efd5c | ||
|
|
2a207dbfcc | ||
|
|
b6dcb23ec6 | ||
|
|
100a2374fb | ||
|
|
8810e00c09 | ||
|
|
050752f5b9 | ||
|
|
97c091c5be | ||
|
|
6b2a530393 | ||
|
|
e527bfefc1 | ||
|
|
9e3fe0461c | ||
|
|
90359513e3 | ||
|
|
8d3c976166 | ||
|
|
dde3250cdb | ||
|
|
958774b4ce | ||
|
|
f29ff5a59a | ||
|
|
34810a3bb3 | ||
|
|
5dce2f63e9 | ||
|
|
c2675c60d1 | ||
|
|
700def65a2 | ||
|
|
58e5d7851e | ||
|
|
7c77f388dd | ||
|
|
42a982e033 | ||
|
|
3e51b1c18a | ||
|
|
9dbb13ee89 | ||
|
|
860e384db7 | ||
|
|
193529239c | ||
|
|
9f45341966 | ||
|
|
da9cf6354f | ||
|
|
0c34a7de3c | ||
|
|
97637e2a14 | ||
|
|
782a8381de | ||
|
|
c763c12ac5 | ||
|
|
00e7626771 | ||
|
|
ba9e818c42 | ||
|
|
1a80ccc981 | ||
|
|
0b2201ce44 | ||
|
|
c486f17251 | ||
|
|
611530d992 | ||
|
|
33bf441cd4 | ||
|
|
154c2990ed | ||
|
|
4735e381ad | ||
|
|
7022bd25d8 | ||
|
|
5d969adde9 | ||
|
|
cb931e7010 | ||
|
|
b5b4a97c0a | ||
|
|
45cbb451e7 | ||
|
|
15f1fe0548 | ||
|
|
5f6d49e959 | ||
|
|
2cc55f6165 | ||
|
|
446a45f750 | ||
|
|
1d21a0f60a | ||
|
|
9c1e61a6f3 | ||
|
|
45faf57702 | ||
|
|
680724b87a | ||
|
|
a445418e5e | ||
|
|
baf261deca | ||
|
|
6bfcfa777f | ||
|
|
ca903c124a | ||
|
|
883d0000a7 | ||
|
|
d692839177 | ||
|
|
aaf3aa3a7c | ||
|
|
dc61a065e3 | ||
|
|
e46b149ae5 | ||
|
|
18c6075562 | ||
|
|
a798d03858 | ||
|
|
bb60de0e8e | ||
|
|
910af91838 | ||
|
|
bcb2133a28 | ||
|
|
f0f056b9bb |
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.7 · engine 0.1.52"
|
||||
placeholder: "Strata 0.2.8 · engine 0.1.54"
|
||||
validations:
|
||||
required: true
|
||||
- type: input
|
||||
|
||||
@@ -89,3 +89,9 @@ node_modules/
|
||||
# screenshots and checkpoints from the Mac rebuild live outside git (StrataSplit\evidence-archive)
|
||||
/apps/host/docs/evidence/
|
||||
/apps/host/docs/checkpoints/
|
||||
|
||||
# DGX Spark build outputs and local venv (docs/DGX_SPARK_LINUX.md)
|
||||
/out-spark/
|
||||
/build-spark/
|
||||
/build-vision-spark/
|
||||
/.venv-build/
|
||||
@@ -1,6 +1,6 @@
|
||||
# AGENTS.md
|
||||
|
||||
Version 0.2.7, engine 0.1.52. The public model families are DeepSeekV4.1-Flash-1017,
|
||||
Version 0.2.8, engine 0.1.54. 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.7-windows-x64-setup.exe`, `Strata-0.2.7-linux-x86_64.AppImage`, `Strata-0.2.7-macos-arm64.dmg`.
|
||||
`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 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
|
||||
|
||||
+58
-2
@@ -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.52 LANGUAGES CXX)
|
||||
project(strata VERSION 0.1.54 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)
|
||||
@@ -92,6 +92,31 @@ if(STRATA_HIP_GFX906)
|
||||
endif()
|
||||
option(STRATA_ENABLE_HIP "Build the HIP targets for wave32 AMD GPUs (gfx1100, gfx1201; gfx1012/gfx1030 unvalidated)" OFF)
|
||||
option(STRATA_PREFILL_MMQ "Build the optional GGML MMQ prefill path for HIP" OFF)
|
||||
# The rows one verify / batch window holds (include/strata/kernels/window_limits.hpp): 8, the 0.2.7 window, or 16 (four pipelined
|
||||
# groups of 16 slots = 64 slots on a four-card split, docs/BATCHING.md). Only the CUDA and HIP engines are built from
|
||||
# these sources with a wide window; the SYCL and Metal builds never read this variable and keep 8. A window of up to
|
||||
# 8 rows runs the kernels it always ran, whichever is chosen (the wide instantiations are extra code). "auto" = 16 for
|
||||
# the HIP engines (a HIP code object loads on first use, so the extra kernels cost nothing until a wide window forms)
|
||||
# and 8 for CUDA, which loads every kernel at start (CUDA_MODULE_LOADING=EAGER: the wide kernels would take VRAM from
|
||||
# the expert cache of every card); -DSTRATA_WINDOW_MAX_T=16 builds the wide CUDA engine.
|
||||
set(STRATA_WINDOW_MAX_T "auto" CACHE STRING "rows of a verify / batch window: 8, 16 or auto (HIP 16, CUDA 8)")
|
||||
set_property(CACHE STRATA_WINDOW_MAX_T PROPERTY STRINGS auto 8 16)
|
||||
if(NOT STRATA_WINDOW_MAX_T STREQUAL "auto" AND NOT STRATA_WINDOW_MAX_T STREQUAL "8" AND NOT STRATA_WINDOW_MAX_T STREQUAL "16")
|
||||
message(FATAL_ERROR "STRATA_WINDOW_MAX_T must be auto, 8 or 16 (got '${STRATA_WINDOW_MAX_T}')")
|
||||
endif()
|
||||
if(STRATA_ENABLE_HIP OR STRATA_ENABLE_CUDA)
|
||||
if(STRATA_WINDOW_MAX_T STREQUAL "auto")
|
||||
if(STRATA_ENABLE_HIP)
|
||||
set(_strata_window_max_t 16)
|
||||
else()
|
||||
set(_strata_window_max_t 8)
|
||||
endif()
|
||||
else()
|
||||
set(_strata_window_max_t ${STRATA_WINDOW_MAX_T})
|
||||
endif()
|
||||
add_compile_definitions(STRATA_VERIFY_MAX_T=${_strata_window_max_t})
|
||||
message(STATUS "Strata: verify / batch windows hold up to ${_strata_window_max_t} rows")
|
||||
endif()
|
||||
# MMQ prompt kernels for Q4_K / Q5_K / Q5_1 / Q6_K (Unsloth's UD-Q4/Q5/Q6_K_XL experts; on HIP also the dense GGUF
|
||||
# projections, STRATA_DENSE_MMQ). On by default since MODEL-UPDATE Phase 1: on CUDA they are their own library
|
||||
# (strata_kq, loaded only for a model with these formats: the engine loads every kernel of its own binary at start,
|
||||
@@ -131,6 +156,11 @@ set(_strata_tests_default OFF)
|
||||
if(EXISTS ${CMAKE_CURRENT_SOURCE_DIR}/tests/CMakeLists.txt AND EXISTS ${CMAKE_CURRENT_SOURCE_DIR}/bench/micro)
|
||||
set(_strata_tests_default ON)
|
||||
endif()
|
||||
if(NOT STRATA_CPU_X86)
|
||||
# arm64 Linux (DGX Spark): much of the suite drives the x86 CPU kernels, which this build does not have. A default, not a
|
||||
# prohibition: -DSTRATA_BUILD_TESTS=ON still builds what builds (classifying the tests by capability is a follow-up).
|
||||
set(_strata_tests_default OFF)
|
||||
endif()
|
||||
option(STRATA_BUILD_TESTS "Build the test suite" ${_strata_tests_default})
|
||||
option(STRATA_BUILD_CONVERSATION_TESTS "Build standalone conversation cache tests" OFF)
|
||||
option(STRATA_WERROR "Treat compiler warnings as errors" OFF)
|
||||
@@ -1628,6 +1658,11 @@ if(STRATA_ENABLE_CUDA OR STRATA_ENABLE_HIP)
|
||||
target_include_directories(ple_residency_test PRIVATE include)
|
||||
add_test(NAME ple_residency_test COMMAND ple_residency_test)
|
||||
|
||||
# #4474: the automatic expert cache's slot rule (the late allocation priced); header-only, needs nothing
|
||||
add_executable(auto_cache_test src/core/auto_cache_test.cpp)
|
||||
target_include_directories(auto_cache_test PRIVATE include)
|
||||
add_test(NAME auto_cache_test COMMAND auto_cache_test)
|
||||
|
||||
# 0.2.7 (#3911): the qwen35 host sampler's penalties on a short (draft-vocab) logit row; header-only, CPU
|
||||
add_executable(qwen35_sampling_test src/qwen35/sampling_test.cpp)
|
||||
target_include_directories(qwen35_sampling_test PRIVATE include)
|
||||
@@ -1690,6 +1725,9 @@ if(STRATA_BUILD_TESTS)
|
||||
add_executable(arch_defaults_test src/core/arch_defaults_test.cpp src/core/arch_defaults.cpp)
|
||||
target_include_directories(arch_defaults_test PRIVATE include)
|
||||
add_test(NAME arch_defaults_test COMMAND arch_defaults_test)
|
||||
add_executable(nv_defaults_test src/core/nv_defaults_test.cpp src/core/nv_defaults.cpp) # the sm_121 (GB10) table and its two off switches
|
||||
target_include_directories(nv_defaults_test PRIVATE include)
|
||||
add_test(NAME nv_defaults_test COMMAND nv_defaults_test)
|
||||
add_executable(kva_test src/prefill/kva_test.cpp) # KVA: the switch, the automatic tail, the reasons a read is turned away
|
||||
target_include_directories(kva_test PRIVATE include)
|
||||
add_test(NAME kva_test COMMAND kva_test)
|
||||
@@ -1728,6 +1766,10 @@ if(STRATA_BUILD_TESTS OR STRATA_BUILD_CONVERSATION_TESTS)
|
||||
add_executable(adapt_decay_test src/program/adapt_decay_test.cpp)
|
||||
target_include_directories(adapt_decay_test PRIVATE include)
|
||||
add_test(NAME adapt_decay_test COMMAND adapt_decay_test)
|
||||
# the window limits (8 rows, or 16 in a wide CUDA / HIP build) and the pipelined-window probe (header-only, CPU)
|
||||
add_executable(window_limits_test src/core/window_limits_test.cpp)
|
||||
target_include_directories(window_limits_test PRIVATE include)
|
||||
add_test(NAME window_limits_test COMMAND window_limits_test)
|
||||
# which window-size graph is freed when the next one does not fit (#3702; header-only, CPU)
|
||||
add_executable(window_evict_test src/core/window_evict_test.cpp)
|
||||
target_include_directories(window_evict_test PRIVATE include)
|
||||
@@ -1844,7 +1886,14 @@ if(STRATA_NATIVE_EXPERTS)
|
||||
set(GGML_BACKEND_DL OFF CACHE BOOL "" FORCE)
|
||||
set(GGML_BUILD_TESTS OFF CACHE BOOL "" FORCE)
|
||||
set(GGML_BUILD_EXAMPLES OFF CACHE BOOL "" FORCE)
|
||||
if(STRATA_PORTABLE)
|
||||
if(STRATA_PORTABLE AND NOT STRATA_CPU_X86)
|
||||
# arm64 (DGX Spark: Cortex-X925 + A725): ggml-cpu for any Armv8.2 CPU with dot product, FP16 and int8 matmul instead of the build
|
||||
# host's, so the engine built here does not depend on which core the compiler ran on. Without it GGML_NATIVE OFF would leave
|
||||
# ggml-cpu at plain armv8-a, and a native pack's CPU experts would lose the NEON dot products.
|
||||
set(GGML_NATIVE OFF CACHE BOOL "" FORCE)
|
||||
set(STRATA_ARM64_ARCH "armv8.2-a+dotprod+fp16+i8mm" CACHE STRING "arm64: ggml-cpu's -march for a STRATA_PORTABLE build")
|
||||
set(GGML_CPU_ARM_ARCH "${STRATA_ARM64_ARCH}" CACHE STRING "" FORCE)
|
||||
elseif(STRATA_PORTABLE)
|
||||
set(GGML_NATIVE OFF CACHE BOOL "" FORCE)
|
||||
foreach(_f GGML_AVX GGML_AVX2 GGML_FMA GGML_F16C GGML_BMI2)
|
||||
set(${_f} ON CACHE BOOL "" FORCE)
|
||||
@@ -1897,6 +1946,10 @@ if(STRATA_NATIVE_EXPERTS)
|
||||
src/kernels/cpu/kq_avx2.cpp src/kernels/cpu/kq_avx1.cpp src/kernels/cpu/q2_avx1.cpp
|
||||
src/kernels/cpu/iq_avx1.cpp src/kernels/cpu/nvfp4_avx512.cpp src/kernels/cpu/nvfp4_avx2.cpp
|
||||
src/kernels/cpu/tq_cpu.cpp src/kernels/cpu/tq_avx2.cpp src/kernels/cpu/tq_avx512.cpp)
|
||||
else()
|
||||
# the trellis dispatcher alone (the DeepSeek engine's CPU lane and Gyro): tq_cpu.cpp runs its scalar rows when the x86 units are
|
||||
# not compiled (the Metal build does the same)
|
||||
target_sources(strata_kernels_cpu PRIVATE src/kernels/cpu/tq_cpu.cpp)
|
||||
endif()
|
||||
target_include_directories(strata_kernels_cpu PRIVATE third_party/ggml)
|
||||
if(MSVC)
|
||||
@@ -1949,6 +2002,8 @@ if(STRATA_NATIVE_EXPERTS)
|
||||
target_link_libraries(dequant_q5_1_test PRIVATE strata_artifact ggml-base)
|
||||
add_test(NAME dequant_q5_1_test COMMAND dequant_q5_1_test)
|
||||
endif()
|
||||
# the x86 kernels' parity programs and benches (to the matching endif below): not on arm64, which has none of those kernels
|
||||
if(STRATA_CPU_X86)
|
||||
# the router lookahead's dot on each rung this CPU has (AVX2, the older CPUs' AVX1) against a double reference
|
||||
add_executable(router_dot_parity src/kernels/cpu/router_dot_parity.cpp)
|
||||
target_link_libraries(router_dot_parity PRIVATE strata_kernels_cpu)
|
||||
@@ -2009,6 +2064,7 @@ if(STRATA_NATIVE_EXPERTS)
|
||||
add_test(NAME q8k_quant_parity COMMAND q8k_quant_parity)
|
||||
add_test(NAME q8k_quant_parity_avx COMMAND ${CMAKE_COMMAND} -E env STRATA_FORCE_ISA=avx $<TARGET_FILE:q8k_quant_parity>)
|
||||
endif()
|
||||
endif() # STRATA_CPU_X86
|
||||
set(_strata_mmq_types q2_0 iq2_xxs iq2_xs iq2_s iq3_xxs iq3_s iq4_nl iq4_xs q8_0)
|
||||
# APR: the agention llama.cpp fork has MMQ instances for its trellis expert types (f62fdead0); any other ggml
|
||||
# tree leaves them to the FP16 prompt path
|
||||
|
||||
+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.7-windows-x64-setup.exe` · `Strata-0.2.7-linux-x86_64.AppImage` · `Strata-0.2.7-macos-arm64.dmg`</sub>
|
||||
<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>
|
||||
|
||||
<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.7-windows-x64-setup.exe` · `Strata-0.2.7-linux-x86_64.AppImage` · `Strata-0.2.7-macos-arm64.dmg`</sub>
|
||||
<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>
|
||||
|
||||
<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.7-windows-x64-setup.exe` · `Strata-0.2.7-linux-x86_64.AppImage` · `Strata-0.2.7-macos-arm64.dmg`</sub>
|
||||
<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>
|
||||
|
||||
<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.7-windows-x64-setup.exe` · `Strata-0.2.7-linux-x86_64.AppImage` · `Strata-0.2.7-macos-arm64.dmg`</sub>
|
||||
<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>
|
||||
|
||||
<p align="center">
|
||||
<img src="docs/media/app-chat.png" width="900" alt="チャットとライブのコードプレビューを並べて表示した Strata デスクトップアプリ">
|
||||
|
||||
@@ -63,8 +63,8 @@ disk space.
|
||||
| 2.00 bpw | 257 GB | about 265 GB |
|
||||
| 2.25 bpw | 274 GB | about 280 GB |
|
||||
|
||||
Runs on NVIDIA RTX 30, 40 and 50 series (Windows and Linux), AMD Radeon AI PRO R9700 (Linux), and AMD Ryzen AI Max — Strix Halo
|
||||
(Windows). Setup downloads the files and the DeepSeek engine, then starts the model.
|
||||
Runs on NVIDIA RTX 30, 40 and 50 series (Windows and Linux), AMD Radeon AI PRO R9700 (Linux), AMD Ryzen AI Max — Strix Halo
|
||||
(Windows), and Apple Silicon Macs with 64 GB or more, text only ([Mac](docs/DEEPSEEK_MAC.md)). Setup downloads the files and the DeepSeek engine, then starts the model.
|
||||
|
||||
```sh
|
||||
strata launch --family deepseek41 --model ist-das-2.00bpw --plan
|
||||
@@ -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.7-windows-x64-setup.exe` · `Strata-0.2.7-linux-x86_64.AppImage` · `Strata-0.2.7-macos-arm64.dmg`</sub>
|
||||
<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>
|
||||
|
||||
<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">
|
||||
@@ -158,6 +158,7 @@ Follow the prompts to download and start a model. [Terminal guide](docs/TERMINAL
|
||||
| **Intel Arc · Linux** | [Intel setup](docs/INTEL_ARC.md) |
|
||||
| **Apple Silicon · M1 or newer** | [Mac setup](docs/MAC.md) |
|
||||
| **AMD Ryzen AI Max · Strix Halo** | [Strix Halo setup](docs/STRIX_HALO.md) |
|
||||
| **NVIDIA DGX Spark · Linux** | [DGX Spark setup](docs/DGX_SPARK_LINUX.md) |
|
||||
|
||||
<a id="using-it"></a>
|
||||
|
||||
|
||||
+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.7-windows-x64-setup.exe` · `Strata-0.2.7-linux-x86_64.AppImage` · `Strata-0.2.7-macos-arm64.dmg`</sub>
|
||||
<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>
|
||||
|
||||
<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.7-windows-x64-setup.exe` · `Strata-0.2.7-linux-x86_64.AppImage` · `Strata-0.2.7-macos-arm64.dmg`</sub>
|
||||
<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>
|
||||
|
||||
<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.7-windows-x64-setup.exe` · `Strata-0.2.7-linux-x86_64.AppImage` · `Strata-0.2.7-macos-arm64.dmg`</sub>
|
||||
<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>
|
||||
|
||||
<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.7",
|
||||
"version": "0.2.8",
|
||||
"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.7"
|
||||
version = "0.2.8"
|
||||
dependencies = [
|
||||
"gtk",
|
||||
"serde",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "strata-desktop"
|
||||
version = "0.2.7"
|
||||
version = "0.2.8"
|
||||
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.7",
|
||||
"version": "0.2.8",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "strata",
|
||||
"version": "0.2.7",
|
||||
"version": "0.2.8",
|
||||
"dependencies": {
|
||||
"html-to-image": "^1.11.13",
|
||||
"katex": "^0.19.0",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "strata",
|
||||
"private": true,
|
||||
"version": "0.2.7",
|
||||
"version": "0.2.8",
|
||||
"type": "module",
|
||||
"description": "Strata \u2014 local AI, tuned to one model. Web shell + engine host.",
|
||||
"scripts": {
|
||||
|
||||
@@ -62,6 +62,18 @@ export function parsePcieThroughput(raw) {
|
||||
}
|
||||
async function read(path) { try {return (await readFile(path,'utf8')).trim();} catch {return '';} }
|
||||
async function list(path) {try{return await readdir(path);}catch{return [];}}
|
||||
/** A server's remote-console chip (ASPEED), a virtual display or a framebuffer is not a graphics card: no maker we know and no memory of its own. */
|
||||
export function isComputeAdapter(vendor,vramBytes) { return vendor!=='unknown'||vramBytes>0; }
|
||||
/**
|
||||
* The temperature of an amdgpu card from its hwmon inputs [{label, milli}]: the edge sensor (what nvtop and rocm-smi show), else the
|
||||
* junction, else the first one that has a plausible value. Cards such as the Instinct line have no edge sensor. 0 is no reading.
|
||||
*/
|
||||
export function pickGpuTemperature(inputs) {
|
||||
const ok=inputs.filter(i=>i.milli!==null&&i.milli>0&&i.milli<=150000);
|
||||
const first=re=>ok.find(i=>re.test(i.label??''));
|
||||
const chosen=first(/edge|^gpu/i)??ok.find(i=>!i.label)??first(/junction|hotspot/i)??ok.find(i=>!/mem/i.test(i.label??''));
|
||||
return chosen?chosen.milli/1000:null;
|
||||
}
|
||||
export async function detectDrmGpus() {
|
||||
const result=[];
|
||||
for(const card of (await list('/sys/class/drm')).filter(n=>/^card\d+$/.test(n))) {
|
||||
@@ -71,6 +83,7 @@ export async function detectDrmGpus() {
|
||||
const vendor={'0x1002':'amd','0x8086':'intel','0x10de':'nvidia'}[vendorId]??'unknown';
|
||||
const name=await probe('lspci',['-s',busId]);
|
||||
const bytes=number(await read(`${device}/mem_info_vram_total`));
|
||||
if(!isComputeAdapter(vendor,bytes))continue;
|
||||
result.push({id:`drm:${busId}`,index:result.length,busId,vendor,name:name.replace(/^[^ ]+\s+[^:]+:\s*/,'')||`${vendor.toUpperCase()} GPU`,vramGb:bytes===null?0:round(bytes/1024**3,1),unified:vendor==='intel'&&!(bytes>0),backend:'vulkan',sensorPath:device});
|
||||
}
|
||||
return result;
|
||||
@@ -89,7 +102,7 @@ export async function readGpuTelemetry(gpus, { transfer: wantTransfer = true, ru
|
||||
if(gpu.vendor==='nvidia')reading={...reading,pcieRxMbs:transfer.get(gpu.sensorIndex??gpu.index)??null};
|
||||
if(gpu.luid&&!nvidia.has(gpu.id)) {
|
||||
const counters=windowsGpuReading(gpu.luid);
|
||||
if(counters)reading={...reading,utilizationPct:counters.utilizationPct,memoryUsedGb:round(counters.memoryUsedGb,2)};
|
||||
if(counters)reading={...reading,utilizationPct:counters.utilizationPct,memoryUsedGb:round(counters.memoryUsedGb,2),temperatureC:counters.temperatureC??null};
|
||||
}
|
||||
if(gpu.sensorPath&&!nvidia.has(gpu.id)) {
|
||||
let temperature=null,power=null,powerLimit=null,fanPwm=null,clockHz=null;
|
||||
@@ -99,12 +112,9 @@ export async function readGpuTelemetry(gpus, { transfer: wantTransfer = true, ru
|
||||
powerLimit ??= number(await read(`${folder}/power1_cap`));
|
||||
fanPwm ??= number(await read(`${folder}/pwm1`)); // 0-255, the fan's duty as the amdgpu driver reports it
|
||||
clockHz ??= number(await read(`${folder}/freq1_input`)); // the graphics clock in Hz
|
||||
for(const input of inputs) {
|
||||
const label=await read(`${folder}/${input.replace('_input','_label')}`);
|
||||
if(label&&!/edge|gpu/i.test(label))continue;
|
||||
const milli=number(await read(`${folder}/${input}`));
|
||||
if(milli!==null&&milli>=0&&milli<=150000){temperature=milli/1000;break;}
|
||||
}
|
||||
const sensors=[];
|
||||
for(const input of inputs)sensors.push({label:await read(`${folder}/${input.replace('_input','_label')}`),milli:number(await read(`${folder}/${input}`))});
|
||||
temperature=pickGpuTemperature(sensors);
|
||||
if(temperature!==null)break;
|
||||
}
|
||||
const utilization=number(await read(`${gpu.sensorPath}/gpu_busy_percent`)),bytes=number(await read(`${gpu.sensorPath}/mem_info_vram_used`));
|
||||
|
||||
@@ -1,12 +1,25 @@
|
||||
const MEASURES=['temperatureC','utilizationPct','memoryUsedGb','powerW','powerLimitW','pcieGen','pcieGenMax','pcieWidth','pcieRxMbs'];
|
||||
/** The engine's numbers win; a number the engine leaves out comes from the host's own reading of the same card. */
|
||||
export function mergeOwnReading(engine,own) {
|
||||
if(!own)return engine;
|
||||
const merged={...own,...Object.fromEntries(Object.entries(engine).filter(([,v])=>v!==null&&v!==undefined))};
|
||||
const engineGave=MEASURES.some(k=>typeof engine[k]==='number');
|
||||
return {...merged,source:engineGave?'engine':own.source};
|
||||
}
|
||||
|
||||
/** Translate the original Strata /metrics dashboard without inventing missing sensors. */
|
||||
export function originalDashboard(raw, hardware, fallback) {
|
||||
const live=raw.live,engine=raw.engine,hw=raw.hardware??{},history=raw.history??{},cache=raw.conversation_cache,totals=raw.totals;
|
||||
const finite=n=>typeof n==='number'&&Number.isFinite(n);
|
||||
const gb=n=>finite(n)?n/1024**3:null;
|
||||
const devices=hardware?.gpus??[];
|
||||
// The host's own sensor readings (nvidia-smi, amdgpu sysfs, the Windows GPU counters) fill in what the engine does not report:
|
||||
// an AMD card on Windows has no engine reader at all, and the engine never gives the fan or the clock (#3727).
|
||||
const own=new Map((fallback?.hardware?.gpus??[]).map(r=>[r.id,r]));
|
||||
const readings=(hw.gpus??(devices.length===1?[{index:devices[0].index,util:hw.gpu_util,mem_used:hw.gpu_mem_used,temp:hw.gpu_temp,power:hw.gpu_power,power_limit:hw.gpu_power_limit,pcie_gen:hw.gpu_pcie_gen,pcie_gen_max:hw.gpu_pcie_gen_max,pcie_width:hw.gpu_pcie_width,pcie_rx_mb:hw.gpu_pcie_rx_mb}]:[])).map(g=>{
|
||||
const device=devices.find(d=>d.index===g.index);
|
||||
return {id:device?.id??`${device?.vendor??'unknown'}:${g.index}`,source:'engine',sampledAt:new Date().toISOString(),temperatureC:g.temp??null,utilizationPct:g.util??null,memoryUsedGb:gb(g.mem_used),powerW:g.power??null,powerLimitW:g.power_limit??null,pcieGen:g.pcie_gen??null,pcieGenMax:g.pcie_gen_max??null,pcieWidth:g.pcie_width??null,pcieRxMbs:g.pcie_rx_mb??null};
|
||||
const id=device?.id??`${device?.vendor??'unknown'}:${g.index}`;
|
||||
return mergeOwnReading({id,source:'engine',sampledAt:new Date().toISOString(),temperatureC:g.temp??null,utilizationPct:g.util??null,memoryUsedGb:gb(g.mem_used),powerW:g.power??null,powerLimitW:g.power_limit??null,pcieGen:g.pcie_gen??null,pcieGenMax:g.pcie_gen_max??null,pcieWidth:g.pcie_width??null,pcieRxMbs:g.pcie_rx_mb??null},own.get(id));
|
||||
});
|
||||
const last=raw.requests?.[0],used=live.state!=='idle'&&live.state!=='unloaded'?(live.prompt_tokens??0)+(live.generated??0):(last?.prompt_tokens??0)+(last?.output_tokens??0);
|
||||
const maximum=engine.max_context??fallback.context.maxTokens;
|
||||
@@ -20,7 +33,7 @@ export function originalDashboard(raw, hardware, fallback) {
|
||||
generation:{done:live.generated??null,total:live.max_tokens??null},
|
||||
queue:{running:live.running??(['reading','generating'].includes(live.state)?1:0),waiting:live.queued??0},context:{usedTokens:used,maxTokens:maximum,pct:maxPct(used,maximum)},
|
||||
expertCache:{used:engine.expert_slots??0,total:engine.experts??0,memoryGb:finite(engine.expert_cache_mib)?engine.expert_cache_mib/1024:null,source:finite(engine.expert_slots)?'engine':'none'},
|
||||
hardware:{...fallback.hardware,gpus:readings.length?readings:fallback.hardware.gpus,gpuLoad:hw.gpu_util??null,vramUsedGb:gb(hw.gpu_mem_used),vramTotalGb:gb(hw.gpu_mem_total)??fallback.hardware.vramTotalGb,gpuTempC:hw.gpu_temp??null,powerW:hw.gpu_power??null,powerLimitW:hw.gpu_power_limit??null,pcieRxMbs:hw.gpu_pcie_rx_mb??null,cpuLoad:hw.cpu??fallback.hardware.cpuLoad,ramUsedGb:gb(hw.ram_used)??fallback.hardware.ramUsedGb,ramTotalGb:gb(hw.ram_total)??fallback.hardware.ramTotalGb,diskReadGbs:finite(hw.disk_read_mb)?hw.disk_read_mb/1024:null,diskWriteMbs:hw.disk_write_mb??null},
|
||||
hardware:{...fallback.hardware,gpus:readings.length?readings:fallback.hardware.gpus,gpuLoad:hw.gpu_util??fallback.hardware?.gpuLoad??null,vramUsedGb:gb(hw.gpu_mem_used)??fallback.hardware?.vramUsedGb??null,vramTotalGb:gb(hw.gpu_mem_total)??fallback.hardware.vramTotalGb,gpuTempC:hw.gpu_temp??fallback.hardware?.gpuTempC??null,powerW:hw.gpu_power??fallback.hardware?.powerW??null,powerLimitW:hw.gpu_power_limit??fallback.hardware?.powerLimitW??null,pcieRxMbs:hw.gpu_pcie_rx_mb??fallback.hardware?.pcieRxMbs??null,cpuLoad:hw.cpu??fallback.hardware.cpuLoad,ramUsedGb:gb(hw.ram_used)??fallback.hardware.ramUsedGb,ramTotalGb:gb(hw.ram_total)??fallback.hardware.ramTotalGb,diskReadGbs:finite(hw.disk_read_mb)?hw.disk_read_mb/1024:null,diskWriteMbs:hw.disk_write_mb??null},
|
||||
history:samples.length?samples:fallback.history,requests,
|
||||
totals:totals?{...fallback.totals,requests:totals.requests??0,tokensIn:totals.prompt_tokens??0,tokensOut:totals.output_tokens??0,cachedTokens:totals.reused??0,uncachedTokens:(totals.prompt_tokens??0)-(totals.reused??0),promptMs:totals.prompt_ms,decodeMs:totals.decode_ms,since:totals.since?new Date(totals.since*1000).toISOString():fallback.totals.since}:fallback.totals,
|
||||
conversationCache:cache?{source:'engine',enabled:cache.enabled,parked:cache.parked,slots:cache.slots,memoryGb:gb(cache.bytes),budgetGb:finite(cache.budget_mib)?cache.budget_mib/1024:null,parks:cache.parks,restores:cache.restores,evictions:cache.evictions,lastEvent:cache.last_event,lastAt:cache.last_at,lastTokens:cache.last_tokens,requests:cache.requests,requestsReused:cache.requests_reused,lastPrompt:cache.last_prompt,lastReused:cache.last_reused}:fallback.conversationCache,
|
||||
|
||||
@@ -13,6 +13,7 @@ import { USER_KEY, capture as captureUserArgs, merge as mergeUserArgs, setFlag a
|
||||
import { resolvePython, venvPython } from './python-resolver.mjs';
|
||||
import os from 'node:os';
|
||||
import { runtimeVision } from './native-vision.mjs';
|
||||
import { dsEngineArgs, dsUserArgs, dsArgsNote, withoutVisionFlag } from './native-ds-args.mjs';
|
||||
import { componentsDirectory, installerEnvironment, controlEnvironment, sourceServerEnvironment } from './control-runtime.mjs';
|
||||
import { createEngineStep } from './engine-step.mjs';
|
||||
import { dataFolderFromPick, isDriveRoot, makeFolder, rememberDataFolder, syncDataFolder, terminalSettingsFile } from './model-folder.mjs';
|
||||
@@ -223,6 +224,49 @@ return {summary:result.summary??'Deleted',removed:result.plan?.gb??null};}
|
||||
const DS_EXPERTS_MIN=[0,1,50];
|
||||
function dsEngineVersion(config){const m=/engine-dsv41[\\/](\d+(?:\.\d+)+)-/.exec(String(config?.exe??''))||/engine-dsv41[\\/](\d+(?:\.\d+)+)-/.exec(String((config?.lib_dirs??[])[0]??''));return m?m[1].split('.').map(Number):null;}
|
||||
function dsExpertsSupported(config){const v=dsEngineVersion(config);if(!v)return false;for(let i=0;i<DS_EXPERTS_MIN.length;i++){const a=v[i]??0,b=DS_EXPERTS_MIN[i];if(a!==b)return a>b;}return true;}
|
||||
/* Control's ceiling for one start wait, read from the Control this app actually found rather than guessed from its version. The
|
||||
app starts a model with `strata runtime start --wait`, and that wait is checked three times before the engine is reached: by
|
||||
management.runtime_action, by the operation record written before the start (journal), and by runtime.start itself. 27cbbfe6
|
||||
raised only the runtime's, so a longer wait died with `start wait must be finite seconds in 0..600` and then with
|
||||
`direct runtime intent has an invalid timeout`. The app therefore asks for the longer wait only when a Control's own sources
|
||||
say all three agree, and keeps today's 600 s whenever one of them still names a ceiling of its own. Reading the installed
|
||||
Control to learn what it supports is what the field list and the /v1/responses capability already do. */
|
||||
const START_WAIT_CEILING=600;
|
||||
let ceilingCache={dir:'',time:0,value:START_WAIT_CEILING};
|
||||
function followsRuntimeCeiling(text,marker){
|
||||
/* The marker picks the one line that bounds the wait (`"wait_s": (` in the record's range table, `"start wait"` in the CLI
|
||||
validator), because both files name those keys elsewhere. A line that still spells out 600 is a ceiling of its own. */
|
||||
const line=(String(text).split(/\r?\n/).find(l=>l.includes(marker))??'');
|
||||
return /runtime\.MAX_START_WAIT|start_wait_max/.test(line)&&!/\b600\b/.test(line);
|
||||
}
|
||||
function startWaitCeiling(){
|
||||
const dir=control?.control?.dir??'';if(!dir)return START_WAIT_CEILING;
|
||||
const files=['management.py','journal.py','runtime.py'].map(name=>join(dir,'strata_control',name));
|
||||
let time=0;
|
||||
try{time=Math.max(...files.map(f=>statSync(f).mtimeMs));}catch{return START_WAIT_CEILING;}
|
||||
if(ceilingCache.dir===dir&&ceilingCache.time===time)return ceilingCache.value;
|
||||
let value=START_WAIT_CEILING;
|
||||
try{
|
||||
const [management,journal]=files.slice(0,2).map(f=>readFileSync(f,'utf8'));
|
||||
// both gates must hand the ceiling to the runtime, or the wait would be refused on the way to it
|
||||
if(followsRuntimeCeiling(management,'"start wait"')&&followsRuntimeCeiling(journal,'"wait_s": (')){
|
||||
const declared=Number(readFileSync(files[2],'utf8').match(/^MAX_START_WAIT\s*=\s*(\d+)/m)?.[1]);
|
||||
if(Number.isFinite(declared)&&declared>value)value=declared;
|
||||
}
|
||||
}catch{value=START_WAIT_CEILING;}
|
||||
ceilingCache={dir,time,value};return value;
|
||||
}
|
||||
/* How long a start waits for this model, in the same shape as `strata launch`'s figure (#3949 #3459 #4191): ten minutes, about eight
|
||||
more seconds per GB above 40 GB, never more than this Control accepts. A model nobody can size keeps the 300 s it always had.
|
||||
The run config's engine_ready_s is the floor, not the ceiling: the server reads that number and ends the engine when it runs out,
|
||||
so the start must never give up before the server does (setup writes 3600 for DeepSeek V4.1-Flash). */
|
||||
function startWaitFor(config,entry,model){
|
||||
const ceiling=startWaitCeiling(),ready=Number(config?.engine_ready_s)||0;
|
||||
const known=Number(entry?.diskGb),fromDisk=Number.isFinite(known)&&known>0?known:0;
|
||||
const gb=fromDisk||Number((snapshot.catalog?.families??[]).flatMap(f=>f.sizes??[]).find(s=>s.id===model)?.download_gb)||0;
|
||||
const sized=gb>40?Math.max(300,600+(gb-40)*8):300;
|
||||
return Math.trunc(Math.max(300,Math.min(ceiling,Math.max(ready,sized))));
|
||||
}
|
||||
function buildConfig(args,config){
|
||||
const vision=runtimeVision(config,args.vision);
|
||||
const visionPreferences=args.vision!==undefined?{ui_vision_enabled:args.vision!=='no',ui_vision_device:args.vision==='no'?config.ui_vision_device??'yes':args.vision}:{};
|
||||
@@ -243,18 +287,22 @@ return {summary:result.summary??'Deleted',removed:result.plan?.gb??null};}
|
||||
if(cfg.vision){if(cfg.vision_tokens!=null)cfg.vision={...cfg.vision,max_tokens:cfg.vision_tokens};if(cfg.vision_min_tokens!=null)cfg.vision={...cfg.vision,min_tokens:cfg.vision_min_tokens};}cfg.mcp={...(cfg.mcp??{}),...(cfg.mcp_timeout_s!=null?{timeout_s:cfg.mcp_timeout_s}:{}),...(cfg.mcp_max_rounds!=null?{max_rounds:cfg.mcp_max_rounds}:{}),...(cfg.mcp_max_result_chars!=null?{max_result_chars:cfg.mcp_max_result_chars}:{})};
|
||||
/* The model's own choices win over what setup wrote into the arguments: "Draft layer (MTP)" off leaves --mtp out of every start, and a picture reading that is off leaves --vision out (setup writes both again whenever it runs; the saved settings are applied here, on every load). */
|
||||
if((cfg.mtp===false||cfg.drafting==='off')&&Array.isArray(cfg.args))cfg.args=withoutDraftLayer(cfg.args);
|
||||
if(!cfg.vision&&Array.isArray(cfg.args))cfg.args=cfg.args.filter(a=>a!=='--vision');
|
||||
/* The Qwen engine's --vision stands alone, so it goes (with a picture word after it, if an older file has one). The DeepSeek engine's is a pair, `--vision on|off`, that setup wrote and the engine reads: it stays whole (taking out only the name left `off` behind: `--eos 1 off`, "unknown option off", #4358 #4359 #4505 #4507). */
|
||||
if(!cfg.vision&&!ownEngine(config)&&Array.isArray(cfg.args))cfg.args=withoutVisionFlag(cfg.args);
|
||||
for(const arg of cfg.args??[])if(arg.endsWith('.gguf')&&!existsSync(resolve(repo(),arg)))throw new Error('An engine argument names a missing GGUF. Restore the saved arguments or repair the files.');
|
||||
/* The app's own choice of request slots (4, fewer where the card has no room) is part of the configuration, so a tune measures the engine a load will start: slots take VRAM from the expert cache. */
|
||||
const parallelChoice=packaged&&!ownEngine(config)?chooseParallel({config:cfg,values:args.values??{},hardware:snapshot.hardware?.gpus?.length?snapshot.hardware:{gpus:hardwareProbe()??[]}}):null;
|
||||
let parallelNote=null;
|
||||
let parallelNote=null,dsNote='';
|
||||
/* The agent asks for long answers (a thinking model can use most of the window): let the engine clamp the output to what is left of the context instead of refusing the request. */
|
||||
if(packaged&&!ownEngine(config)&&cfg.fit_max_tokens===undefined)cfg.fit_max_tokens=true;
|
||||
if(parallelChoice){const applied=applyParallel(cfg,parallelChoice);for(const k of Object.keys(cfg))if(!(k in applied.cfg))delete cfg[k];Object.assign(cfg,applied.cfg);parallelNote=applied.note;}
|
||||
/* DeepSeek's Speed choice: the engine decides by itself (--experts auto: 5 when the cards total 16 GB or less, else 6), so nothing is passed unless the person chose 5 or 6. */
|
||||
if(ownEngine(config)){const list=[];const old=Array.isArray(cfg.args)?cfg.args:[];for(let i=0;i<old.length;i++){if(old[i]==='--experts'){i++;continue;}list.push(old[i]);}if(dsExpertsSupported(config)&&(cfg.ds_experts==='5'||cfg.ds_experts==='6'))list.push('--experts',cfg.ds_experts);else if(!dsExpertsSupported(config)&&old.includes('--experts'))list.splice(0,list.length,...old);cfg.args=list;delete cfg.ds_experts;}
|
||||
if(ownEngine(config)){const list=[];const old=Array.isArray(cfg.args)?cfg.args:[];for(let i=0;i<old.length;i++){if(old[i]==='--experts'){i++;continue;}list.push(old[i]);}if(dsExpertsSupported(config)&&(cfg.ds_experts==='5'||cfg.ds_experts==='6'))list.push('--experts',cfg.ds_experts);else if(!dsExpertsSupported(config)&&old.includes('--experts'))list.splice(0,list.length,...old);cfg.args=list;delete cfg.ds_experts;
|
||||
/* Only the options strata-dsv41 takes go to it (#4504): --vram-reserve-mib is its --reserve-mib, and --pool-workers and the other Qwen-engine names are left out, in the arguments and in the person's saved options. */
|
||||
const built=dsEngineArgs(cfg.args);cfg.args=built.args;dsNote=dsArgsNote(built);
|
||||
if(Array.isArray(cfg[USER_KEY])){const kept=dsUserArgs(cfg[USER_KEY]);if(kept.length)cfg[USER_KEY]=kept;else delete cfg[USER_KEY];}}
|
||||
else delete cfg.ds_experts;
|
||||
return {cfg,resources,visionPreferences,parallelChoice,parallelNote};
|
||||
return {cfg,resources,visionPreferences,parallelChoice,parallelNote,dsNote};
|
||||
}
|
||||
/* The DFlash2 drafter, before a load that uses it: Control says whether the choice needs it (and the file is not here), downloads it pinned and checked, and packs it, with plain progress in the job.
|
||||
Never stops the load: a drafter that cannot be had leaves the MTP running, and the job says so. The choice itself is remembered in the model's settings file. */
|
||||
@@ -279,7 +327,7 @@ return {summary:result.summary??'Deleted',removed:result.plan?.gb??null};}
|
||||
await refresh(true);if(job.state!=='running')return;const {entry,path:configFile}=recordFile(args.model);const config=checkedSettingsConfig(readFileSync(configFile,'utf8'));if(packaged){const engine=await control.engine();if(!engine.usable)throw new Error(ENGINE_NOT_RECOGNISED+(engine.notice?` ${engine.notice}`:''));}if(!entry.ready){const error=missingFilesError(args.model,entry.missing);append(args.model,error.details[0]);throw error;}
|
||||
if(externalRunning().length)throw new Error('A Strata server is already running outside this UI. Stop it before loading here, or connect its API in Settings → Models.');
|
||||
if(args.multiple&&instances.size>=8)throw new Error('Unload an instance before adding another. Up to eight native instances can run here.');
|
||||
const {cfg,resources,visionPreferences,parallelChoice,parallelNote:chosenNote}=buildConfig(args,config);
|
||||
const {cfg,resources,visionPreferences,parallelChoice,parallelNote:chosenNote,dsNote}=buildConfig(args,config);
|
||||
let parallelNote=chosenNote;
|
||||
await prepareDrafter(job,args,cfg,configFile);if(job.state!=='running')return;
|
||||
const fingerprint=createHash('sha256').update(JSON.stringify({...cfg,api_key:undefined,port:undefined})).digest('hex');
|
||||
@@ -300,12 +348,19 @@ return {summary:result.summary??'Deleted',removed:result.plan?.gb??null};}
|
||||
/* Another model on the same graphics card: the engine takes the card's free memory, so a second one usually finds none. Loading goes on (recommend, never force); if it fails, the reason is said. */
|
||||
const sharedWith=instance.gpus.length?[...instances.values()].find(i=>i.id!==id&&i.status!=='error'&&i.model!==args.model&&i.gpus.some(g=>instance.gpus.includes(g))):null;
|
||||
const sharedSentence=sharedWith?`GPU ${instance.gpus.find(g=>sharedWith.gpus.includes(g))+1} is already used by ${sharedWith.label}, so this model did not fit there. Load it on another GPU, or unload ${sharedWith.label} first.`:null;
|
||||
let proc=null,started=null,retried=null;
|
||||
let proc=null,started=null,retried=null,startWait=null;
|
||||
if(packaged){
|
||||
/* How long this start waits, and the server's own engine wait kept above it. The server ends the engine when its engine wait runs
|
||||
out (900 s, or the config's engine_ready_s), so a start that waits longer than that can never win: as in `strata launch`, the
|
||||
server gets a wait above the start's. It is only ever raised, never lowered, so a config that already names a longer one
|
||||
(setup's 3600 for DeepSeek V4.1-Flash) is left alone. */
|
||||
startWait=startWaitFor(config,entry,args.model);
|
||||
if(startWait+60>Math.max(900,Number(cfg.engine_ready_s)||0))cfg.engine_ready_s=startWait+60;
|
||||
/* The engine starts under Control's owner runtime, not as our child: a saved profile per slot, then `runtime start`. */
|
||||
const used=new Set([...instances.values()].map(i=>i.profile));let slot=1;while(used.has('app-'+slot))slot++;instance.profile='app-'+slot;
|
||||
instances.set(id,instance);if(!defaultId||!args.multiple||wasDefault)defaultId=id;emit();
|
||||
const startWith=value=>{writeFileSync(configPath,JSON.stringify(value,null,2),{mode:0o600});return control.stop(instance.profile).catch(()=>{}).then(()=>control.saveProfile(instance.profile,configPath)).then(()=>control.start(instance.profile,{waitSeconds:ownEngine(config)?Math.min(600,Math.max(300,Number(config.engine_ready_s)||300)):300}));};
|
||||
if(dsNote)append(id,dsNote);
|
||||
const startWith=value=>{writeFileSync(configPath,JSON.stringify(value,null,2),{mode:0o600});return control.stop(instance.profile).catch(()=>{}).then(()=>control.saveProfile(instance.profile,configPath)).then(()=>control.start(instance.profile,{waitSeconds:startWait}));};
|
||||
started=startWith(cfg);
|
||||
started.then(()=>append(id,'Strata Control reports the engine is ready.'),error=>{append(id,error.message);});
|
||||
try{await started;}catch(firstError){let error=firstError;
|
||||
@@ -397,16 +452,19 @@ return {summary:result.summary??'Deleted',removed:result.plan?.gb??null};}
|
||||
function writeModelConfig(path,next){const mine=mergeUserArgs(captureUserArgs(readJson(path)),next[USER_KEY]??[]);next=stampedUserArgs(next,mine);copyFileSync(path,path+'.bak');chmodSync(path+'.bak',0o600);const temp=path+'.ui-tmp';writeFileSync(temp,JSON.stringify(next,null,2)+'\n',{mode:0o600});renameSync(temp,path);}
|
||||
async function tune(job,args){
|
||||
await refresh(true);if(job.state!=='running')return;
|
||||
const chosen=record(args.model);resourceSettings(chosen.config,snapshot.hardware,args.profile,modelArena(args.model));
|
||||
const chosen=record(args.model);
|
||||
/* The resource preference writes the Qwen engine's options (--pool-workers, --vram-reserve-mib); the DeepSeek engine takes neither (#4504), so its settings file is left as it is. */
|
||||
const resourceFor=config=>ownEngine(config)?{}:resourceSettings(config,snapshot.hardware,args.profile,modelArena(args.model));
|
||||
resourceFor(chosen.config);
|
||||
if(!chosen.entry.ready)throw new Error('Prepare the model before tuning.');
|
||||
if(externalRunning().length)throw new Error('Stop the native server outside this UI before tuning.');
|
||||
if(busy(requests().filter(r=>instances.has(r.instanceId))))throw new Error('Finish requests before tuning.');
|
||||
for(const id of [...instances.keys()])await unload(id);
|
||||
if(job.state!=='running')return;
|
||||
const initialValues=resourceSettings(chosen.config,snapshot.hardware,args.profile,modelArena(args.model));writeModelConfig(chosen.path,{...chosen.config,...initialValues,ui_resource_profile:args.profile});
|
||||
const initialValues=resourceFor(chosen.config);writeModelConfig(chosen.path,{...chosen.config,...initialValues,ui_resource_profile:args.profile});
|
||||
if(args.calibrate!==false&&state().capabilities.calibration){job.summary='Measuring the best engine settings for this computer…';emit();await program(job,args);}
|
||||
if(job.state!=='running')return;
|
||||
const current=record(args.model),values=resourceSettings(current.config,snapshot.hardware,args.profile,modelArena(args.model));
|
||||
const current=record(args.model),values=resourceFor(current.config);
|
||||
const next={...current.config,...values,ui_resource_profile:args.profile};
|
||||
writeModelConfig(current.path,next);
|
||||
job.summary='Applying your resource preference and loading the model…';emit();
|
||||
|
||||
@@ -0,0 +1,91 @@
|
||||
/** The engine options of the DeepSeek-V4.1 engine (strata-dsv41), and the two ways the app used to hand it options it does not take.
|
||||
*
|
||||
* strata-dsv41 is its own program (src/dsv41/ds4_main.cpp) with its own short option list, and it exits with "unknown option X" (code 2)
|
||||
* on anything else. The Qwen engine takes other names (--vram-reserve-mib, --pool-workers, --resident-budget-gib, ...), and the app's
|
||||
* settings, resource profile and tune presets write those into a model's "args" and "user_args". Before the load starts, the arguments
|
||||
* of a DeepSeek model are built from what the engine takes (#4504): a name it spells differently is renamed, anything else is left out
|
||||
* with its value and said in the job's log. test/native-ds-args.test.mjs reads ds4_main.cpp and fails when this list and the engine drift.
|
||||
*/
|
||||
|
||||
/** Options that take one value. */
|
||||
export const DS_VALUE_FLAGS = new Set([
|
||||
'--pack', '--gpus', '--max-context', '--reserve-mib', '--vram-experts', '--cpu-threads', '--expert-profile', '--ssd-cache', '--ssd-policy',
|
||||
'--eos', '--dspark', '--dspark-conf', '--dspark-block', '--dspark-depth', '--experts', '--tp', '--prefill-chunk', '--threads', '--vision',
|
||||
'--mmap-experts', '--layer-split',
|
||||
]);
|
||||
/** Options that stand alone. */
|
||||
export const DS_SWITCH_FLAGS = new Set(['--no-expert-cache', '--profile-readonly', '--verbose', '-v']);
|
||||
/** The Qwen engine's names for settings the DeepSeek engine has under another name (same meaning). */
|
||||
export const DS_RENAMED = { '--vram-reserve-mib': '--reserve-mib' };
|
||||
|
||||
const isFlag = text => typeof text === 'string' && text.startsWith('-') && text.length > 1 && /^-{1,2}[A-Za-z]/.test(text);
|
||||
/** The words that follow `flag` at `at` up to the next option: its value (one word, or none for a switch at the end of the list). */
|
||||
function valueSpan(list, at) {
|
||||
return at + 1 < list.length && !isFlag(list[at + 1]) ? 1 : 0;
|
||||
}
|
||||
|
||||
/** `args` for strata-dsv41: accepted options kept (a renamed one under the engine's name, unless the list already has that name),
|
||||
* everything else left out together with its value. Returns {args, dropped: [{flag, value}], renamed: [{from, to}]}. */
|
||||
export function dsEngineArgs(args) {
|
||||
const list = (Array.isArray(args) ? args : []).map(String);
|
||||
const out = [], dropped = [], renamed = [];
|
||||
const has = new Set(list.filter(isFlag));
|
||||
for (let i = 0; i < list.length; i++) {
|
||||
const item = list[i];
|
||||
if (!isFlag(item)) { out.push(item); continue; } // a stray word: kept as it was (the engine says what it is)
|
||||
const own = DS_RENAMED[item];
|
||||
if (own) {
|
||||
const n = valueSpan(list, i), value = n ? list[i + 1] : null;
|
||||
i += n;
|
||||
if (has.has(own)) { dropped.push({ flag: item, value, because: `${own} is set` }); continue; }
|
||||
out.push(own, ...(value === null ? [] : [value]));
|
||||
renamed.push({ from: item, to: own });
|
||||
continue;
|
||||
}
|
||||
const takes = DS_VALUE_FLAGS.has(item), alone = DS_SWITCH_FLAGS.has(item);
|
||||
if (takes || alone) {
|
||||
out.push(item);
|
||||
if (takes && i + 1 < list.length && !isFlag(list[i + 1])) out.push(list[++i]);
|
||||
continue;
|
||||
}
|
||||
const n = valueSpan(list, i);
|
||||
dropped.push({ flag: item, value: n ? list[i + 1] : null });
|
||||
i += n;
|
||||
}
|
||||
return { args: out, dropped, renamed };
|
||||
}
|
||||
|
||||
/** The person's own options of a DeepSeek model: unaccepted ones leave the list (so the settings file heals itself); the app's
|
||||
* --vram-reserve-mib stays, because the Settings field writes it under that name and it is renamed at every load. */
|
||||
export function dsUserArgs(user) {
|
||||
const list = (Array.isArray(user) ? user : []).map(String);
|
||||
const out = [];
|
||||
for (let i = 0; i < list.length; i++) {
|
||||
const item = list[i];
|
||||
if (!isFlag(item)) { out.push(item); continue; }
|
||||
const n = valueSpan(list, i);
|
||||
const keep = DS_VALUE_FLAGS.has(item) || DS_SWITCH_FLAGS.has(item) || item in DS_RENAMED;
|
||||
if (keep) out.push(item, ...list.slice(i + 1, i + 1 + n));
|
||||
i += n;
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
/** One line for the job's log about what was changed, or '' when nothing was. */
|
||||
export function dsArgsNote({ dropped = [], renamed = [] }) {
|
||||
const parts = [];
|
||||
if (renamed.length) parts.push(`${renamed.map(r => `${r.from} is passed as ${r.to}`).join(', ')}`);
|
||||
if (dropped.length) parts.push(`left out because the DeepSeek engine does not take them: ${dropped.map(d => [d.flag, d.value].filter(v => v !== null && v !== undefined).join(' ')).join(', ')}`);
|
||||
return parts.length ? `DeepSeek engine options: ${parts.join('; ')}.` : '';
|
||||
}
|
||||
|
||||
/** `args` without `flag` and the picture word that follows it (the Qwen engine's --vision stands alone; an older config may carry a value). */
|
||||
const PICTURE_WORDS = new Set(['on', 'off', 'yes', 'no', 'gpu', 'cpu', 'none']);
|
||||
export function withoutVisionFlag(args) {
|
||||
const list = (Array.isArray(args) ? args : []).map(String), out = [];
|
||||
for (let i = 0; i < list.length; i++) {
|
||||
if (list[i] === '--vision') { if (i + 1 < list.length && PICTURE_WORDS.has(list[i + 1].toLowerCase())) i++; continue; }
|
||||
out.push(list[i]);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
@@ -32,7 +32,8 @@ export const ENV_FLAG=/^(?:STRATA|DS4)_[A-Z0-9_]+$/;
|
||||
* The speed features of 0.2.7 that have a switch in Expert settings. A switch has a default ('on' or 'off'); outside it, it writes exactly the
|
||||
* environment entries `env` into the model's settings file ("env"); back in its default it removes them, so a default launch carries none of them.
|
||||
* "Faster long prompts" (Qwen KVA) is OFF by default (owner decision, 26 October): STRATA_KVA=1 turns it on, and an earlier test build's STRATA_KVA=0 reads as off.
|
||||
* The other two are on by default. The names are the engine switch contract (phaseA-tests/notes-027-plan.md); strata_control/speed_switches.py says the
|
||||
* "Faster answers on 12 GB NVIDIA cards" (residency-biased routing) is OFF by default since 0.2.8 (owner decision, 29 October; #4402): STRATA_ROUTE_PRIOR=1.5
|
||||
* turns it on, and the STRATA_ROUTE_PRIOR=0 that 0.2.7's Off wrote is the default state now. The other two are on by default. The names are the engine switch contract (phaseA-tests/notes-027-plan.md); strata_control/speed_switches.py says the
|
||||
* same for the Terminal (tests/terminal/test_speed_switches.py compares the two).
|
||||
*/
|
||||
export const SPEED_SWITCHES=[
|
||||
@@ -40,8 +41,8 @@ export const SPEED_SWITCHES=[
|
||||
hint:'Reads very long prompts faster by approximating part of the work. Off by default: answers about long documents can miss or mix up details.',
|
||||
default:'off',env:{STRATA_KVA:'1'}},
|
||||
{id:'faster_12gb_nvidia',label:'Faster answers on 12 GB NVIDIA cards',group:'Speed',
|
||||
hint:'Prefers the parts of the model already on the graphics card, which makes answers much faster on 12 GB cards. Turn off if answers look worse than before.',
|
||||
default:'on',env:{STRATA_ROUTE_PRIOR:'0'}},
|
||||
hint:'Prefers the parts of the model already on the graphics card, which makes answers faster on 12 GB cards. Off by default: in long agent sessions it can repeat itself and give weaker results. Turn on if you mostly chat and want the speed.',
|
||||
default:'off',env:{STRATA_ROUTE_PRIOR:'1.5'}},
|
||||
{id:'faster_radeon_9000',label:'Faster answers on Radeon RX 9000 / AI PRO R9700',group:'Speed',
|
||||
hint:'Shares the work between the graphics card and the processor in a way that makes answers faster on these cards. Turn off to go back to the previous setting, which is slower.',
|
||||
default:'on',env:{STRATA_PCIE_FRAC_DEFAULT:'old'}},
|
||||
|
||||
@@ -3,7 +3,11 @@
|
||||
* - size: the DirectX adapter list in the registry (HKLM\SOFTWARE\Microsoft\DirectX) has a 64-bit DedicatedVideoMemory and the adapter
|
||||
* LUID. Win32_VideoController.AdapterRAM is 32 bits and reads 0 or 4 GB for a 16 GB card (#3727, #3836).
|
||||
* - load and memory in use: the Windows GPU performance counters (the ones Task Manager shows), read by one long-lived PowerShell and
|
||||
* matched to the card by LUID. Temperature, power and the PCIe link need the vendor's own library, so they stay "not available".
|
||||
* matched to the card by LUID.
|
||||
* - temperature: the display driver's own D3DKMT adapter performance data (gdi32.dll, no vendor library, no administrator), asked for the
|
||||
* same LUID. An AMD 7900 XTX gave the same value as ADLX at idle and under load (noctrex, #3727); the card answers in tenths of a degree.
|
||||
* A driver that does not answer leaves the temperature "not available". Power, fan and the PCIe link stay "not available": D3DKMT gave
|
||||
* 0 for power and for the PCIe rate on that card.
|
||||
*/
|
||||
import { createPowerShellLoop, endWithHost } from './windows-poll.mjs';
|
||||
import { utf8Script } from './powershell-utf8.mjs';
|
||||
@@ -55,20 +59,30 @@ export function windowsOtherGpus(videoRows, dx) {
|
||||
return out;
|
||||
}
|
||||
|
||||
/** One counter line: "G <luid> <load percent> <dedicated bytes in use>". */
|
||||
export function parseGpuCounterLine(line) {
|
||||
const m = /^G\s+(luid_0x[0-9a-f]+_0x[0-9a-f]+)\s+(\d+(?:\.\d+)?)\s+(\d+)\s*$/i.exec(String(line));
|
||||
return m ? { luid: m[1].toLowerCase(), utilizationPct: Math.max(0, Math.min(100, Number(m[2]))), memoryUsedGb: Number(m[3]) / 1024 ** 3 } : null;
|
||||
/** The temperature D3DKMT reports in tenths of a degree -> whole degrees, or null when the driver gave nothing believable. */
|
||||
export function d3dkmtTemperatureC(deci) {
|
||||
const c = Number(deci) / 10;
|
||||
return Number.isFinite(c) && c > 0 && c <= 150 ? Math.round(c) : null;
|
||||
}
|
||||
|
||||
/** One counter line: "G <luid> <load percent> <dedicated bytes in use> [<temperature in tenths of a degree>]". */
|
||||
export function parseGpuCounterLine(line) {
|
||||
const m = /^G\s+(luid_0x[0-9a-f]+_0x[0-9a-f]+)\s+(\d+(?:\.\d+)?)\s+(\d+)(?:\s+(-?\d+))?\s*$/i.exec(String(line));
|
||||
return m ? { luid: m[1].toLowerCase(), utilizationPct: Math.max(0, Math.min(100, Number(m[2]))), memoryUsedGb: Number(m[3]) / 1024 ** 3, temperatureC: d3dkmtTemperatureC(m[4]) } : null;
|
||||
}
|
||||
|
||||
/** The C# that asks D3DKMT (KMTQAITYPE_ADAPTERPERFDATA = 62, 64-byte D3DKMT_ADAPTER_PERFDATA, Temperature at byte 56) for one LUID; -1 = no answer. */
|
||||
export const GPU_TEMP_CSHARP = 'using System;using System.Runtime.InteropServices;public static class StrataGpuTemp{[StructLayout(LayoutKind.Sequential)]struct Q{public uint L;public int H;public uint A;}[StructLayout(LayoutKind.Sequential)]struct Info{public uint A;public int T;public IntPtr P;public uint S;}[DllImport("gdi32.dll")]static extern int D3DKMTOpenAdapterFromLuid(ref Q q);[DllImport("gdi32.dll")]static extern int D3DKMTCloseAdapter(ref uint a);[DllImport("gdi32.dll")]static extern int D3DKMTQueryAdapterInfo(ref Info i);public static int Read(int high,uint low){Q q=new Q();q.L=low;q.H=high;if(D3DKMTOpenAdapterFromLuid(ref q)!=0)return -1;IntPtr buf=Marshal.AllocHGlobal(64);try{for(int k=0;k<64;k++)Marshal.WriteByte(buf,k,0);Info i=new Info();i.A=q.A;i.T=62;i.P=buf;i.S=64;if(D3DKMTQueryAdapterInfo(ref i)!=0)return -1;return Marshal.ReadInt32(buf,56);}finally{Marshal.FreeHGlobal(buf);uint a=q.A;D3DKMTCloseAdapter(ref a);}}}';
|
||||
|
||||
export function createWindowsGpuReader({ spawnFn, intervalSec = 4, maxStarts = 3, now = Date.now } = {}) {
|
||||
const readings = new Map();
|
||||
const script = utf8Script(
|
||||
`$t=$false;try{Add-Type -TypeDefinition '${GPU_TEMP_CSHARP}';$t=$true}catch{};` +
|
||||
`while($true){` +
|
||||
`$e=@(Get-CimInstance Win32_PerfFormattedData_GPUPerformanceCounters_GPUEngine);$m=@(Get-CimInstance Win32_PerfFormattedData_GPUPerformanceCounters_GPUAdapterMemory);` +
|
||||
`$u=@{};foreach($r in $e){if($r.Name -match '(luid_0x[0-9A-Fa-f]+_0x[0-9A-Fa-f]+)_phys_\\d+_eng_\\d+_engtype_(\\w+)'){$k=$matches[1].ToLower()+'|'+$matches[2];$u[$k]=[double]$u[$k]+[double]$r.UtilizationPercentage}};` +
|
||||
`$b=@{};foreach($k in $u.Keys){$l=$k.Split('|')[0];if(-not $b.ContainsKey($l) -or $u[$k] -gt $b[$l]){$b[$l]=$u[$k]}};` +
|
||||
`foreach($r in $m){if($r.Name -match '(luid_0x[0-9A-Fa-f]+_0x[0-9A-Fa-f]+)'){$l=$matches[1].ToLower();[Console]::Out.WriteLine('G '+$l+' '+[math]::Round([double]$b[$l],0)+' '+$r.DedicatedUsage)}};` +
|
||||
`foreach($r in $m){if($r.Name -match '(luid_0x[0-9A-Fa-f]+_0x[0-9A-Fa-f]+)'){$l=$matches[1].ToLower();$d=-1;if($t){try{$p=$l.Split('_');$d=[StrataGpuTemp]::Read([Convert]::ToInt32($p[1].Substring(2),16),[Convert]::ToUInt32($p[2].Substring(2),16))}catch{}};[Console]::Out.WriteLine('G '+$l+' '+[math]::Round([double]$b[$l],0)+' '+$r.DedicatedUsage+' '+$d)}};` +
|
||||
`[Console]::Out.Flush();Start-Sleep -Seconds ${intervalSec}}`);
|
||||
const loop = createPowerShellLoop({ script, spawnFn, maxStarts, onLine: line => {
|
||||
const parsed = parseGpuCounterLine(line);
|
||||
|
||||
@@ -96,7 +96,7 @@ export function SettingsUpdates() {
|
||||
</>;
|
||||
}
|
||||
|
||||
/** "Strata 0.2.7 · Engine 0.1.52": the app's own version first, the engine's beside it (About). */
|
||||
/** "Strata 0.2.8 · Engine 0.1.54": 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);
|
||||
|
||||
@@ -81,5 +81,5 @@
|
||||
"fx80": "Braucht etwa {0} GB freien Speicherplatz. Was nicht in den Speicher passt, wird von Ihrer SSD gelesen.",
|
||||
"fx81": "{0} RAM + VRAM, dazu etwa {1} GB Speicherplatz",
|
||||
"fx82": "Schnellere Antworten auf NVIDIA-Karten mit 12 GB",
|
||||
"fx83": "Bevorzugt die Teile des Modells, die schon auf der Grafikkarte liegen. Das macht Antworten auf Karten mit 12 GB deutlich schneller. Schalten Sie die Option aus, wenn die Antworten schlechter aussehen als vorher."
|
||||
"fx83": "Bevorzugt die Teile des Modells, die schon auf der Grafikkarte liegen. Das macht Antworten auf Karten mit 12 GB schneller. Standardmäßig aus: In langen Agent-Sitzungen kann sich das Modell wiederholen und schwächere Ergebnisse liefern. Schalten Sie die Option ein, wenn Sie vor allem chatten und die Geschwindigkeit wollen."
|
||||
}
|
||||
@@ -81,5 +81,5 @@
|
||||
"fx80": "Needs about {0} GB of free disk space. What does not fit in memory is read from your SSD.",
|
||||
"fx81": "{0} RAM + VRAM, plus about {1} GB of disk",
|
||||
"fx82": "Faster answers on 12 GB NVIDIA cards",
|
||||
"fx83": "Prefers the parts of the model already on the graphics card, which makes answers much faster on 12 GB cards. Turn off if answers look worse than before."
|
||||
"fx83": "Prefers the parts of the model already on the graphics card, which makes answers faster on 12 GB cards. Off by default: in long agent sessions it can repeat itself and give weaker results. Turn on if you mostly chat and want the speed."
|
||||
}
|
||||
@@ -81,5 +81,5 @@
|
||||
"fx80": "Necesita unos {0} GB de espacio libre en disco. Lo que no cabe en memoria se lee de tu SSD.",
|
||||
"fx81": "{0} de RAM + VRAM, más unos {1} GB de disco",
|
||||
"fx82": "Respuestas más rápidas en tarjetas NVIDIA de 12 GB",
|
||||
"fx83": "Prefiere las partes del modelo que ya están en la tarjeta gráfica, lo que hace las respuestas mucho más rápidas en tarjetas de 12 GB. Desactívalo si las respuestas parecen peores que antes."
|
||||
"fx83": "Prefiere las partes del modelo que ya están en la tarjeta gráfica, lo que hace las respuestas más rápidas en tarjetas de 12 GB. Desactivado por defecto: en sesiones largas de agente puede repetirse y dar resultados más débiles. Actívalo si sobre todo chateas y quieres la velocidad."
|
||||
}
|
||||
@@ -81,5 +81,5 @@
|
||||
"fx80": "Il faut environ {0} Go d’espace disque libre. Ce qui ne tient pas en mémoire est lu depuis votre SSD.",
|
||||
"fx81": "{0} de RAM + VRAM, plus environ {1} Go de disque",
|
||||
"fx82": "Réponses plus rapides sur les cartes NVIDIA de 12 Go",
|
||||
"fx83": "Privilégie les parties du modèle déjà présentes sur la carte graphique, ce qui accélère nettement les réponses sur les cartes de 12 Go. Désactivez-le si les réponses semblent moins bonnes qu’avant."
|
||||
"fx83": "Privilégie les parties du modèle déjà présentes sur la carte graphique, ce qui accélère les réponses sur les cartes de 12 Go. Désactivé par défaut : dans de longues sessions d’agent, le modèle peut se répéter et donner de moins bons résultats. Activez-le si vous discutez surtout et voulez la vitesse."
|
||||
}
|
||||
@@ -81,5 +81,5 @@
|
||||
"fx80": "約 {0} GB の空きディスク容量が必要です。メモリに入らない分は SSD から読み込みます。",
|
||||
"fx81": "RAM + VRAM {0}、ほかにディスク約 {1} GB",
|
||||
"fx82": "12 GB の NVIDIA カードでの高速な応答",
|
||||
"fx83": "グラフィックスカード上にすでにあるモデルの部分を優先して使うため、12 GB のカードで応答がとても速くなります。以前より回答が悪くなったと感じたらオフにしてください。"
|
||||
"fx83": "グラフィックスカード上にすでにあるモデルの部分を優先して使うため、12 GB のカードで応答が速くなります。初期設定ではオフです。長いエージェント作業では、同じ内容を繰り返したり、結果が弱くなったりすることがあります。主にチャットで使い、速度が欲しい場合はオンにしてください。"
|
||||
}
|
||||
@@ -81,5 +81,5 @@
|
||||
"fx80": "약 {0}GB의 여유 디스크 공간이 필요합니다. 메모리에 들어가지 않는 부분은 SSD에서 읽습니다.",
|
||||
"fx81": "RAM + VRAM {0}, 디스크 약 {1}GB 추가",
|
||||
"fx82": "12GB NVIDIA 카드에서 더 빠른 응답",
|
||||
"fx83": "이미 그래픽 카드에 올라와 있는 모델 부분을 우선 사용해 12GB 카드에서 응답이 훨씬 빨라집니다. 답변이 이전보다 나빠 보이면 끄세요."
|
||||
"fx83": "이미 그래픽 카드에 올라와 있는 모델 부분을 우선 사용해 12GB 카드에서 응답이 더 빨라집니다. 기본값은 꺼짐입니다. 긴 에이전트 세션에서는 같은 말을 반복하거나 결과가 나빠질 수 있습니다. 주로 대화에 쓰고 속도를 원하면 켜세요."
|
||||
}
|
||||
@@ -81,5 +81,5 @@
|
||||
"fx80": "Precisa de cerca de {0} GB de espaço livre em disco. O que não cabe na memória é lido do seu SSD.",
|
||||
"fx81": "{0} de RAM + VRAM, mais cerca de {1} GB de disco",
|
||||
"fx82": "Respostas mais rápidas em placas NVIDIA de 12 GB",
|
||||
"fx83": "Prefere as partes do modelo que já estão na placa de vídeo, o que deixa as respostas bem mais rápidas em placas de 12 GB. Desative se as respostas parecerem piores do que antes."
|
||||
"fx83": "Prefere as partes do modelo que já estão na placa de vídeo, o que deixa as respostas mais rápidas em placas de 12 GB. Desativado por padrão: em sessões longas de agente ele pode se repetir e dar resultados mais fracos. Ative se você usa principalmente o chat e quer a velocidade."
|
||||
}
|
||||
@@ -81,5 +81,5 @@
|
||||
"fx80": "Нужно около {0} ГБ свободного места на диске. То, что не помещается в память, читается с вашего SSD.",
|
||||
"fx81": "{0} RAM + VRAM, плюс около {1} ГБ на диске",
|
||||
"fx82": "Более быстрые ответы на видеокартах NVIDIA с 12 ГБ",
|
||||
"fx83": "Отдаёт предпочтение частям модели, которые уже находятся на видеокарте, поэтому ответы на картах с 12 ГБ становятся намного быстрее. Отключите, если ответы стали выглядеть хуже, чем раньше."
|
||||
"fx83": "Отдаёт предпочтение частям модели, которые уже находятся на видеокарте, поэтому ответы на картах с 12 ГБ становятся быстрее. По умолчанию выключено: в длинных сессиях агента модель может повторяться и давать более слабые результаты. Включите, если вы в основном общаетесь в чате и хотите скорость."
|
||||
}
|
||||
@@ -81,5 +81,5 @@
|
||||
"fx80": "大约需要 {0} GB 可用磁盘空间。内存放不下的部分会从你的 SSD 读取。",
|
||||
"fx81": "{0} 内存 + 显存,另需约 {1} GB 磁盘空间",
|
||||
"fx82": "12 GB NVIDIA 显卡上更快的回答",
|
||||
"fx83": "优先使用已经在显卡上的模型部分,使 12 GB 显卡上的回答快很多。如果回答看起来比以前差,请关闭。"
|
||||
"fx83": "优先使用已经在显卡上的模型部分,使 12 GB 显卡上的回答更快。默认关闭:在长时间的智能体会话中,它可能重复内容并给出较弱的结果。如果你主要用来聊天并想要速度,请开启。"
|
||||
}
|
||||
@@ -1,9 +1,9 @@
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { parseCpuTopology, parseNvidiaReadings, parsePcieThroughput, gpuMemoryTotals } from '../server/device-probes.mjs';
|
||||
import { parseCpuTopology, parseNvidiaReadings, parsePcieThroughput, gpuMemoryTotals, pickGpuTemperature, isComputeAdapter } from '../server/device-probes.mjs';
|
||||
import { cpuUtilization, diskRate, parseLinuxDiskCounters, parseMacDiskCounters } from '../server/system-telemetry.mjs';
|
||||
import { parseEngineMetrics } from '../server/supervisor.mjs';
|
||||
import { originalDashboard } from '../server/monitor-dashboard.mjs';
|
||||
import { originalDashboard, mergeOwnReading } from '../server/monitor-dashboard.mjs';
|
||||
import { gpuMetric, gpuCapacity, gpuReported, shortGpuName, defaultGpuSet, gpuId } from '../src/lib/monitor-hardware.ts';
|
||||
import { referencePaths } from '../src/lib/monitor-chart.ts';
|
||||
|
||||
@@ -117,3 +117,50 @@ test('an RTX 5070 beside the unsupported Radeon iGPU: All GPUs is the RTX alone,
|
||||
assert.deepEqual(defaultGpuSet([igpu]).map(g=>g.id),[igpu.id]);
|
||||
assert.equal(defaultGpuSet([{...igpu,vramGb:96,unified:true},rtx]).length,2);
|
||||
});
|
||||
|
||||
// ---- 0.2.8: Monitor readings for AMD cards (#3727) ----
|
||||
const amd=(id,index)=>({id,index,unified:false,vramGb:16,name:'AMD Radeon RX 9070 XT',vendor:'amd',backend:'vulkan'});
|
||||
const ownReading=(id,extra={})=>({id,source:'sensor',sampledAt:'t0',temperatureC:null,utilizationPct:37,memoryUsedGb:5.5,powerW:null,powerLimitW:null,pcieGen:null,pcieGenMax:null,pcieWidth:null,pcieRxMbs:null,...extra});
|
||||
const shell=(own)=>({context:{maxTokens:32768},hardware:{gpus:own,gpuLoad:null},history:[],totals:{},telemetry:{}});
|
||||
const rawNoGpu={engine:{max_context:32768},live:{state:'generating'},hardware:{cpu:12},history:{}};
|
||||
|
||||
test('Windows AMD, one card, engine running: the engine has no GPU reader, the host load and memory stay (#3727)',()=>{
|
||||
const mapped=originalDashboard(rawNoGpu,{gpus:[amd('dx:1',0)]},shell([ownReading('dx:1')]));
|
||||
assert.equal(mapped.hardware.gpus.length,1);
|
||||
assert.equal(mapped.hardware.gpus[0].utilizationPct,37);
|
||||
assert.equal(mapped.hardware.gpus[0].memoryUsedGb,5.5);
|
||||
assert.equal(mapped.hardware.gpus[0].source,'sensor');
|
||||
assert.equal(mapped.hardware.gpus[0].temperatureC,null,'a value nobody read stays unavailable');
|
||||
});
|
||||
test('engine numbers win, the host fills only the gaps (fan and clock come from the host)',()=>{
|
||||
const raw={...rawNoGpu,hardware:{gpu_util:80,gpu_mem_used:4*1024**3,gpu_temp:61,gpu_power:200}};
|
||||
const mapped=originalDashboard(raw,{gpus:[amd('a',0)]},shell([ownReading('a',{utilizationPct:10,temperatureC:50,fanPct:35,clockMhz:2400,powerLimitW:300})]));
|
||||
const g=mapped.hardware.gpus[0];
|
||||
assert.equal(g.utilizationPct,80); assert.equal(g.temperatureC,61); assert.equal(g.powerW,200);
|
||||
assert.equal(g.powerLimitW,300); assert.equal(g.fanPct,35); assert.equal(g.clockMhz,2400); assert.equal(g.source,'engine');
|
||||
});
|
||||
test('a card the host has no reading for keeps the engine reading unchanged',()=>{
|
||||
const e={id:'x',source:'engine',temperatureC:null,utilizationPct:5};
|
||||
assert.equal(mergeOwnReading(e,undefined),e);
|
||||
});
|
||||
test('the headline aggregates fall back to the host when the engine reports none',()=>{
|
||||
const fb=shell([ownReading('a')]); fb.hardware.gpuTempC=55; fb.hardware.vramUsedGb=5.5;
|
||||
const mapped=originalDashboard(rawNoGpu,{gpus:[amd('a',0)]},fb);
|
||||
assert.equal(mapped.hardware.gpuTempC,55); assert.equal(mapped.hardware.vramUsedGb,5.5);
|
||||
});
|
||||
test('amdgpu temperature: edge first (what nvtop shows), junction when there is no edge, 0 is no reading',()=>{
|
||||
assert.equal(pickGpuTemperature([{label:'edge',milli:36000},{label:'junction',milli:38000},{label:'mem',milli:56000}]),36);
|
||||
assert.equal(pickGpuTemperature([{label:'mem',milli:56000},{label:'junction',milli:48000}]),48);
|
||||
assert.equal(pickGpuTemperature([{label:'',milli:41000}]),41);
|
||||
assert.equal(pickGpuTemperature([{label:'edge',milli:0},{label:'junction',milli:44000}]),44);
|
||||
assert.equal(pickGpuTemperature([{label:'edge',milli:0}]),null);
|
||||
assert.equal(pickGpuTemperature([{label:'mem',milli:56000}]),null,'a memory sensor alone is not the GPU temperature');
|
||||
assert.equal(pickGpuTemperature([]),null);
|
||||
});
|
||||
test('a remote-console chip with no memory is not a GPU; an unnamed card with memory and an Intel iGPU are',()=>{
|
||||
assert.equal(isComputeAdapter('unknown',null),false);
|
||||
assert.equal(isComputeAdapter('unknown',0),false);
|
||||
assert.equal(isComputeAdapter('unknown',8*1024**3),true);
|
||||
assert.equal(isComputeAdapter('intel',null),true);
|
||||
assert.equal(isComputeAdapter('amd',34208743424),true);
|
||||
});
|
||||
@@ -0,0 +1,173 @@
|
||||
/**
|
||||
* The options the app hands to the DeepSeek engine (strata-dsv41), which exits with "unknown option X" on a name it does not take:
|
||||
* - #4358 #4359 #4505 #4507: a model with pictures off had its `--vision off` pair cut in half (`--eos 1 off --dspark auto`);
|
||||
* - #4504: the Settings field "GPU memory for other apps" wrote --vram-reserve-mib, the resource preference wrote --pool-workers.
|
||||
* Nothing here starts an engine: Control is a fake that records the run config the host saved for the start (a scratch profile,
|
||||
* never the real one).
|
||||
*/
|
||||
import './helpers/isolate-profile.mjs';
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { existsSync, mkdtempSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
|
||||
import { join } from 'node:path';
|
||||
import { tmpdir } from 'node:os';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { createNativeControl } from '../server/native-control.mjs';
|
||||
import { DS_VALUE_FLAGS, DS_SWITCH_FLAGS, DS_RENAMED, dsEngineArgs, dsUserArgs, dsArgsNote, withoutVisionFlag } from '../server/native-ds-args.mjs';
|
||||
|
||||
const DS_ARGS = ['--pack', '/p/ds', '--max-context', '32768', '--eos', '1', '--vision', 'off', '--dspark', 'auto', '--dspark-conf', '0.65', '--prefill-chunk', '1024'];
|
||||
|
||||
function fixture(config = {}, id = 'deepseek-v4.1-flash-ist-das-2.25bpw') {
|
||||
const root = mkdtempSync(join(tmpdir(), 'strata-ds-args-'));
|
||||
const install = join(root, 'install'), data = join(root, 'data');
|
||||
mkdirSync(install, { recursive: true });
|
||||
process.env.STRATA_INSTALL_DIR = install;
|
||||
const path = join(install, `strata-${id}.json`);
|
||||
writeFileSync(path, JSON.stringify({
|
||||
exe: 'C:\\Users\\x\\AppData\\Local\\Strata\\engine-dsv41\\0.1.52-windows-x86_64-cuda-cuda13\\strata-dsv41.exe', arch: 'deepseek41',
|
||||
tokenizer: join(install, 'tok'), args: DS_ARGS, model_name: 'DeepSeek V4.1', port: 8080, ...config }));
|
||||
const saved = [];
|
||||
const control = {
|
||||
control: { dir: join(root, 'control') }, version: '0.2.7',
|
||||
async installedModels() { return { install_dir: install, models: [{ id, name: 'DeepSeek V4.1', source_config: path, component_status: 'present', context_tokens: 32768 }] }; },
|
||||
async engine() { return { usable: true, backend: 'cuda', components_dir: 'E:\\c', engine: { version: '0.1.52', platform: 'windows-x86_64', backend: 'cuda', exe: 'E:\\e\\strata.exe', root: 'E:\\e' } }; },
|
||||
async saveProfile(name, file) { saved.push(JSON.parse(readFileSync(file, 'utf8'))); return {}; },
|
||||
async start() { return { ok: true, state: 'ready' }; },
|
||||
async stop() { return { ok: true }; },
|
||||
};
|
||||
const native = createNativeControl({ control, hardwareProbe: () => [{ index: 0, vram_gb: 24 }], directory: data, spawnProcess: () => { throw new Error('no process'); }, fetcher: async () => Response.json({ status: 'ok' }), requests: () => [] });
|
||||
return { native, saved, id, path, cleanup: () => rmSync(root, { recursive: true, force: true }) };
|
||||
}
|
||||
const finish = async (native, id) => { for (let i = 0; i < 500; i++) { const job = native.state().jobs.find(j => j.id === id); if (job.state !== 'running') return job; await new Promise(r => setTimeout(r, 10)); } throw new Error('job did not finish'); };
|
||||
/** Every word of the argument list that is not an option name or the value right after one: a stray word means a flag lost its name. */
|
||||
function strayWords(args, valued) {
|
||||
const stray = [];
|
||||
for (let i = 0; i < args.length; i++) {
|
||||
if (args[i].startsWith('-')) { if (valued.has(args[i]) && i + 1 < args.length && !args[i + 1].startsWith('--')) i++; continue; }
|
||||
stray.push(args[i]);
|
||||
}
|
||||
return stray;
|
||||
}
|
||||
|
||||
test('the DeepSeek option list is a subset of what ds4_main.cpp parses (it cannot drift ahead of the engine)', () => {
|
||||
const source = fileURLToPath(new URL('../../../src/dsv41/ds4_main.cpp', import.meta.url));
|
||||
if (!existsSync(source)) return; // a package without the engine source
|
||||
const parsed = new Set([...readFileSync(source, 'utf8').matchAll(/a == "(-{1,2}[a-z][a-z0-9-]*)"/g)].map(m => m[1]));
|
||||
assert.ok(parsed.has('--eos') && parsed.has('--reserve-mib'), 'the pattern found the engine option parser');
|
||||
for (const flag of [...DS_VALUE_FLAGS, ...DS_SWITCH_FLAGS, ...Object.values(DS_RENAMED)]) assert.ok(parsed.has(flag), `${flag} is not an option of strata-dsv41`);
|
||||
for (const flag of Object.keys(DS_RENAMED)) assert.ok(!parsed.has(flag), `${flag} is the Qwen engine's name, strata-dsv41 rejects it`);
|
||||
assert.ok(!parsed.has('--pool-workers'), '--pool-workers is rejected by strata-dsv41');
|
||||
});
|
||||
|
||||
test('dsEngineArgs renames --vram-reserve-mib, leaves out --pool-workers with its value and keeps the --vision pair', () => {
|
||||
const r = dsEngineArgs([...DS_ARGS, '--vram-reserve-mib', '512', '--pool-workers', '20', '--resident-budget-gib', '40']);
|
||||
assert.deepEqual(r.args, [...DS_ARGS, '--reserve-mib', '512']);
|
||||
assert.deepEqual(r.renamed, [{ from: '--vram-reserve-mib', to: '--reserve-mib' }]);
|
||||
assert.deepEqual(r.dropped.map(d => [d.flag, d.value]), [['--pool-workers', '20'], ['--resident-budget-gib', '40']]);
|
||||
assert.match(dsArgsNote(r), /--vram-reserve-mib is passed as --reserve-mib/);
|
||||
assert.match(dsArgsNote(r), /--pool-workers 20, --resident-budget-gib 40/);
|
||||
assert.equal(dsArgsNote(dsEngineArgs(DS_ARGS)), '', 'nothing changed, nothing said');
|
||||
});
|
||||
|
||||
test('dsEngineArgs: a reserve under both names keeps the engine\'s own, a switch at the end and values that look like numbers survive', () => {
|
||||
assert.deepEqual(dsEngineArgs(['--reserve-mib', '300', '--vram-reserve-mib', '512']).args, ['--reserve-mib', '300']);
|
||||
assert.deepEqual(dsEngineArgs(['--pack', '/p', '--no-expert-cache']).args, ['--pack', '/p', '--no-expert-cache']);
|
||||
assert.deepEqual(dsEngineArgs(['--pool-workers']).args, [], 'a flag with no value');
|
||||
assert.deepEqual(dsEngineArgs(['--spec', '4', '--tp', '2', '--kv', 'int8', '--experts', 'auto']).args, ['--tp', '2', '--experts', 'auto']);
|
||||
assert.deepEqual(dsEngineArgs(undefined).args, []);
|
||||
});
|
||||
|
||||
test('dsUserArgs drops what the engine rejects and keeps the options it takes and the app\'s reserve', () => {
|
||||
assert.deepEqual(dsUserArgs(['--pool-workers', '20', '--vram-reserve-mib', '512', '--cpu-threads', '8']), ['--vram-reserve-mib', '512', '--cpu-threads', '8']);
|
||||
assert.deepEqual(dsUserArgs(['--pool-workers', '20']), []);
|
||||
});
|
||||
|
||||
test('withoutVisionFlag takes the Qwen engine\'s stand-alone --vision out, and a picture word an old file left after it', () => {
|
||||
assert.deepEqual(withoutVisionFlag(['--max-context', '4096', '--vision', '--kv', 'int8']), ['--max-context', '4096', '--kv', 'int8']);
|
||||
assert.deepEqual(withoutVisionFlag(['--a', '1', '--vision']), ['--a', '1']);
|
||||
assert.deepEqual(withoutVisionFlag(['--vision', 'off', '--eos', '1']), ['--eos', '1']);
|
||||
assert.deepEqual(withoutVisionFlag(['--vision', 'gpu', '--x']), ['--x']);
|
||||
});
|
||||
|
||||
test('#4358 #4359 #4505 #4507: a DeepSeek load with pictures off leaves the --vision pair whole and no stray word', async () => {
|
||||
const f = fixture();
|
||||
try {
|
||||
await f.native.refresh(true);
|
||||
// what the Models screen sends for a model it does not list pictures for: vision "no"
|
||||
const job = await finish(f.native, f.native.operation('load', { model: f.id, vision: 'no' }).id);
|
||||
assert.equal(job.state, 'done', job.error);
|
||||
const args = f.saved[0].args;
|
||||
assert.equal(args[args.indexOf('--vision') + 1], 'off', args.join(' '));
|
||||
assert.equal(args[args.indexOf('--eos') + 1], '1', 'the word after --eos is its value, not a left-over of --vision');
|
||||
assert.equal(args[args.indexOf('--eos') + 2], '--vision');
|
||||
assert.deepEqual(strayWords(args, new Set(['--pack', '--max-context', '--eos', '--vision', '--dspark', '--dspark-conf', '--prefill-chunk'])), []);
|
||||
assert.deepEqual(args, DS_ARGS, 'nothing was added or removed');
|
||||
} finally { await f.native.close(); f.cleanup(); }
|
||||
});
|
||||
|
||||
test('a DeepSeek model with pictures on keeps `--vision on` too', async () => {
|
||||
const f = fixture({ args: DS_ARGS.map(a => (a === 'off' ? 'on' : a)) });
|
||||
try {
|
||||
await f.native.refresh(true);
|
||||
const job = await finish(f.native, f.native.operation('load', { model: f.id, vision: 'no' }).id);
|
||||
assert.equal(job.state, 'done', job.error);
|
||||
const args = f.saved[0].args;
|
||||
assert.equal(args[args.indexOf('--vision') + 1], 'on');
|
||||
assert.equal(args[args.indexOf('--eos') + 1], '1');
|
||||
} finally { await f.native.close(); f.cleanup(); }
|
||||
});
|
||||
|
||||
test('#4504: the reserve field and the resource preference reach strata-dsv41 as --reserve-mib, with no --pool-workers', async () => {
|
||||
const f = fixture({ vram_reserve_mib: 512, ui_resource_profile: 'work', args: [...DS_ARGS, '--pool-workers', '20', '--vram-reserve-mib', '512'], args_written: [...DS_ARGS, '--pool-workers', '20', '--vram-reserve-mib', '512'], user_args: ['--pool-workers', '20'] });
|
||||
try {
|
||||
await f.native.refresh(true);
|
||||
const job = await finish(f.native, f.native.operation('load', { model: f.id, vision: 'no', resourceProfile: 'work' }).id);
|
||||
assert.equal(job.state, 'done', job.error);
|
||||
const saved = f.saved[0], args = saved.args;
|
||||
assert.ok(!args.includes('--pool-workers') && !args.includes('20'), args.join(' '));
|
||||
assert.ok(!args.includes('--vram-reserve-mib'), args.join(' '));
|
||||
assert.equal(args[args.indexOf('--reserve-mib') + 1], '512', args.join(' '));
|
||||
assert.equal(args.filter(a => a === '--reserve-mib').length, 1);
|
||||
assert.ok(!(saved.user_args ?? []).includes('--pool-workers'), 'the person\'s saved options lose it too');
|
||||
for (const flag of args.filter(a => a.startsWith('--'))) assert.ok(DS_VALUE_FLAGS.has(flag) || DS_SWITCH_FLAGS.has(flag), `${flag} is an option of strata-dsv41`);
|
||||
} finally { await f.native.close(); f.cleanup(); }
|
||||
});
|
||||
|
||||
test('the resource preference no longer writes Qwen-engine options into a DeepSeek model\'s file when it is tuned', async () => {
|
||||
const f = fixture({ args: DS_ARGS });
|
||||
try {
|
||||
await f.native.refresh(true);
|
||||
const before = JSON.parse(readFileSync(f.path, 'utf8'));
|
||||
const job = await finish(f.native, f.native.operation('tune', { model: f.id, profile: 'work', calibrate: false }).id);
|
||||
void job; // the load and benchmark after it need a real engine; the file is what matters
|
||||
const after = JSON.parse(readFileSync(f.path, 'utf8'));
|
||||
assert.ok(!(after.args ?? []).includes('--pool-workers'), (after.args ?? []).join(' '));
|
||||
assert.ok(!(after.args ?? []).includes('--vram-reserve-mib'), (after.args ?? []).join(' '));
|
||||
assert.deepEqual(after.args.slice(0, before.args.length), before.args);
|
||||
} finally { await f.native.close(); f.cleanup(); }
|
||||
});
|
||||
|
||||
test('a Qwen model with pictures off still loses its stand-alone --vision (nothing else changed)', async () => {
|
||||
const root = mkdtempSync(join(tmpdir(), 'strata-ds-args-q-'));
|
||||
const install = join(root, 'install');
|
||||
mkdirSync(install, { recursive: true });
|
||||
process.env.STRATA_INSTALL_DIR = install;
|
||||
const path = join(install, 'strata-q2_0.json');
|
||||
writeFileSync(path, JSON.stringify({ exe: 'component:strata-engine', tokenizer: join(install, 'tok'), args: ['--pack', '/p', '--max-context', '4096', '--vision', '--kv', 'int8'], model_name: 'Qwen', port: 8080 }));
|
||||
const saved = [];
|
||||
const control = {
|
||||
control: { dir: join(root, 'control') }, version: '0.2.7',
|
||||
async installedModels() { return { install_dir: install, models: [{ id: 'q2_0', name: 'Qwen', source_config: path, component_status: 'present', context_tokens: 4096 }] }; },
|
||||
async engine() { return { usable: true, backend: 'cuda', components_dir: 'E:\\c', engine: { version: '0.2.0', platform: 'windows-x86_64', backend: 'cuda', exe: 'E:\\e\\strata.exe', root: 'E:\\e' } }; },
|
||||
async saveProfile(name, file) { saved.push(JSON.parse(readFileSync(file, 'utf8'))); return {}; },
|
||||
async start() { return { ok: true, state: 'ready' }; },
|
||||
async stop() { return { ok: true }; },
|
||||
};
|
||||
const native = createNativeControl({ control, hardwareProbe: () => [{ index: 0, vram_gb: 12 }], directory: join(root, 'data'), spawnProcess: () => { throw new Error('no process'); }, fetcher: async () => Response.json({ status: 'ok' }), requests: () => [] });
|
||||
try {
|
||||
await native.refresh(true);
|
||||
const job = await finish(native, native.operation('load', { model: 'q2_0', vision: 'no' }).id);
|
||||
assert.equal(job.state, 'done', job.error);
|
||||
assert.deepEqual(saved[0].args, ['--pack', '/p', '--max-context', '4096', '--kv', 'int8']);
|
||||
} finally { await native.close(); rmSync(root, { recursive: true, force: true }); }
|
||||
});
|
||||
@@ -88,10 +88,26 @@ test('runCli passes the arguments untouched (no shell) and reads JSON', async ()
|
||||
|
||||
// ---------------------------------------------------------------------------------- native-control in packaged mode
|
||||
|
||||
function packagedFixture({ engineUsable = true, updater = null, repair = false, ids = ['iq2_xs', 'iq3_s'] } = {}) {
|
||||
function packagedFixture({ engineUsable = true, updater = null, repair = false, ids = ['iq2_xs', 'iq3_s'], controlVersion = '0.1.45', extraSizes = [], cliStartWait = 'old' } = {}) {
|
||||
const root = mkdtempSync(join(tmpdir(), 'strata-packaged-test-'));
|
||||
const install = join(root, 'install'), data = join(root, 'data');
|
||||
mkdirSync(install, { recursive: true });
|
||||
/* The Control this app found, as it is on disk. The app reads the start-wait ceiling out of these files instead of trusting
|
||||
the version, because a start wait is checked three times before the engine is reached and a released Control has those
|
||||
three ceilings disagreeing:
|
||||
'old' no runtime ceiling at all, both gates 600 (Control before #3949)
|
||||
'split' the runtime takes 1800, the CLI and the record still stop at 600 (every Control 0.1.50 to 0.1.52)
|
||||
'cli-only' the CLI follows the runtime, the record still stops at 600 (`direct runtime intent has an invalid timeout`)
|
||||
'runtime' all three hand the wait to the runtime's own ceiling */
|
||||
const controlSource = join(root, 'control', 'strata_control');
|
||||
mkdirSync(controlSource, { recursive: true });
|
||||
writeFileSync(join(controlSource, 'runtime.py'), cliStartWait === 'old' ? '' : 'MAX_START_WAIT = 1800\n');
|
||||
writeFileSync(join(controlSource, 'management.py'), ['cli-only', 'runtime'].includes(cliStartWait)
|
||||
? ' wait = _seconds(wait, 0, runtime.MAX_START_WAIT, "start wait")\n'
|
||||
: ' wait = _seconds(wait, 0, 600, "start wait")\n');
|
||||
writeFileSync(join(controlSource, 'journal.py'), cliStartWait === 'runtime'
|
||||
? ' ranges = {"wait_s": (0, start_wait_max) if kind == "runtime.start" else\n'
|
||||
: ' ranges = {"wait_s": (0, 600) if kind == "runtime.start" else\n');
|
||||
const earlierInstallEnv = process.env.STRATA_INSTALL_DIR;
|
||||
process.env.STRATA_INSTALL_DIR = install; // the app's own install folder (the host always tells Control which one; it never falls back to an older CLI folder)
|
||||
const configs = ids.map(id => {
|
||||
@@ -105,7 +121,7 @@ function packagedFixture({ engineUsable = true, updater = null, repair = false,
|
||||
const rpc = [];
|
||||
let startFailure = null, startFailureLeft = Infinity;
|
||||
const control = {
|
||||
control: { dir: join(root, 'control'), python: join(root, 'control', 'python', 'python.exe') }, version: '0.1.45',
|
||||
control: { dir: join(root, 'control'), python: join(root, 'control', 'python', 'python.exe') }, version: controlVersion,
|
||||
async installedModels(options) { calls.push(['installedModels', options?.installDir ?? null]); return { install_dir: install, models: configs.map(c => ({ id: c.id, name: `Qwen ${c.id}`, source_config: c.path, component_status: 'present', context_tokens: 32768, backend: 'cuda' })) }; },
|
||||
async engine() {
|
||||
calls.push(['engine']);
|
||||
@@ -126,7 +142,7 @@ function packagedFixture({ engineUsable = true, updater = null, repair = false,
|
||||
// answers like it: the catalog, the hardware with the GPUs, and an install plan or job.
|
||||
const catalog = { families: [{ family: 'qwen', title: 'Qwen3.8-Flash-Next', about: 'x', images: true, experimental: false, sizes: [
|
||||
{ id: 'q2_0', model: 'Q2_0', download_gb: 66.4, ram_needed_gb: 48, about: 'fast', on_this_pc: 'fits', installed: false, images: true, experimental: false },
|
||||
{ id: 'iq3_xxs', model: 'IQ3_XXS', download_gb: 75.8, ram_needed_gb: 60, about: 'balanced', on_this_pc: 'fits', installed: false, images: true, experimental: false }] }],
|
||||
{ id: 'iq3_xxs', model: 'IQ3_XXS', download_gb: 75.8, ram_needed_gb: 60, about: 'balanced', on_this_pc: 'fits', installed: false, images: true, experimental: false }, ...extraSizes] }],
|
||||
contexts: [8192, 32768], recommendation: { family: 'qwen', model: 'IQ3_XXS', context: 32768 }, context_note: '' };
|
||||
const hardware = { ram_gb: 63, cpu: { name: 'cpu', cores: 12 }, gpus: [{ index: 0, name: 'NVIDIA GeForce RTX 5070', vram_gb: 11.9, usable: true }] };
|
||||
let installJob = null;
|
||||
@@ -606,3 +622,43 @@ test("onboarding 'use your own GGUF' (#3507, #3424): the models folder the perso
|
||||
assert.ok(sent.data_dir === undefined || sent.data_dir === join(f.root, 'Strata-data'), `data_dir was ${sent.data_dir}`);
|
||||
} finally { await f.native.close(); f.cleanup(); }
|
||||
});
|
||||
|
||||
/* The start wait follows the model's size, as `strata launch` does since #3949, and never asks an older Control for more than it takes.
|
||||
Reported on the app side by #3459 and #4191: a big model on a slow disk was stopped at 300 s while the engine went on reading. */
|
||||
test('packaged: a big model is waited for by its size, inside the ceiling of the Control this app has (#3459 #4191)', async () => {
|
||||
const run = async (cliStartWait, downloadGb) => {
|
||||
const f = packagedFixture({ ids: ['big'], cliStartWait, extraSizes: [{ id: 'big', model: 'BIG', download_gb: downloadGb, ram_needed_gb: 60, about: 'x', on_this_pc: 'fits', installed: false, images: false, experimental: false }] });
|
||||
try {
|
||||
await f.native.refresh(true);
|
||||
const job = await finish(f.native, f.native.operation('load', { model: 'big' }).id);
|
||||
assert.equal(job.state, 'done', job.error);
|
||||
const start = f.calls.find(c => c[0] === 'start');
|
||||
const saved = f.saved[0];
|
||||
await f.native.close();
|
||||
return { wait: start[2], ready: saved.engine_ready_s };
|
||||
} finally { f.cleanup(); }
|
||||
};
|
||||
assert.equal((await run('old', 180)).wait, 600, 'a Control with no long-wait ceiling at all is asked for 600 s, as today');
|
||||
assert.equal((await run('split', 180)).wait, 600, 'a Control whose runtime takes 1800 s but whose CLI still stops at 600 is asked for 600 s: the CLI would refuse the start outright (#3949)');
|
||||
assert.equal((await run('cli-only', 180)).wait, 600, 'a Control whose CLI takes it but whose operation record still stops at 600 is asked for 600 s too: the record is written before the start runs');
|
||||
const long = await run('runtime', 180);
|
||||
assert.equal(long.wait, 1720, '600 s + 8 s per GB above 40 GB, as in strata launch, once the CLI really takes it');
|
||||
assert.equal(long.ready, 1780, 'the server readies after the start gave up, never before it');
|
||||
const small = await run('runtime', 20);
|
||||
assert.equal(small.wait, 300, 'a model under 40 GB keeps the 300 s it always had');
|
||||
assert.equal(small.ready, undefined, 'nothing is added to a small model’s run config');
|
||||
const huge = await run('runtime', 400);
|
||||
assert.equal(huge.wait, 1800, 'and never more than this Control accepts');
|
||||
});
|
||||
|
||||
test('packaged: a run config that names a longer engine wait is honoured, and never shortened by the app', async () => {
|
||||
const f = packagedFixture({ ids: ['deepseek-v4.1-flash-ist-das-2.00bpw'], cliStartWait: 'runtime' });
|
||||
try {
|
||||
writeFileSync(join(f.install, 'strata-deepseek-v4.1-flash-ist-das-2.00bpw.json'), JSON.stringify({ exe: join(f.install, 'engine-dsv41', 'strata-dsv41'), arch: 'deepseek41', tokenizer: join(f.install, 'tok'), args: ['--pack', join(f.install, 'pack')], model_name: 'deepseek-v4.1-flash', port: 8080, engine_ready_s: 3600 }));
|
||||
await f.native.refresh(true);
|
||||
const job = await finish(f.native, f.native.operation('load', { model: 'deepseek-v4.1-flash-ist-das-2.00bpw' }).id);
|
||||
assert.equal(job.state, 'done', job.error);
|
||||
assert.equal(f.calls.find(c => c[0] === 'start')[2], 1800, 'the wait of the newest Control, which is what the ceiling allows');
|
||||
assert.equal(f.saved[0].engine_ready_s, 3600, 'setup’s own longer wait stays as it is');
|
||||
} finally { await f.native.close(); f.cleanup(); }
|
||||
});
|
||||
@@ -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.7');
|
||||
assert.equal(cargo, '0.2.8');
|
||||
});
|
||||
@@ -13,19 +13,19 @@ import { nativeFixture } from './helpers/native-fixture.mjs';
|
||||
const read = rel => readFileSync(fileURLToPath(new URL(rel, import.meta.url)), 'utf8').replace(/\r\n/g, '\n');
|
||||
const ENV = { // what each switch writes outside its default
|
||||
faster_long_prompts: { STRATA_KVA: '1' },
|
||||
faster_12gb_nvidia: { STRATA_ROUTE_PRIOR: '0' },
|
||||
faster_12gb_nvidia: { STRATA_ROUTE_PRIOR: '1.5' },
|
||||
faster_radeon_9000: { STRATA_PCIE_FRAC_DEFAULT: 'old' },
|
||||
deepseek_speedups: { DS4_DEFAULTS: '026', DS4_FAST_ATTN: '0', DS4_ATTN_SLAB: '0', DS4_COPY_PIN: '0', DS4_COPY_HUGETLB: '0', DS4_STORE_SPARE: '0', DS4_STORE_PUT: '0' },
|
||||
};
|
||||
const SIX = ['DS4_FAST_ATTN', 'DS4_ATTN_SLAB', 'DS4_COPY_PIN', 'DS4_COPY_HUGETLB', 'DS4_STORE_SPARE', 'DS4_STORE_PUT'];
|
||||
const DEFAULT = { faster_long_prompts: 'off', faster_12gb_nvidia: 'on', faster_radeon_9000: 'on', deepseek_speedups: 'on' };
|
||||
const DEFAULT = { faster_long_prompts: 'off', faster_12gb_nvidia: 'off', faster_radeon_9000: 'on', deepseek_speedups: 'on' };
|
||||
const RTX3060 = { index: 0, name: 'NVIDIA GeForce RTX 3060', vendor: 'nvidia', arch: '86', vram_gb: 12 };
|
||||
const R9700 = { index: 0, name: 'AMD Radeon AI PRO R9700', vendor: 'amd', arch: 'gfx1201', vram_gb: 32 };
|
||||
const RX7900 = { index: 0, name: 'AMD Radeon RX 7900 XTX', vendor: 'amd', arch: 'gfx1100', vram_gb: 24 };
|
||||
const RTX = { index: 0, name: 'NVIDIA GeForce RTX 4090', vendor: 'nvidia', arch: '89', vram_gb: 24 };
|
||||
const STRIX = { index: 0, name: 'AMD Radeon 8060S', vendor: 'amd', arch: 'gfx1151', vram_gb: 96, uma: true };
|
||||
|
||||
test('there are exactly four switches in the app (the Terminal has one more); each writes the engine names of the contract and has its default (KVA off, the others on)', () => {
|
||||
test('there are exactly four switches in the app (the Terminal has one more); each writes the engine names of the contract and has its default (KVA and the 12 GB routing off, the other two on)', () => {
|
||||
assert.deepEqual(SPEED_SWITCHES.map(s => s.id), Object.keys(ENV));
|
||||
for (const sw of SPEED_SWITCHES) {
|
||||
assert.deepEqual(sw.env, ENV[sw.id], sw.id);
|
||||
@@ -38,7 +38,7 @@ test('the Terminal\'s Qwen "0.2.6 behaviour" switch is not in the app', () => {
|
||||
assert.ok(!Object.values(SPEED_SWITCHES.flatMap(s => Object.keys(s.env))).includes('STRATA_DEFAULTS'));
|
||||
});
|
||||
|
||||
test('Faster answers on 12 GB NVIDIA cards: only for Qwen on a discrete CUDA card of 13 GiB or less; Off writes STRATA_ROUTE_PRIOR=0', () => {
|
||||
test('Faster answers on 12 GB NVIDIA cards: only for Qwen on a discrete CUDA card of 13 GiB or less; off by default since 0.2.8 (#4402), On writes STRATA_ROUTE_PRIOR=1.5', () => {
|
||||
const ids = context => speedSwitchRows({}, context).map(r => r.id);
|
||||
assert.deepEqual(ids({ backend: 'cuda', gpus: [RTX3060] }), ['faster_long_prompts', 'faster_12gb_nvidia']);
|
||||
assert.deepEqual(ids({ backend: 'cuda', gpus: [{ ...RTX3060, vram_gb: 13 }] }), ['faster_long_prompts', 'faster_12gb_nvidia'], '13 GiB counts');
|
||||
@@ -55,12 +55,24 @@ test('Faster answers on 12 GB NVIDIA cards: only for Qwen on a discrete CUDA car
|
||||
assert.deepEqual(ids({ backend: 'sycl', gpus: [RTX3060] }), [], 'not a CUDA build');
|
||||
assert.deepEqual(ids({ backend: 'cuda', gpus: [RTX3060], arch: 'qwen35moe' }), [], 'Qwen3.6-35B-A3B: the routing needs 512 experts');
|
||||
assert.deepEqual(ids({ deepseek: true, backend: 'cuda', gpus: [RTX3060] }), ['deepseek_speedups']);
|
||||
const off = setSpeedSwitch({ PATH: '/bin' }, 'faster_12gb_nvidia', false);
|
||||
assert.deepEqual(off, { PATH: '/bin', STRATA_ROUTE_PRIOR: '0' });
|
||||
assert.equal(speedSwitchIsOn(off, 'faster_12gb_nvidia'), false);
|
||||
assert.equal(speedSwitchIsOn({}, 'faster_12gb_nvidia'), true);
|
||||
assert.deepEqual(setSpeedSwitch(off, 'faster_12gb_nvidia', true), { PATH: '/bin' });
|
||||
assert.equal(speedSwitchIsOn({ STRATA_ROUTE_PRIOR: '2.0' }, 'faster_12gb_nvidia'), true, 'any other value is the user\'s own and reads as the default');
|
||||
// a default launch carries nothing: the engine then runs as 0.2.6 did (STRATA_ROUTE_PRIOR unset = 0)
|
||||
assert.equal(speedSwitchIsOn({}, 'faster_12gb_nvidia'), false);
|
||||
assert.equal(speedSwitchRows({}, { backend: 'cuda', gpus: [RTX3060] }).find(r => r.id === 'faster_12gb_nvidia').on, false);
|
||||
assert.deepEqual(setSpeedSwitch({ PATH: '/bin' }, 'faster_12gb_nvidia', false), { PATH: '/bin' }, 'Off is the default: it writes nothing');
|
||||
// the explicit opt-in the row writes
|
||||
const on = setSpeedSwitch({ PATH: '/bin' }, 'faster_12gb_nvidia', true);
|
||||
assert.deepEqual(on, { PATH: '/bin', STRATA_ROUTE_PRIOR: '1.5' });
|
||||
assert.equal(speedSwitchIsOn(on, 'faster_12gb_nvidia'), true);
|
||||
assert.deepEqual(setSpeedSwitch(on, 'faster_12gb_nvidia', false), { PATH: '/bin' }, 'back to Off removes the entry');
|
||||
// the 0.2.7 "Off" wrote STRATA_ROUTE_PRIOR=0: that file is in the default state now, and On replaces it with 1.5
|
||||
assert.equal(speedSwitchIsOn({ STRATA_ROUTE_PRIOR: '0' }, 'faster_12gb_nvidia'), false);
|
||||
assert.deepEqual(setSpeedSwitch({ STRATA_ROUTE_PRIOR: '0' }, 'faster_12gb_nvidia', true), { STRATA_ROUTE_PRIOR: '1.5' });
|
||||
assert.deepEqual(setSpeedSwitch({ STRATA_ROUTE_PRIOR: '0' }, 'faster_12gb_nvidia', false), {}, 'and Off clears it');
|
||||
// the words say both sides
|
||||
const hint = SPEED_SWITCHES.find(s => s.id === 'faster_12gb_nvidia').hint;
|
||||
assert.match(hint, /faster on 12 GB cards/);
|
||||
assert.match(hint, /Off by default/);
|
||||
assert.match(hint, /long agent sessions it can repeat itself/);
|
||||
});
|
||||
|
||||
test('DeepSeek speed-ups Off writes DS4_DEFAULTS=026 and the six; On removes all seven; the six alone (an earlier test build) still read as Off', () => {
|
||||
|
||||
@@ -45,3 +45,44 @@ test('counter lines are read, and the load and VRAM in use reach the card by its
|
||||
assert.equal(reader.read('luid_0x00000000_0x00099999'), null);
|
||||
assert.equal(procs.length, 1, 'one process for every read');
|
||||
});
|
||||
|
||||
// ---- 0.2.8: the card's temperature from the display driver (D3DKMT), #3727 ----
|
||||
import { d3dkmtTemperatureC, GPU_TEMP_CSHARP } from '../server/windows-gpu.mjs';
|
||||
|
||||
test('temperature comes in tenths of a degree; no answer, zero and nonsense are not a reading', () => {
|
||||
assert.equal(d3dkmtTemperatureC(300), 30);
|
||||
assert.equal(d3dkmtTemperatureC(437), 44);
|
||||
assert.equal(d3dkmtTemperatureC(-1), null);
|
||||
assert.equal(d3dkmtTemperatureC(0), null);
|
||||
assert.equal(d3dkmtTemperatureC(4294967295), null);
|
||||
assert.equal(d3dkmtTemperatureC(undefined), null);
|
||||
assert.equal(d3dkmtTemperatureC('abc'), null);
|
||||
});
|
||||
|
||||
test('a counter line carries the temperature; the older three-field line and a failed query still parse', () => {
|
||||
assert.equal(parseGpuCounterLine('G luid_0x00000000_0x00016102 37 4294967296 430').temperatureC, 43);
|
||||
assert.equal(parseGpuCounterLine('G luid_0x00000000_0x00016102 37 4294967296 -1').temperatureC, null);
|
||||
const old = parseGpuCounterLine('G luid_0x00000000_0x00016102 37 4294967296');
|
||||
assert.equal(old.temperatureC, null); assert.equal(old.utilizationPct, 37);
|
||||
});
|
||||
|
||||
test('the temperature reaches the reader by LUID next to load and memory', () => {
|
||||
const procs = [];
|
||||
const reader = createWindowsGpuReader({ spawnFn: () => { const p = new EventEmitter(); p.stdout = new EventEmitter(); p.kill = () => {}; procs.push(p); return p; }, now: () => 5 });
|
||||
reader.read('luid_0x00000000_0x00016102');
|
||||
procs[0].stdout.emit('data', 'G luid_0x00000000_0x00016102 90 17000000000 465\n');
|
||||
const r = reader.read('luid_0x00000000_0x00016102');
|
||||
assert.equal(r.temperatureC, 47); assert.equal(r.utilizationPct, 90);
|
||||
});
|
||||
|
||||
test('the PowerShell script that asks D3DKMT is single-quote safe and uses adapter performance data type 62 with the temperature at byte 56', () => {
|
||||
assert.ok(!GPU_TEMP_CSHARP.includes("'"), 'it sits inside a single-quoted PowerShell string');
|
||||
assert.match(GPU_TEMP_CSHARP, /i\.T=62/);
|
||||
assert.match(GPU_TEMP_CSHARP, /ReadInt32\(buf,56\)/);
|
||||
assert.match(GPU_TEMP_CSHARP, /AllocHGlobal\(64\)/);
|
||||
let script = '';
|
||||
createWindowsGpuReader({ spawnFn: (_cmd, args) => { script = args.at(-1); const p = new EventEmitter(); p.stdout = new EventEmitter(); p.kill = () => {}; return p; } }).read('x');
|
||||
assert.ok(script.includes('Add-Type -TypeDefinition'), 'compiled once, before the loop');
|
||||
assert.ok(script.indexOf('Add-Type') < script.indexOf('while($true)'));
|
||||
assert.match(script, /try\{Add-Type[^]*\}catch\{\}/, 'a failed compile must not stop load and memory');
|
||||
});
|
||||
@@ -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.7-windows-x64-setup.exe`, `Strata-0.2.7-linux-x86_64.AppImage`,
|
||||
`Strata-0.2.7-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.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
|
||||
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.7-windows-x64-setup.exe`,
|
||||
`Strata-0.2.7-linux-x86_64.AppImage`, `Strata-0.2.7-macos-arm64.dmg`, from the
|
||||
- **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
|
||||
[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.7-windows-x64-setup.exe`, `Strata-0.2.7-linux-x86_64.AppImage`) has no engine inside: on its first start the app
|
||||
(`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
|
||||
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.7-windows-x64-setup.exe` from the
|
||||
Install the [app](INSTALL.md) (`Strata-0.2.8-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
|
||||
|
||||
+62
-3
@@ -87,13 +87,13 @@ With a layer split, the engine options go into the config's `args`:
|
||||
|
||||
| Option | What it does |
|
||||
| --- | --- |
|
||||
| `"parallel": N` / `--batch N` / `--slots N` (2..8 normally; with a layer split up to 8 per group, see below) | up to N conversations have batch slots; more requests wait for a free slot. Each slot gets its own state (a session carved like the stage's own: GDN recurrence, QSA K/V and indexer, PLE history) on every GPU of the split. With grouped MTP, more than 8 slots can rotate through eight-row windows if memory permits. |
|
||||
| `"parallel": N` / `--batch N` / `--slots N` (2..8 normally; with a layer split up to 8 per group, 16 in a HIP build, see below) | up to N conversations have batch slots; more requests wait for a free slot. Each slot gets its own state (a session carved like the stage's own: GDN recurrence, QSA K/V and indexer, PLE history) on every GPU of the split. With grouped MTP, more than 8 slots can rotate through eight-row windows if memory permits. |
|
||||
| `--batch-groups G` | with a layer split (default: auto, below): the N slots in G groups that flow through the GPUs as a pipeline (GPU k runs one group while GPU k+1 runs another). G must divide N. 1 = all slots in one window, GPU after GPU. |
|
||||
| `--batch-groups auto` | the default on a layer split since 0.1.41 (give no `--batch-groups`): the engine pipelines one group per GPU stage (the most that divide the slots; 8 slots on 4 GPUs = 4 groups of 2) and says so (`INFO batch_groups=G`). `--batch-groups 1` turns it off (all slots in one window, GPU after GPU). Measured, 8 clients, total tok/s against one group: 4 x R9700 166 against 86, 2 GPUs 109 against 78, 3 GPUs 110 against 78. With `--adapt-async 1` and no `--batch-groups` the default is one group (the adaptive tier between batch windows does not run in pipelined groups) and the log says so; with `--batch-mtp` and no `--batch-groups` the pipeline wins and `--batch-mtp` is left off (`--batch-groups 1` or `STRATA_BATCH_MTP_SERIAL=1` keeps it); `--batch-groups G` or `--batch-groups auto` given with `--batch-mtp` pipelines the groups and turns `--batch-mtp` off, and given with `--adapt-async 1` pipelines the groups and turns `--adapt-async` off (the pipelined path does not adapt). | PROPOSED for 0.2.1 (owner decides; branch f-021-split): on NVIDIA builds with exactly two stages the default is one group instead (2x RTX A4000, IQ3_XXS, aggregate tok/s against the pipelined default: +16% at 2 slots, +12% at 4, +15% at 8, 5 of 5 pairs each; `--batch-groups auto` or a number asks for the pipeline, `STRATA_BATCH_GROUPS_NV_SERIAL=0` restores the old default).
|
||||
| `--trim-stage-weights` | with an **explicit** `--layer-split` (e.g. `12,24,36`, not `auto`): every GPU loads only the dense weights of its own layers instead of the whole model's (the same as `STRATA_STAGE_TRIM=1`, PR #639). The VRAM this frees goes to the expert cache. Useful without `--batch` too. |
|
||||
|
||||
The engine never refuses a count it cannot run: it says so in its log and runs what it can - at most 8 slots without a
|
||||
layer split (a window holds 8 rows; with a layer split and pipelined groups, 8 per group), as many as fit in VRAM, or none (one request at a time) when not two fit. The server reads
|
||||
layer split (a window holds 8 rows; with a layer split and pipelined groups, 8 per group, 16 in a HIP build: see "Windows of 16 rows" below), as many as fit in VRAM, or none (one request at a time) when not two fit. The server reads
|
||||
the count the engine reports (`INFO batch_slots=N`), and `GET /v1/status` says it (`concurrency.serving`).
|
||||
|
||||
## More than 8 slots on a layer split, and the opt-in admission and decode switches
|
||||
@@ -103,7 +103,7 @@ the count the engine reports (`INFO batch_slots=N`), and `GET /v1/status` says i
|
||||
**More than 8 slots.** With a layer split a pipelined window holds at most 8 rows, but each group of the pipeline is its
|
||||
own window, so `--batch 16` on 4 GPUs runs 4 groups of 4 slots (`--batch-groups auto`), `--batch 32` runs 4 groups of 8.
|
||||
Until 0.1.44.1 the engine cut such a request down to 8 slots and said so; it now runs what is asked for, as many slots
|
||||
as fit in VRAM and at most 8 per group (`INFO batch_slots=N`). `--batch-mtp` keeps its limit (rows per window are
|
||||
as fit in VRAM and at most 8 per group, 16 in a HIP build (`INFO batch_slots=N`; "Windows of 16 rows" below). `--batch-mtp` keeps its limit (rows per window are
|
||||
doubled by the proposals). Nothing changes for 8 slots or fewer. Every slot of a 16-slot pipelined batch decodes
|
||||
exactly the tokens it decodes alone (`tools/batch_test.py --n 16`, which now takes up to 32 prompts).
|
||||
|
||||
@@ -536,6 +536,65 @@ run without it; inside one run every slot still equals its own solo decode.
|
||||
Not run: AMD (HIP), the draft layer on the first stage, helper expert caches, more than two stages, SYCL (the SYCL
|
||||
tree has its own copy of the batch code and does not have this).
|
||||
|
||||
## Windows of 16 rows, and the four-card AMD profile
|
||||
|
||||
A verify or batch window held at most 8 rows. On a layer split every group of the pipeline is its own window, so 32 slots ran as
|
||||
4 groups of 8 and nothing larger could run. HIP builds now take windows of 16 rows: `--batch 64` on four cards is 4 groups of
|
||||
16, and `--batch 16 --batch-groups 1` is one 16-row window over all stages. The limit is a build option
|
||||
(`-DSTRATA_WINDOW_MAX_T=auto|8|16`, default `auto`):
|
||||
|
||||
| build | window | why |
|
||||
|---|---|---|
|
||||
| HIP (AMD) | 16 | the code objects load when a kernel is first used: the bigger binary (44.9 to 53.7 MB) costs +7 MiB per card after the first request and about 30 to 45 ms once per server start |
|
||||
| CUDA | 8 | CUDA loads every kernel when the engine starts: 16 adds 14 MB of kernels and +16 MiB of VRAM (RTX PRO 5000) and nothing is gained below 9 rows. On a 16 GB card those MiB shrink the auto-sized expert cache (2 x A4000: 5292 slots instead of 5301), and a cached expert rounds differently from a CPU one, so the texts differ from the 8-row build by that alone (main run with a 17 MiB larger `--vram-reserve-mib` gives the 16-row build's texts, 10 of 10). `-DSTRATA_WINDOW_MAX_T=16` builds it |
|
||||
| SYCL, Metal | 8 | not touched (they build the shared sources with the limit left at 8) |
|
||||
|
||||
A window of up to 8 rows runs exactly the kernels of 0.2.7; the code for 9 to 16 rows is only reached by a wider window. Checked
|
||||
instruction by instruction: HIP gfx1201, window 8 against the previous main, 3096 of 3097 kernels identical (the other is a
|
||||
`STRATA_DBG_GDN` debug variant) and 5 renamed; CUDA sm_86 SASS, 3393 of 3397 identical or renamed, the other four differ in the
|
||||
order of the same instructions. The CPU expert kernels keep 8 tokens per job (a group of 9 to 16 rows is two jobs over the same
|
||||
expert), and the hyper-connection read is made in slices of 8 rows.
|
||||
|
||||
Exactness, `tools/batch_test.py` (greedy tokens of every slot against the same prompt alone): 4 x R9700, 64 slots in 4 groups of 16
|
||||
(average 13.7 rows a window), 64 of 64 identical, with and without the three switches below; one R9700, `--batch 16`, 16 of 16;
|
||||
4 x R9700, `--batch 16 --batch-groups 1` (one 16-row window), 16 of 16; 32 slots in 4 groups of 8 with the profile below, 32 of 32.
|
||||
(Note: `--batch 16` on four stages is 4 groups of 4 slots, windows of 4 rows; it does not test 16-row windows.) Identity
|
||||
(`rules-144` flags, 10 prompts and a 4K and a 20K prompt, residual dumps compared): p620 (IQ3_S and IQ3_XXS), r9700, l620 and boston (CUDA, window 8 and 16), z440 and p1004 (CUDA, window 8) identical to main
|
||||
(z440 with window 16: see the CUDA row above).
|
||||
|
||||
### Four Radeon AI PRO R9700 cards: the serving profile
|
||||
|
||||
The four cards hold the whole of Qwen3.8-Flash-Next IQ3_S, so what limits them is how many requests decode together and how the
|
||||
four stages are fed. Measured through `serve/server.py` with the launcher's own configuration (400-token answers, short
|
||||
prompts, total tokens per second of the second round; 7.9K-token prompts for the long row):
|
||||
|
||||
| | 1 request | 8 at once | 32 at once | 4 long prompts, out / in tok/s | follow-up TTFT |
|
||||
|---|---:|---:|---:|---:|---:|
|
||||
| before: `parallel` 4, split auto | 66.6 | 156.6 | 155.4 | 24.3 / 1892 | 0.78 s |
|
||||
| profile (below) | 67.9 | 261.2 | 369.6 | 35.0 / 2706 | 0.77 s |
|
||||
|
||||
The profile, filled in by `setup.py` and by the launcher only for four cards of that kind (R9700, at least 28 GB each, no
|
||||
`--peer-device`) running Qwen3.8 IQ3_S:
|
||||
|
||||
- `parallel` 32 and `--batch-groups 4`: 32 slots, one group of 8 per card;
|
||||
- layer split `13,26,39`: the output head sits on the last card and costs about four layers, so it takes 9 and the others 13;
|
||||
- `--prefill 2048` instead of `auto`;
|
||||
- `STRATA_QSA_SLOTS=1`, `STRATA_BATCH_SELF_COMMIT=1`, `STRATA_SLOT_COPY_DIRECT=1` (switches from the table above).
|
||||
|
||||
What each part gives at 32 at once (tok/s, against 398.7 for the profile with the prompt cache off, which is the owner's profile B): without the three switches 354.3,
|
||||
split `auto` 374.6, 8 slots instead of 32 277. `--prefill auto` gives the same throughput but a longer first token for a 4.7K prompt
|
||||
(2.06 against 1.52 s). 64 slots give 418.9 at 64 clients against 398.4 for 32 slots (+5%, measured at an 8K context only) and were left out; `GPU_MAX_HW_QUEUES=4` changes nothing. The prompt cache stays on: turning it off was +8% at 32 clients
|
||||
with new prompts, but a follow-up turn of a conversation then waits 1.5 s for its first token instead of 0.8 s, and agent loops
|
||||
are mostly follow-up turns.
|
||||
|
||||
A setting of the person always wins: the profile fills only what is not set (the config, `--parallel`, `--layer-split`,
|
||||
`--batch`, `--prefill` other than `auto`, an environment variable). `STRATA_FOUR_CARD_PROFILE=0`, or `"four_card_profile": false`
|
||||
in the run config, turns it off. 25 minutes of 32 clients (1088 requests of 600 tokens) through the launcher's configuration ran
|
||||
without an error at a steady 435 tok/s and with no change in VRAM.
|
||||
|
||||
Not done: the hand-off of a batch window between stages (`§7f` of the parallelization report, one flag a group) is not part of
|
||||
this; it can emit a wrong token from time to time and is listed for later.
|
||||
|
||||
## Exactness
|
||||
|
||||
A batch row's arithmetic is the single-token window's, so with greedy decoding **every conversation of a batch
|
||||
|
||||
@@ -0,0 +1,98 @@
|
||||
# DeepSeek-V4.1-Flash on a Mac
|
||||
|
||||
DeepSeek-V4.1-Flash runs on Apple Silicon Macs on its own Metal engine, `strata-dsv41`. It is a separate download from the
|
||||
Qwen engine: the app and `strata launch` fetch it the first time you pick DeepSeek. This page lists what it needs, what it did
|
||||
on the one Mac it was measured on, and what it cannot do yet.
|
||||
|
||||
> **Short version:** a Mac with 64 GB of memory or more, macOS 15 or newer, and 260 to 280 GB of free disk on a fast drive.
|
||||
> Text only, no pictures. The model does not fit in memory, so part of it is read from the SSD for every word. On an M5 Max with
|
||||
> 128 GB, DeepSeek 2.00 answers a short question at 15 to 18 tokens a second and reads a 3,000-token prompt in about 11 seconds.
|
||||
|
||||
## What it needs
|
||||
|
||||
| | |
|
||||
|---|---|
|
||||
| Mac | Apple Silicon (M1 or newer), macOS 15 or newer. Measured on an M5 Max with 128 GB. |
|
||||
| Memory | 64 GB or more. A Mac with less still starts, but most of the model is read from the SSD for every word, so it is much slower. Setup says so and asks before it goes on. |
|
||||
| Disk | 2.00 set: 257 GB. 2.25 set: 274 GB (the 111 GB n-gram table is shared). Use the internal SSD or an NVMe drive, not a USB or network drive. |
|
||||
| Pictures | Off. DeepSeek's picture reader was not run on the Metal engine, so a Mac reads text only. Qwen on the Mac keeps its pictures. |
|
||||
|
||||
## Install it
|
||||
|
||||
In the app: Models, DeepSeek-V4.1-Flash, pick 2.00 bpw (the default) or 2.25 bpw, Download and prepare.
|
||||
|
||||
In the Terminal:
|
||||
|
||||
```sh
|
||||
strata launch --family deepseek41 --model ist-das-2.00bpw --plan # what it would do on this Mac
|
||||
strata launch --family deepseek41 --model ist-das-2.00bpw
|
||||
```
|
||||
|
||||
Setup downloads the set from Hugging Face (pinned revision, size and SHA-256 checked, resumable), builds the pack (a few MB),
|
||||
downloads the DeepSeek engine package for macOS from the signed update feed, and runs a short test question before it starts.
|
||||
Files you already have: `--model-path DIR`.
|
||||
|
||||
## What it did (M5 Max, 128 GB, 0.2.7.1 build)
|
||||
|
||||
Speeds are from the packaged engine with the shipped defaults (two Engram reader threads), one fresh process per run, two runs each. They were
|
||||
measured while another person's Qwen server (a 60 GB model, loaded and idle) was running on the same Mac, so the file cache that
|
||||
serves the streamed experts was shared with it. A Mac with nothing else running may be faster; that was not measured.
|
||||
|
||||
| | 2.00 bpw | 2.25 bpw |
|
||||
|---|---|---|
|
||||
| Short answer, code prompt | 15.2 to 16.0 tokens/s | 14.7 to 15.3 |
|
||||
| Short answer, agent prompt | 15.3 to 16.2 | 13.1 to 13.6 |
|
||||
| After a 3,000-token prompt | 14.5 to 15.1 | 11.7 to 11.8 |
|
||||
| After a 14,000-token prompt | 13.8 to 15.1 | 10.5 to 11.1 |
|
||||
| 3,000-token prompt | 236 to 241 tokens/s | 293 to 294 |
|
||||
| 14,000-token prompt | 479 to 512 | 553 to 586 |
|
||||
| 32,000-token prompt | 609 | 720 |
|
||||
| Read from the SSD per word | 48 MB | 111 MB |
|
||||
| Quality against the original model (KL, 4,995 positions) | 0.170 | 0.111 |
|
||||
|
||||
The 2.25 set is the better of the two (about 8% above the original model's perplexity on the Mac's scoring run, against about 17%). The 2.00 set is
|
||||
faster and smaller. The quality numbers come from the engine's scoring run with a fixed expert placement
|
||||
(`--profile-readonly`); a run that learns the placement as it goes moves the last digits.
|
||||
|
||||
How the memory is used: the engine keeps about 68 GB of the experts in memory on a 128 GB Mac. That share follows from the
|
||||
Mac's memory (the RAM less 40% and the engine's own footprint), not from what happens to be free at the start. The rest of the memory goes to
|
||||
macOS and its file cache, and a Mac with less memory keeps fewer experts. The remaining experts are read from the SSD in place.
|
||||
A decode token is limited by those SSD reads and by a CPU lane that computes the experts the GPU does not hold, not by the GPU.
|
||||
If another program already holds a lot of memory (a browser, another model's server), the engine keeps fewer experts so that
|
||||
macOS does not run out. That costs some speed and keeps the Mac responsive.
|
||||
|
||||
The needle test (a code hidden in a long text, asked for at the end) finds it at 14,600, 17,000, 19,500 and 26,200 tokens.
|
||||
|
||||
## Speed setting
|
||||
|
||||
DeepSeek's Speed choice (Faster = 5 experts a word, Full quality = 6) works as on a PC. On a Mac the engine's own default is
|
||||
Full quality. That is what was measured and scored.
|
||||
|
||||
## Limits
|
||||
|
||||
- Text only. A picture sent to a DeepSeek model on a Mac is refused.
|
||||
- The drafter (DSpark) is off. It gave the same answers but no speed-up on the Mac.
|
||||
- Context longer than about 32,000 tokens was not measured on a Mac.
|
||||
- Smaller Macs: nothing under 128 GB was run with DeepSeek. The plan screen shows how much of the model your Mac keeps in memory
|
||||
and warns when that is under a quarter of it. That estimate follows the engine's own rule and is not a measurement.
|
||||
- The 1.75 bpw set shares the 2.00 set's kernel but was not run on a Mac, so it is not offered there.
|
||||
- On the Mac, DeepSeek reads its Engram rows with two threads, because more threads gave slightly different words from run to
|
||||
run (in 3 to 9 of 16 runs; the cause is being studied). This costs about 3 to 12% of prompt speed (3,000 tokens about 12%,
|
||||
14,000 tokens 7 to 12%, 32,000 tokens about 3%); answer speed is the same. `DS4_ENGRAM_THREADS=N` sets another number.
|
||||
- Two runs of the same prompt can differ in the last digits of a score, because the engine moves experts between memory and the
|
||||
SSD as it learns what the prompt needs. The answers stay equally good.
|
||||
|
||||
## Building and packaging the engine (developers)
|
||||
|
||||
```sh
|
||||
bash packaging/mac/build_dsv41_engine.sh # configure, build, stage, package and verify the macOS package
|
||||
```
|
||||
|
||||
It needs the Xcode Command Line Tools, cmake and ninja (a pip venv is enough), Apple's metal-cpp and the llama.cpp tree that
|
||||
setup pins (`METAL_CPP_DIR`, `STRATA_GGML_DIR`). The result is `pkg/engine/<version>-macos-arm64-metal/` with `strata-dsv41`,
|
||||
the Metal sources it compiles at start-up and `engine-component.json`. The package is unsigned; the feed key signs it
|
||||
(`packaging/UPDATES.md`). The Qwen Metal engine is not rebuilt by this: the DeepSeek runtime rules sit behind
|
||||
`STRATA_DS4_RUNTIME` and are compiled only into `strata_ds4`.
|
||||
|
||||
Diagnostics, all off by default: `DS4_CPU_LOG=1` (per-token phase times), `DS4_PROF=1` (per-kernel GPU time),
|
||||
`DS4_GPU_TOTAL=1`, `DS4_ENC_TIMES=1`, `DS4_TLOG=1`, `DS4_TRACE_DISP=1`.
|
||||
+23
-4
@@ -472,7 +472,7 @@ applies at the next start. Network, key, MCP and program keys are not editable t
|
||||
| `repeat_stop_tokens` | `256` | A reply that repeats one token this many times in a row ends as `"length"`. `0` turns it off. |
|
||||
| `effort_position` | `"start"` | `"end"` puts a `low` or `medium` effort in a short system turn right before the answer, so a request that only changes the effort reuses the conversation cache. How well the model follows a level in that place is not measured. |
|
||||
| `anthropic_thinking` | as the template | `"on_request"` renders a `/v1/messages` request that asks for no thinking without thinking, as Anthropic does. Claude Code's short helper calls need it. |
|
||||
| `tool_call_recovery` | off | Also reads a declared tool's call written as `<parameter=NAME>`, as JSON inside `<tool_call>`, as a `<function=NAME>` block without `<tool_call>`, or as a second call inside one `<tool_call>`. On 1,462 agent turns 98.3% read as intended with it and 92.9% without. |
|
||||
| `tool_call_recovery` | on (since 0.2.8; off in 0.2.7 and before) | Also reads a declared tool's call written as `<parameter=NAME>`, as JSON inside `<tool_call>`, as a `<function=NAME>` block without `<tool_call>`, or as a second call inside one `<tool_call>`. On 1,462 agent turns 98.3% read as intended with it and 92.9% without; the 49 specimens of the test corpus read the same with it on or off. Only a declared tool's call is ever recovered; anything else stays text. `"tool_call_recovery": false` turns it off. |
|
||||
| `env_to_user` | off | Moves opencode's `<env>` block (working directory, date) to the first user message so the system turn is the same for every session and the disk prefix cache restores it. |
|
||||
| `return_progress` | off | Per request `"return_progress": true` adds llama.cpp's `prompt_progress` (`total`, `cache`, `processed`, `time_ms`) to the stream, one update per `--prefill` chunk. A prompt shorter than one chunk sends nothing. |
|
||||
| `prevent_sleep` | off | Windows only: keeps the PC awake while a request runs or waits (`SetThreadExecutionState`). On Linux and macOS one startup line says it does nothing. |
|
||||
@@ -501,6 +501,16 @@ applies at the next start. Network, key, MCP and program keys are not editable t
|
||||
guessed: `bash` is not `shell`, and a reordered name (`server1_label_values` for `label_values_mcp_server1`) is not the declared one.
|
||||
A call whose envelope drifted by a token (`<=NAME>`,
|
||||
`<function:NAME>` or `<NAME>` instead of `<function=NAME>`) is read as the call when NAME is a declared tool.
|
||||
- **What the parser repairs, and what it leaves.** A string argument that quotes a whole tool call, or repeats its own
|
||||
`</parameter><parameter=NAME>`, stays exact (#4078). A call whose `</function>` was left out is read as the call (#4427), in the
|
||||
answer and inside the thinking. An argument the tool declares as an array or an object, or declares nothing for, is read as
|
||||
written when it has a slip in it: a trailing or doubled comma, single quotes or `True`/`False`/`None`, a bare string between two
|
||||
members of an object (`"status": "pending", "in_progress", "pending": false`), brackets left open at the end. The server window
|
||||
says `N tool call argument(s) repaired` and `/metrics/prometheus` counts them in `strata:tool_args_repaired_total`. Text that
|
||||
is not an array or an object, a string cut short and a string-typed parameter are never touched. After the thinking has ended at
|
||||
a closing think tag, a second one at the start of a line in the answer is dropped (the model quoted the tag while thinking, #4077):
|
||||
the call that follows is kept. The reasoning between the two went out as the answer already, because a stream cannot take text
|
||||
back.
|
||||
- **A call that came back as text because its name was not declared (#3796).** The text stays exactly as the model wrote it and
|
||||
`finish_reason` stays `stop` (or `end_turn` on `/v1/messages`). Next to it the reply carries a list, only when there is something in it:
|
||||
`"strata_rejected_tool_calls": [{"name": "bash", "reason": "undeclared"}]` (at the top level of a Chat Completions reply, in
|
||||
@@ -698,6 +708,15 @@ the engine (v2 `memory.max` and `memory.high`, v1 `memory.limit_in_bytes`, less
|
||||
`--memory-limit-mib` cap. The startup line `strata generate: memory guard: limit X GiB (source: flag|cgroup|meminfo) ...` names
|
||||
the source. It covers parking and session files only, not the startup sizing of the arena, the resident budget or the Windows commit check.
|
||||
|
||||
**When the arena does not fit in the RAM that is free (#4051).** The engine checks the RAM before it reads the experts into the
|
||||
arena. A memory limit below the arena (a container) stops the start with the numbers. Less free RAM than the arena (an engine
|
||||
that was still exiting, another program) is waited for up to 20 s, and the log says so (`STRATA_ARENA_RAM_WAIT_S` changes the
|
||||
seconds, `0` loads at once); if it is still short the load goes on with a warning that says how much, never a refusal. Below half
|
||||
of the arena the warning adds that the whole PC can stall in memory reclaim, and that a systemd `MemoryHigh=` below the RAM keeps
|
||||
one program from taking all of it. The server, before it starts an engine, ends one that an earlier run left behind and now waits up
|
||||
to 120 s for it to be gone (it used to wait 10 s): an engine with tens of GB of pinned memory needs longer to give it back, and the
|
||||
next one started beside it ran the PC out of RAM (#4051).
|
||||
|
||||
Snapshots hold running state, checkpoints, used K/V pages and draft-layer K/V. They add host RAM, not VRAM. Oldest parked entries
|
||||
are evicted first. A new conversation that starts at a checkpoint inside a parked one (sub-agents that share a system prompt)
|
||||
restores that prefix and leaves the parked conversation in place (the log says "borrowed"). Oversized snapshots or host
|
||||
@@ -1019,7 +1038,7 @@ them on your own card.
|
||||
| `STRATA_PREFILL_EQUAL=1` | CUDA | A prompt segment is read in chunks of equal size, not full chunks and a short last one. Changes rounding. | |
|
||||
| `STRATA_OWNED_PRICE=exact` | CUDA | The cache sizing prices the prompt path's own buffers by their real allocation. | |
|
||||
| `STRATA_IQ_MT_MIN=1`, `STRATA_IQ3S_MT1=1` | CPU | See [Draft layer](#draft-layer) (reproducible output; the second gives IQ3_S the multi-token kernel for one token on Alder Lake and later without AVX-512). | |
|
||||
| `STRATA_ROUTE_PRIOR`, `STRATA_ROUTE_RESIDENT`, `STRATA_ROUTE_RESIDENT_MTP` | CUDA | Routing changes that add speed and also add KL. | Not recommended, except that 0.2.7 turns `STRATA_ROUTE_PRIOR=1.5` on by itself on a discrete NVIDIA card of 13 GiB or less (the owner approved the quality trade for 12 GB cards; the first 0.2.7 candidates used 2.0, see the 12 GB default in the release notes for why it is 1.5); `STRATA_ROUTE_PRIOR=0` turns it off and an explicit value wins. It is never a default on a Radeon, an integrated GPU or a bigger card. The 1.5 default was checked on an RTX 3060 12 GB (IQ3_XXS) with 80 tasks: no copy or code errors seen; agent loops may finish slightly less often, within the run-to-run spread (out of 10 agent tasks 4.5 passed on average over 4 runs at 1.5, against 5.6 over 7 runs with the prior off, where the off runs alone ranged from 4 to 6). Speed against off: story +15%, code +37%. Off: the Expert-settings row "Faster answers on 12 GB NVIDIA cards" or `STRATA_ROUTE_PRIOR=0`. |
|
||||
| `STRATA_ROUTE_PRIOR`, `STRATA_ROUTE_RESIDENT`, `STRATA_ROUTE_RESIDENT_MTP` | CUDA | Routing changes that add speed and also add KL. | Off by default. `STRATA_ROUTE_PRIOR=1.5` makes the router prefer experts that are already on the card; it is the one worth trying, on a discrete NVIDIA card of 13 GiB or less (an RTX 3060 12 GB, IQ3_XXS: story +15%, code +37% decode against off). It is an opt-in because it can repeat itself and give weaker results in long agent sessions (#4402; our own check on that card: out of 10 agent tasks 4.5 passed on average over 4 runs at 1.5, against 5.6 over 7 runs with the prior off, where the off runs alone ranged from 4 to 6; a stronger setting, 2.0, showed copy and code defects). Recommended for a 12 GB card used mostly for chat and short answers, not for long agent runs. 0.2.7 turned 1.5 on by itself on those cards and 0.2.8 took that back (owner decision of 2026-10-29): with nothing set, the engine routes exactly as 0.2.6 did (the same as `STRATA_ROUTE_PRIOR=0` and as `STRATA_DEFAULTS=026`). Turn it on: the Expert-settings row "Faster answers on 12 GB NVIDIA cards" (it writes `STRATA_ROUTE_PRIOR=1.5`; shown only for a discrete NVIDIA card of 13 GiB or less), `strata launch --set STRATA_ROUTE_PRIOR=1.5`, or the variable itself. Any value you set wins on every card; it is never on by itself. |
|
||||
| `STRATA_HOST_CORE=last\|sibling` (or `--host-core`) | Windows | `last` puts the host thread on the last physical core, away from the core that gets the GPU's interrupts. `sibling` keeps it on the first core's other hardware thread, so the workers keep every core. The default `first` leaves a hybrid CPU as it is. | One layer-split box went from a ±6% swing to steady and 8% faster. |
|
||||
| `--pool-affinity measure` | Hybrid CPUs, native packs | Times the `all` and `p-cores` layouts at start (about a second) and takes the faster. The measurement is taken once; where the E-cores' speed changes while the engine runs, `--pool-affinity p-cores` is the steady choice. | |
|
||||
| `STRATA_KEEP_EMPTY_TURNS=1`, `STRATA_TOPK_STREAM=0` | | See [TROUBLESHOOTING.md](TROUBLESHOOTING.md). | |
|
||||
@@ -1222,7 +1241,7 @@ On 12.0 cards (sm_120) these also apply:
|
||||
prompt time and start-up time, which is why smaller PCs keep the SSD reads.
|
||||
|
||||
A prompt chunk above 8192 (`--prefill auto:16384` or `auto:32768`) is not bit-exact on CUDA, so the default stays 8192 on
|
||||
NVIDIA. It is exact on gfx12, where 16384 is the default. `STRATA_ROUTE_PRIOR` raises KL, so below 0.2.7 it was opt-in; 0.2.7 sets 1.5 by itself only on a discrete NVIDIA card of 13 GiB or less. An 80-task check of 1.5 on an RTX 3060 saw no copy or code errors; agent loops may finish slightly less often, within the run-to-run spread; story +15%, code +37%.
|
||||
NVIDIA. It is exact on gfx12, where 16384 is the default. `STRATA_ROUTE_PRIOR` raises KL and is an opt-in again in 0.2.8 (0.2.7 set 1.5 by itself on a discrete NVIDIA card of 13 GiB or less, and the owner took that back on 2026-10-29 because of repetition loops and weaker results in long agent sessions, #4402). `STRATA_ROUTE_PRIOR=1.5` on an RTX 3060 12 GB: story +15%, code +37%, 80 tasks with no copy or code errors, but 4.5 of 10 agent tasks against 5.6 with it off.
|
||||
|
||||
---
|
||||
|
||||
@@ -1446,7 +1465,7 @@ with `--batch 2` at 262,144 context saw 538 MiB at the free line become 395 MiB
|
||||
| not stated | RTX 5070 Ti Laptop, Swift 1.5 IQ3_XXS, a new opencode session in a second folder | `env_to_user`: first request 14,073 tokens with 13,693 from the cache, 1.1 s, against 14,753 read, 11.9 s |
|
||||
| 0.1.40.2 | Tesla P100 | A question after a long document re-reads up to 16K tokens (55 s at 262K) without `strata_prefix` |
|
||||
| not stated | Codex CLI 0.160.0, Q2_0, RTX 5070 | First prompt 9,443 tokens in 10 s; later tool-loop turns reused about 96% from the cache and read the new part in 1-2 s. Compaction after an 85,000-token conversation reused 84,895 of 84,997 tokens with `codex_compaction_cache` and 41 of 80,683 without |
|
||||
| not stated | Signalnine/q27's drift corpus, 1,462 agent turns (Qwen3.8 under Claude Code) | `tool_call_recovery`: 98.3% read as intended, 92.9% without |
|
||||
| not stated | Signalnine/q27's drift corpus, 1,462 agent turns (Qwen3.8 under Claude Code) | `tool_call_recovery`: 98.3% read as intended, 92.9% without (on by default from 0.2.8) |
|
||||
| not stated | RTX 3080, RVN IQ3_S config, 8-turn chat replay on a 16K-token system prompt | NaN guard on and off: byte-identical answers in 3 pairs; cost 4.8 us of GPU time a window (7.8 us at most), 0.009% of decode time |
|
||||
|
||||
### Context beyond 262K (contributors' runs, RTX 5080, IQ3_S, native path, PR #84)
|
||||
|
||||
@@ -0,0 +1,143 @@
|
||||
# Strata on the NVIDIA DGX Spark (Linux, ARM64)
|
||||
|
||||
The DGX Spark is a small desktop box with a GB10 chip: a Blackwell GPU (compute capability 12.1, 48 SMs) and 20 Arm CPU cores that share
|
||||
128 GB of memory. Strata 0.2.7.1 runs Qwen3.8-Flash-Next and DeepSeek-V4.1-Flash on it. This page says what works, how to install it, what
|
||||
the Spark does with its memory, and how the speeds were measured. It was tested on one Spark (Ubuntu 24.04, driver 580.173, CUDA 13.0).
|
||||
Nothing here is known to work on another ARM machine.
|
||||
|
||||
## What works, and what does not
|
||||
|
||||
- **Works:** Qwen3.8-Flash-Next IQ3_S and the NVFP4 GPTQ pack (64K context, every expert in the GPU's cache), DeepSeek-V4.1-Flash 2.00 bpw
|
||||
(a short chat), the Terminal (`strata launch`, `strata update`, `strata serve`) and the engine packages of the signed feed.
|
||||
- **Not built for ARM:** the desktop app (no AppImage; use the Terminal), the HIP, SYCL and CPU-only engines, and the Q2_0 and Gyro packs,
|
||||
whose CPU kernels are x86-only (the engine refuses them at start). Picture reading is compiled in and was not tried on the Spark.
|
||||
- **Not tested:** contexts above 128K, more than eight chat slots (four and eight were measured: "Several chats at once" below), a long soak. `strata doctor --live` lists the GB10 with no memory
|
||||
figure, because `nvidia-smi` reports none for it.
|
||||
|
||||
## Install with the Terminal
|
||||
|
||||
```sh
|
||||
curl -fsSL https://github.com/Niko1221/Strata/releases/latest/download/install.sh -o install.sh && sh install.sh
|
||||
strata launch
|
||||
```
|
||||
|
||||
`install.sh` unpacks the Terminal into `~/.local/share/strata` (no administrator rights) and checks the release's signed feed. `strata launch`
|
||||
fetches the `linux-aarch64` engine package, shows what each model needs, downloads the one you choose and starts it. Pick Qwen3.8-Flash-Next
|
||||
IQ3_S (84 GB download) or the NVFP4 pack, or DeepSeek-V4.1-Flash 2.00 bpw (257 GB). The engine package needs glibc 2.39 or newer
|
||||
(Ubuntu 24.04, DGX OS 7) and NVIDIA driver 580.65 or newer.
|
||||
|
||||
## Install from a source checkout
|
||||
|
||||
```sh
|
||||
./setup.sh --setup --family qwen --model IQ3_S --context 65536 --vision no --yes --no-start --data-dir ~/Strata-data
|
||||
./setup.sh --setup --family nvfp4 --model NVFP4 --context 65536 --vision no --yes --no-start --data-dir ~/Strata-data
|
||||
```
|
||||
|
||||
`--data-dir` must be a folder you can write (the default, next to the checkout, may be under `/home`, which a normal user cannot create).
|
||||
Setup finds the GB10, calls it unified memory, takes the engine from the feed (or compiles it with `spark/build_engine.sh` when the release
|
||||
has none) and writes the run config with the settings measured on this part:
|
||||
|
||||
| Setting | Why |
|
||||
|---|---|
|
||||
| `--mmap-experts` | the experts are read from the model files in place, so the GPU's cache holds all 24,576 of them instead of a second copy in RAM |
|
||||
| `--prefill auto:32768` | a 32,768-token prompt chunk; the automatic 8,192 measured 2 to 6% slower here |
|
||||
| `--mtp-q4 all` | the draft layer as 4-bit copies: +3.5% decode, the answers do not change because the target checks every draft |
|
||||
| `--mtp-hnorm stream` | one norm per hyper-connection stream in the draft layer: draft acceptance 0.60 to 0.63, +4.3% decode |
|
||||
|
||||
Setup prints `--draft-vocab en` as a tip and does not set it: it measured +3.1% decode on English prompts, and it would cut the Chinese,
|
||||
Japanese and Korean drafts.
|
||||
|
||||
Start a model with its run script (`./run-iq3_s.sh`) or `./setup.sh`.
|
||||
|
||||
## Build the engine yourself
|
||||
|
||||
You need the CUDA 13.0 toolkit (`/usr/local/cuda`; DGX OS has it), cmake, ninja and Python 3.10 or newer.
|
||||
|
||||
```sh
|
||||
python3 -m venv .venv-build
|
||||
.venv-build/bin/pip install -r requirements.txt -r requirements-build.txt
|
||||
PATH="$PWD/.venv-build/bin:/usr/local/cuda/bin:$PATH" PYTHON="$PWD/.venv-build/bin/python" JOBS=16 sh spark/build_engine.sh
|
||||
cp -r out-spark/engine ./engine
|
||||
```
|
||||
|
||||
About 3 minutes for the Qwen engine and 6 with the DeepSeek engine and the picture reader (the first run also fetches llama.cpp at the
|
||||
pinned commit). `out-spark/engine/` holds `strata`, `strata-dsv41`, `strata-vision`, `libstrata_kq.so`, the CUDA libraries and a `BUILD.json`.
|
||||
`BUILD_VISION=0` and `BUILD_DSV41=0` leave a part out. `packaging/build_cuda_linux_aarch64.sh` runs this build and makes the engine packages
|
||||
for the feed ([packaging/README.md](../packaging/README.md)). A model on a Q2_0 pack cannot run: its CPU kernels need AVX-512.
|
||||
|
||||
## What the Spark does with its memory
|
||||
|
||||
The GPU's memory and the CPU's are the same 121.6 GB, so a model's experts are held once. With `--mmap-experts` there is one copy: the model
|
||||
file's pages, which the GPU cache uses.
|
||||
|
||||
| | IQ3_S | NVFP4 GPTQ |
|
||||
|---|---|---|
|
||||
| Expert bytes | 50.3 GB | 68.0 GB |
|
||||
| Expert cache | 24,576 of 24,576 slots, 46.84 GiB | 24,576 of 24,576 slots, 63.29 GiB |
|
||||
| n-gram table | 28.8 GB, locked in RAM (`--ple-io ram`) | 51.2 GB, read from the SSD |
|
||||
| Context | 64K, 8-bit KV, 32,768 cells streamed | same |
|
||||
| Free memory at start | about 1 GiB | about 1.2 GiB |
|
||||
|
||||
The NVFP4 table does not fit in RAM beside the page cache the 68 GB expert file needs. Locking it measured 137 tok/s on a 4K prompt and 26 tok/s
|
||||
decode, against 1,252 and 39 without. Leave it on the SSD (setup does).
|
||||
|
||||
DeepSeek-V4.1-Flash 2.00 bpw places 12,163 of its 15,744 experts in the GPU's share and runs the rest on the CPU lane from the model's files.
|
||||
The engine sizes that from the memory Linux can still give back (`MemAvailable` less 6 GiB, `DS4_UMA_HEADROOM_GIB`), because the model files' page
|
||||
cache is most of the memory `cudaMemGetInfo` calls used.
|
||||
|
||||
## Speeds
|
||||
|
||||
Measured on the release-candidate engine package of 0.2.7.1 with `tools/dgx_bench.py`: fresh 4K, 16K and 32K prompts at temperature 0, 256-token
|
||||
answers, 64K context, the model's own run config, four samples a cell in two blocks (on, off, off, on), medians. "Table off" is
|
||||
`STRATA_SM121_PERF=0`. One Spark, 27 October 2026; the figures move a few percent between sessions.
|
||||
|
||||
| Model | Table | Prompt tok/s (4K / 16K / 32K) | Decode tok/s (4K / 16K / 32K) |
|
||||
|---|---|---|---|
|
||||
| Qwen3.8-Flash-Next IQ3_S | on (default) | 1,296 / 1,680 / 1,735 | 45.5 / 46.5 / 46.5 |
|
||||
| Qwen3.8-Flash-Next IQ3_S | off | 1,203 / 1,416 / 1,276 | 48.5 / 45.3 / 45.2 |
|
||||
| Qwen3.8-Flash-Next NVFP4 | on (default) | 1,408 / 1,670 / 1,698 | 42.8 / 40.5 / 40.1 |
|
||||
| Qwen3.8-Flash-Next NVFP4 | off | 1,156 / 1,350 / 1,353 | 41.0 / 41.2 / 38.4 |
|
||||
|
||||
The table makes prompts faster: IQ3_S 1.20x over the three sizes (90% interval 1.12 to 1.31; 1.08x at 4K, 1.19x at 16K, 1.36x at 32K), NVFP4
|
||||
1.24x (1.14 to 1.36). It does not change decode speed in this check: 0.996x (0.97 to 1.02) on IQ3_S, with the 4K cell 0.94x, and 1.02x (1.00 to
|
||||
1.05) on NVFP4. The first run of the port (26 October) had measured +5% each for the two decode switches; this check did not see it.
|
||||
|
||||
DeepSeek-V4.1-Flash 2.00 bpw, 8K context, the default Faster mode (5 of 6 experts per word), 11,187 of 15,744 experts in the GPU's share: a
|
||||
short chat answers at 13 to 18 tok/s (the first answer after a start is the slowest). Full quality (`--experts 6`) places more of them and was
|
||||
measured at 17 to 23 tok/s on a 1.7K-token chat in the first run.
|
||||
|
||||
Several chats at once (`"parallel"` is 4 on a Spark for `strata launch`; the setup script leaves it off): IQ3_S, 2K-token prompts, 128-token cap,
|
||||
the aggregate tokens per second over all clients: 8 clients 35.6, 16 clients 41.7, 32 clients 47.3 at 4 slots; 32 clients 54.2 at 8 slots. With
|
||||
a prompt of its own and a hidden code for each client, every answer named its own code and nobody else's (8 clients at 4 slots, 16 clients at 8
|
||||
slots), and none failed or hung.
|
||||
|
||||
## The four speed defaults of this part
|
||||
|
||||
On a GB10 the engine switches on four settings that were measured here, and prints them at every start. `STRATA_SM121_PERF=0` (or
|
||||
`STRATA_DEFAULTS=026`, which gives back everything 0.2.7.1 changed) turns the whole set off; a switch you set yourself is kept.
|
||||
|
||||
| Switch | What it does | Measured in the first run (26 October) |
|
||||
|---|---|---|
|
||||
| `STRATA_PF_FUSED=1` | the fused IQ prompt kernels | IQ3_S prompt +10 to 27% |
|
||||
| `STRATA_PREFILL_NVFP4=w4a4` | NVFP4 prompt matrix product with FP4 activations | NVFP4 prompt +16% |
|
||||
| `STRATA_HC_UPMIX=1` | the hyper-connection read's up projection and mix in one kernel | decode +5.4% |
|
||||
| `STRATA_SPEC_COST=1` | the draft window follows what a draft costs | decode +4.7% |
|
||||
|
||||
**What the first two switches do to the answers.** The fused prompt path and the FP4 activations round differently from the paths they replace,
|
||||
and the other two do not touch the text (the fused hyper-connection read gives bit-identical logits; the cost-aware window only chooses
|
||||
which drafts to try, and the target checks every draft). 24 short greedy prompts gave 24 identical answers on IQ3_S and on NVFP4. On
|
||||
24 long prompts (1.5K to 24K tokens, the first-token logits compared with the switches off, `STRATA_DUMP_FIRST_LOGITS`) the distribution
|
||||
does move: IQ3_S mean KL 0.019 (largest 0.082), NVFP4 0.032 (largest 0.126); the most likely token changed on 2 of 24 prompts for each, in
|
||||
IQ3_S's case a near tie both times (0.20 against 0.20, 0.15 against 0.14); and a 24-token greedy answer stayed the same on 1 of 6 prompts. Two runs
|
||||
of the same setting gave identical logits, so the difference is the switches, not noise. All of the IQ3_S difference comes from the fused
|
||||
prompt path, and all of the NVFP4 difference from `w4a4`. On the 61-task quality suite (long-document retrieval, extraction of up to 72 exact rows, summaries, code, JSON, agent loops with tools), run through the server with the switches on and off: IQ3_S passed 56 tasks on and 51 off, NVFP4 56 on and 52 off. Retrieval found all 63 facts in every run, and no agent loop made an invalid tool call. The extraction facts were equal on IQ3_S (621 of 642 both ways); on NVFP4 one 40-row register lost a row with the switches on (616 against 621 of 642). One summary in each model gained or lost a flagged name or figure. Answers differ in wording on 21 of 61 tasks (IQ3_S) and 25 of 61 (NVFP4), and the wins and losses go both ways, so read the suite as "no loss we could measure", not as an improvement. The same setting run twice gave identical answers, so every difference comes from the switches. If you want the answers of 0.2.7, set `STRATA_SM121_PERF=0`: you keep the other
|
||||
Spark settings and lose the prompt speed-up.
|
||||
|
||||
With `"parallel": 4` (the default) the set adds up: 32 clients at 2,000-token prompts read 41.3 tok/s with all four on and 35.7 with all off. At
|
||||
`"parallel": 8` the two decode switches cost about 3.5% each; set `STRATA_HC_UPMIX=0 STRATA_SPEC_COST=0` there.
|
||||
|
||||
## What this does not cover
|
||||
|
||||
Decode on the 48-SM GB10 is limited by the GPU's work, about 930 kernel launches per token. No configuration switch measured here moved it more
|
||||
than a few percent. A DFlash2 drafter is slower here than the MTP drafter (37 against 45 tok/s) and is not used. Teacher-forced quality over
|
||||
long text and task suites was run only for the prompt path (above).
|
||||
+4
-3
@@ -24,6 +24,7 @@ install does not need a source checkout or a startup `.bat` file.
|
||||
| Linux x86-64 with Intel Arc | [Intel Arc guide](INTEL_ARC.md); install the driver/compute runtime and check model preparation requirements |
|
||||
| Apple Silicon Mac, macOS 15 or newer | [Mac guide](MAC.md). Intel Macs are not supported by the Mac package. |
|
||||
| AMD Ryzen AI Max / Strix Halo | [Strix Halo guide](STRIX_HALO.md) |
|
||||
| NVIDIA DGX Spark (Linux ARM64) | [DGX Spark guide](DGX_SPARK_LINUX.md). Use the Terminal; there is no desktop app for ARM64 Linux. |
|
||||
|
||||
Memory and disk needs depend on the **model, quantization, context and enabled features**. Check
|
||||
[which model fits](MODELS.md#pick-by-memory); download size is not the same as RAM use. Unified memory counts once.
|
||||
@@ -37,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.7-windows-x64-setup.exe`. It installs for your user and needs no administrator rights.
|
||||
- **Linux:** make `Strata-0.2.7-linux-x86_64.AppImage` executable, then open it. If FUSE is unavailable, run the
|
||||
- **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
|
||||
downloaded file with `--appimage-extract-and-run`. [Linux help](TROUBLESHOOTING.md).
|
||||
- **Mac:** open `Strata-0.2.7-macos-arm64.dmg`, drag Strata into Applications, then open it. [Mac help](MAC.md).
|
||||
- **Mac:** open `Strata-0.2.8-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
|
||||
|
||||
@@ -38,6 +38,11 @@ engine package. Otherwise [build the image yourself](DOCKER.md#build-it-yourself
|
||||
- **Linux**, for example Ubuntu 24.04, with Intel's GPU driver: the `xe` or `i915` kernel driver plus the compute runtime and
|
||||
Level Zero. On Ubuntu: `intel-opencl-icd libze1 libze-intel-gpu1`, or follow Intel's
|
||||
[client GPU guide](https://dgpu-docs.intel.com/driver/client/overview.html).
|
||||
- **The CPU and the model folder.** The CPU does not need AVX-512. A Ryzen 3000-5000 or an Intel Core 12th-14th generation runs the
|
||||
experts that do not fit on the card with AVX2 (measured on an A750 with a Ryzen 5 5600X, [INTEL.md](INTEL.md)). This applies to
|
||||
model folders that contain `native_experts.txt` (IQ3_XXS, IQ3_S, NVFP4, Unsloth). An older Q2_0 folder has no such file, and its
|
||||
CPU kernel needs AVX-512. With that folder the engine stops with "this CPU cannot run the expert kernel", and `strata doctor`
|
||||
warns about it before the start. Building the model folder again with `strata setup` (or `tools/iq_pack.py`) fixes it.
|
||||
- For a source build without Docker: **Intel oneAPI** with the DPC++ compiler (`icpx`, 2025.3 or newer) and **oneMKL**
|
||||
(about 5 GB), `cmake` 3.24+, `ninja`, `git`, Python 3. The build fetches ggml unless you point `STRATA_GGML_DIR` at a
|
||||
llama.cpp checkout.
|
||||
@@ -121,6 +126,13 @@ Things that matter on an Arc (details in [INTEL.md](INTEL.md)):
|
||||
- `--layer-split auto` on two equal cards takes the split that leaves the fullest card the most VRAM to spare, within 0.2% of
|
||||
the best predicted time (`STRATA_SPLIT_HEADROOM_TOL`). `STRATA_SPLIT_HEADROOM=0` turns that off. A full card under `xe` moves
|
||||
buffers to system memory instead of failing. The startup line names both splits when the choice moves.
|
||||
- **Two Arc B-series cards decode faster by default.** With exactly two B-series cards (B50, B60, B65, B70, B570, B580) in a layer
|
||||
split, the first card starts the next window while the second still checks this one (`--pipeline-windows 2`), and setup writes
|
||||
`--spec 2` for the pair. On 2x Arc Pro B65 this decodes 17% to 23% faster than the old serial `--spec 4` setup, 5 pairs out of 5:
|
||||
chat 71-74 tok/s against 60-63, JSON 80-83 against 67-68, code 72-78 against 59-65, plain text 67-68 against 56-57. The answers are
|
||||
those of a serial `--spec 2` run (100 of 100 agent requests); a serial `--spec 4` run differs in a rare near-tie (3 of 100).
|
||||
One card, an A-series card, a mixed pair and three or more cards run as before. `--pipeline-windows 0` turns it off, and a
|
||||
`--spec` of your own, put in the config's `user_args`, is kept.
|
||||
- **Arc Pro B60:** PCI id `8086:e211` (`lspci -nn`), `sycl-ls` prints `Intel(R) Arc(TM) Pro B60 Graphics 20.1.0`, so
|
||||
`-DSTRATA_SYCL_AOT=bmg-g21` is the right target. `setup_intel.py` knows both B60 ids, `e211` and `e221`. On a `--layer-split`
|
||||
the startup line "N experts are neither in VRAM nor mirrored" counts the other card's layers; read the following
|
||||
@@ -149,6 +161,7 @@ Things that matter on an Arc (details in [INTEL.md](INTEL.md)):
|
||||
| Community | One Arc Pro B60 24 GB | Coder IQ1_M with 4,042 of 12,288 experts mirrored in RAM: 11.9 tok/s decode | same |
|
||||
| Maintainers | Arc Pro B70 32 GB (xe), 0.1.40.2 and later | Flash-Next IQ3_S with 11.5k of 24.6k experts resident: 10-prompt gate passes, prompt 980-1,000 tok/s on 4K tokens, decode 30 (prose) to 41 (code) tok/s | [INTEL.md](INTEL.md#arc-pro-b70-with-a-model-that-does-not-fit-flash-next-iq3_s) |
|
||||
| Maintainers | Arc A750 8 GB (i915) | Flash-Next IQ3_XXS about 10-19 tok/s decode | [INTEL.md](INTEL.md#arc-a750-and-the-other-alchemist-cards) |
|
||||
| Maintainers | 2x Arc Pro B65 32 GB (xe), 0.2.8 | Flash-Next IQ3_S, every expert in VRAM, the JIT package build: decode 71-83 tok/s (chat, JSON, code), prompts 1,261 / 1,511 / 1,628 tok/s at 4K / 8K / 16K | the bullet above |
|
||||
|
||||
Two cards on `xe` (2x Arc Pro B70, IQ3_S, 0.1.44): `--layer-split auto` picked a split at about 11 tok/s where
|
||||
`--layer-split 30` ran about 54. The auto choice now prefers the split with the most VRAM headroom (above).
|
||||
|
||||
+2
-2
@@ -3,8 +3,8 @@
|
||||
This page is for people with an Apple Silicon Mac (M1 or newer). Strata runs there on its own Metal engine: the NVIDIA kernels
|
||||
are replaced by Metal ones (`metal/`), and the host code and the server are the PC's. Intel Macs cannot run it.
|
||||
|
||||
DeepSeek V4.1-Flash does not run on a Mac yet. It runs in the Terminal on NVIDIA RTX 30 and 50 cards and on the Radeon AI PRO
|
||||
R9700 ([MODELS.md](MODELS.md#deepseek-v41-flash)).
|
||||
DeepSeek V4.1-Flash runs on a Mac with 64 GB of memory or more, as text only: [DEEPSEEK_MAC.md](DEEPSEEK_MAC.md). It has its
|
||||
own engine, separate from the Qwen one described on this page.
|
||||
|
||||
> **On this page:** [What you need](#what-you-need) · [Install](#install) · [Defaults](#defaults-and-options) ·
|
||||
> [Gyro](#gyro-s-and-gyro-m-agentionais-trellis-experts) · [Measured](#measured-m5-max-128-gb) · [Limits](#limits-this-version) ·
|
||||
|
||||
+3
-2
@@ -71,11 +71,12 @@ you have the disk. Perplexity is measured on a fixed set of 4,995 test texts; lo
|
||||
| NVIDIA RTX 30, 40 and 50 series, 12 GB or more | Windows, Linux |
|
||||
| AMD Radeon AI PRO R9700 | Linux |
|
||||
| AMD Ryzen AI Max (Strix Halo, gfx1151, 128 GB unified; 64 GB or more shared with Windows) | Windows |
|
||||
| Apple Silicon Mac, 64 GB or more, macOS 15 or newer (text only, [DEEPSEEK_MAC.md](DEEPSEEK_MAC.md)) | macOS |
|
||||
|
||||
Tested on an RTX 3060 (Linux), an RTX 5070 (Windows), four R9700 cards (Linux) and a Ryzen AI Max+ 395 with 128 GB unified (Windows:
|
||||
14 to 16 tokens a second on code with the default Faster mode, 10 to 11 on chat, 78 tokens a second on a 4K prompt and 109 on
|
||||
8K). The RTX 30 kernels were also checked on an RTX A4000. Windows on RTX 30 and the RX 9070 are built but were not run. Mac
|
||||
and Strix Halo on Linux do not run it yet; on Strix Halo Windows the graphics memory is the RAM, so most of the experts are
|
||||
8K). The RTX 30 kernels were also checked on an RTX A4000. Windows on RTX 30 and the RX 9070 are built but were not run. On a
|
||||
Mac it was run on an M5 Max with 128 GB ([DEEPSEEK_MAC.md](DEEPSEEK_MAC.md)). Strix Halo on Linux does not run it yet; on Strix Halo Windows the graphics memory is the RAM, so most of the experts are
|
||||
read from the SSD and an NVMe drive matters.
|
||||
|
||||
RTX 40 cards are covered by the engine packages since 0.2.7 (sm_89 code); we have not run one ourselves, see [DEEPSEEK41.md](DEEPSEEK41.md#the-engine-package) for a user's measurement.
|
||||
|
||||
@@ -181,6 +181,9 @@ tokens. The tokens are the serial loop's, up to CPU rounding.
|
||||
- **Default:** on for AMD RDNA4 (gfx1200 / gfx1201) with `--serve`, the draft layer, and a split of two or more stages when the
|
||||
drafter can sit on the first card, otherwise three or more. The log says "is on by default for a layer split of N stages".
|
||||
`--pipeline-windows 0` or `STRATA_PIPELINE_AUTO=0` turns it off. NVIDIA builds keep it opt-in (`--pipeline-windows 2`).
|
||||
The Intel Arc engine turns it on for exactly two Arc B-series (Battlemage) cards, with `--serve` and the draft layer (`--spec 2`
|
||||
or more): on 2x Arc Pro B65 decode is 17% to 23% faster, and setup writes `--spec 2` for those cards ([INTEL_ARC.md](INTEL_ARC.md)).
|
||||
It runs exactly two cards, so one card, Arc A-series cards and three or more cards stay serial.
|
||||
- **Draft layer placement:** with the pipeline on, the draft layer loads on the first card, whose window has finished when the
|
||||
chain starts. `STRATA_MTP_STAGE0=0` keeps the old place.
|
||||
- **Off, with one log line saying why:** `--batch` slots, `--peer-device`, the helper caches (`--expert-cache-device1..3`, which
|
||||
|
||||
+5
-3
@@ -87,9 +87,11 @@ The choice is kept in the model's config (`"cuda": 12`). Starts and `UPDATE.bat`
|
||||
Setting the model up again chooses again (add `--cuda 12` to keep a forced choice). A Pascal or Volta card added to a model at a
|
||||
start (`--gpus`) moves that model to the CUDA 12 engine.
|
||||
|
||||
`--cuda 12` is also the way to run Strata with an NVIDIA driver older than 580. The CUDA 12 engine needs driver 528 or newer on
|
||||
Windows and 525 on Linux (`CUDA12_MIN_DRIVER` in `setup.py`); setup's "driver too old" stop says so. The Docker `cuda12` image
|
||||
asks for 575 ([DOCKER.md](DOCKER.md)). Drivers that old were not tested here.
|
||||
`--cuda 12` is also the way to run Strata with an NVIDIA driver older than 580. The oldest driver the CUDA 12 engine runs on is the
|
||||
`min_driver` of its package (575.51 or newer at the moment, as in the Docker `cuda12` image, [DOCKER.md](DOCKER.md)). Setup reads it from
|
||||
the installed package or the signed update feed and says it in the "driver too old" stop; `strata update` enforces the same number. NVIDIA's
|
||||
own table lets CUDA 12 programs start on older drivers (525 on Linux, 527 on Windows), but the packages are built with CUDA 12.9 and were
|
||||
not tested on such drivers, so Strata does not promise that range.
|
||||
|
||||
### Mixed cards
|
||||
|
||||
|
||||
+2
-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.7-linux-x86_64.AppImage` (it downloads the engine on first start), or `strata launch` | `linux-x86_64-hip-halo` |
|
||||
| Windows | `Strata-0.2.7-windows-x64-setup.exe` (it downloads the engine on first start), or `strata launch` | `windows-x86_64-hip-rdna3` |
|
||||
| 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` |
|
||||
|
||||
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
|
||||
|
||||
@@ -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.7-windows-x64-setup.exe`, `Strata-0.2.7-linux-x86_64.AppImage` or `Strata-0.2.7-macos-arm64.dmg`), not a
|
||||
(`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-engine-…` archive.
|
||||
|
||||
**`strata` is not found.**
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
// include/strata/core/auto_cache.hpp - the slot count of the automatic expert cache (--expert-cache auto), as a rule with a test.
|
||||
//
|
||||
// The auto cache is sized from the VRAM free after the weights, the session and the drafter, minus --vram-reserve-mib (the reserve is what
|
||||
// the operator wants LEFT once everything is loaded), the draft head that binds later (priced), the prompt path's own buffers when it does
|
||||
// not borrow cache slots (priced) and the second verifier of --pipeline-windows (priced). What was never priced is what the load allocates
|
||||
// AFTER the cache whatever the settings: the CUDA libraries the prompt path and the verifier warm up (cuBLAS handles and workspaces, the
|
||||
// lazily loaded modules) and the verifier's small buffers. Measured with STRATA_TRACE=1 on the same build, config and flags
|
||||
// (--vram-reserve-mib 700, 256K context, MTP, 4 drafts):
|
||||
// RTX A4000 16 GB (z440, IQ3_XXS): 883 MiB free after the cache -> 719 after the head and the prompt path (-164) -> 461 after the
|
||||
// verifier and the drafter's binding (-258 = 178 MiB draft head, priced, + 80) = 244 MiB unpriced
|
||||
// RTX A6000 48 GB (boston, IQ3_S): 919 -> 755 (-164) -> 459 (-296 = 213 MiB draft head, priced, + 83) = 247 MiB unpriced
|
||||
// --prefill 512 / 2048 / auto on the A4000: the same 164 + 80. v0.2.6 ends at 461 MiB on the same card: not new, but every 16 GB card with
|
||||
// the default 700 MiB ends under the 512 MiB line the engine itself warns about (#2065, #4474), and the warning cannot be cured by the
|
||||
// operator raising the reserve by the advice's figure without losing the cache it was meant to keep.
|
||||
// So the reserve is now honest: the cache leaves `reserve + kLateAllocMib`, and the card ends with about the reserve.
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
namespace strata::core {
|
||||
|
||||
/// MiB the load allocates after the cache that no setting prices (see above; measured 244-247, a little margin).
|
||||
constexpr int64_t kLateAllocMib = 256;
|
||||
|
||||
struct AutoCacheSlots {
|
||||
int64_t slots = 0; ///< the cache's slot count (of the largest blob), >= 0
|
||||
bool late_priced = false; ///< kLateAllocMib was left out of the cache
|
||||
};
|
||||
|
||||
/// `free_b`: VRAM free now; `reserve_b`: the reserve and everything else that is priced (bytes); `blob`: the largest slot; `min_slots`: the least a working
|
||||
/// cache needs (#496). `late_mib` 0 turns the late allocation off (STRATA_DEFAULTS=026). The late allocation is only left out when the cache still has
|
||||
/// `min_slots` after it: a small card keeps the sizing it had (its reserve is lowered further elsewhere, #496), never a smaller cache than it can work with.
|
||||
inline AutoCacheSlots auto_cache_slots(int64_t free_b, int64_t reserve_b, int64_t blob, int64_t min_slots, int64_t late_mib = kLateAllocMib) {
|
||||
AutoCacheSlots r;
|
||||
if (blob <= 0) return r;
|
||||
const int64_t plain = (free_b - reserve_b) / blob;
|
||||
const int64_t late = late_mib > 0 ? (free_b - reserve_b - (late_mib << 20)) / blob : plain;
|
||||
if (late_mib > 0 && late >= min_slots && late > 0) {
|
||||
r.slots = late;
|
||||
r.late_priced = true;
|
||||
} else {
|
||||
r.slots = plain > 0 ? plain : 0;
|
||||
}
|
||||
return r;
|
||||
}
|
||||
|
||||
} // namespace strata::core
|
||||
@@ -465,6 +465,33 @@ public:
|
||||
superseded_ += dropped;
|
||||
return dropped;
|
||||
}
|
||||
// A batch slot's chain holds one checkpoint (the one its admission copied in), while the main session's parked copy of the same
|
||||
// conversation keeps the whole chain - the root checkpoint at the end of an agent's system prompt and tools among them, the one its
|
||||
// next conversations borrow (borrows()). Parking the slot (#2526) would drop that copy as superseded (#342: the slot's chain holds its
|
||||
// deepest checkpoint) and the root with it: every sibling then reads the shared prefix again (#4420, 0.2.6 -> 0.2.7: 323 ms -> 4130 ms
|
||||
// for a 5 K-token prefix). True when drop_superseded(ids, images, checkpoints, cvec) would drop an entry that has a checkpoint the
|
||||
// given chain does not hold. Nothing is changed.
|
||||
bool supersedes_richer(const std::vector<int32_t>& ids, const std::vector<ConversationImageKey>& images,
|
||||
const std::vector<ConversationCheckpoint>& checkpoints, bool cvec) const {
|
||||
auto held = [&](const ConversationCheckpoint& c) {
|
||||
if (c.ids == ids && c.imgs == images) return true;
|
||||
for (const auto& k : checkpoints)
|
||||
if (k.ids == c.ids && k.imgs == c.imgs) return true;
|
||||
return false;
|
||||
};
|
||||
for (size_t i = 0; i < entries_.size(); ++i) {
|
||||
const auto& e = entries_[i];
|
||||
const ConversationCheckpoint* deepest = nullptr;
|
||||
for (const auto& c : e.checkpoints)
|
||||
if (!deepest || c.ids.size() > deepest->ids.size()) deepest = &c;
|
||||
if (e.cvec != cvec || !deepest || deepest->ids.empty() || !held(*deepest) ||
|
||||
(guarded_ != 0 && serials_[i] == guarded_))
|
||||
continue;
|
||||
for (const auto& c : e.checkpoints)
|
||||
if (!held(c)) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
size_t superseded() const { return superseded_; }
|
||||
/// --conversation-cache-park-gain N: the longest parked entry this conversation's outgoing state supersedes, in
|
||||
/// tokens, 0 when it has none. It is the entry drop_superseded() would remove, so the caller can tell whether a
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
// include/strata/core/defaults_026.hpp - STRATA_DEFAULTS=026: the master switch of the Qwen engine's 0.2.7 defaults.
|
||||
//
|
||||
// Every default the Qwen engine (`strata`) changed since v0.2.6 goes back to what v0.2.6 did when STRATA_DEFAULTS=026 is in the
|
||||
// environment (the app's "off" for its speed switches sends it): the residency-biased routing (STRATA_ROUTE_PRIOR), the wide
|
||||
// environment (the app's "off" for its speed switches sends it): the wide
|
||||
// hyper-connection up read (STRATA_GR_UP_WIDE), the stream-split hyper-connection read (STRATA_GR_V3), the int8 prompt attention
|
||||
// (STRATA_PROMPT_ATTN_IMMA), the four gfx12 decode switches of the arch table, the PLE residency policy (STRATA_PLE_AUTO_RAM), and the
|
||||
// gfx12 decode miss split of 0.15 (back to 0.55). KVA is off by default in 0.2.7 and stays whatever STRATA_KVA says.
|
||||
// The residency-biased routing (STRATA_ROUTE_PRIOR) was in this set in 0.2.7; it is off by default again (owner, 2026-10-29), so the master switch
|
||||
// has nothing to take back there, and route_prior_default() answers 0 with the switch on or off.
|
||||
// An explicit per-feature variable (STRATA_ROUTE_PRIOR=2, STRATA_GR_V3=1, STRATA_PLE_AUTO_RAM=1, ...) or flag (--pcie-frac, --ple-io)
|
||||
// still wins over the master switch. Each rule is a pure function that takes the answer of qwen_defaults_026() as its `v026`
|
||||
// argument, so a test can check both settings without touching the environment (src/core/arch_defaults_test.cpp,
|
||||
|
||||
@@ -36,6 +36,7 @@
|
||||
#include <cstddef>
|
||||
#include <condition_variable>
|
||||
#include <deque>
|
||||
#include <functional>
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
#include <thread>
|
||||
@@ -94,6 +95,16 @@ bool host_available_memory(HostMemory& m, const std::string& meminfo = "/proc/me
|
||||
const std::string& self_cgroup = "/proc/self/cgroup",
|
||||
const std::string& cgroup_root = "/sys/fs/cgroup");
|
||||
|
||||
/// #4051: wait for the RAM an arena needs to come back. An engine that was still exiting (or a program that was ending)
|
||||
/// holds memory a few seconds longer than its process; loading into that shortfall sent a 60 GiB host into reclaim for
|
||||
/// good. Calls `sample(available)` (false: the RAM cannot be read; the wait ends) every `step_s` seconds, through
|
||||
/// `sleep(step_s)`, until `available >= need` or `max_wait_s` seconds have gone. `waited` gets the seconds waited,
|
||||
/// `available` the last reading. True when the RAM is there. `progress(waited, available)` is called about every
|
||||
/// `note_s` seconds while waiting. `max_wait_s <= 0` is no wait at all.
|
||||
bool wait_for_ram(uint64_t need, double max_wait_s, double step_s, double note_s,
|
||||
const std::function<bool(uint64_t&)>& sample, const std::function<void(double)>& sleep,
|
||||
const std::function<void(double, uint64_t)>& progress, double& waited, uint64_t& available);
|
||||
|
||||
/// Bound a resident budget by RAM and commit capacity after headroom; leave 256 MiB more when clamping.
|
||||
/// Pass UINT64_MAX for commit when the platform does not report it.
|
||||
uint64_t clamp_resident_budget(uint64_t requested, uint64_t physical, uint64_t commit, uint64_t headroom);
|
||||
|
||||
@@ -8,6 +8,9 @@
|
||||
//
|
||||
// STRATA_SM86_PERF=0 turns the sm_86 table off (cards of compute capability 8.6: RTX 3050-3090, RTX A2000-A6000)
|
||||
// STRATA_SM60_PERF=0 turns the sm_60 table off (Pascal GP100: Tesla P100)
|
||||
// STRATA_SM121_PERF=0 turns the sm_121 table off (GB10: NVIDIA DGX Spark; new in 0.2.7.1, also off under STRATA_DEFAULTS=026). Its
|
||||
// switches are measured speed settings, not bit-exact ones: the fused prompt kernels and the FP4-activation NVFP4
|
||||
// prompt round differently from the paths they replace (24 of 24 greedy answers identical; no KL run yet).
|
||||
//
|
||||
// A switch the user set (to anything, including 0) is never overridden. Applied once at start, before any engine code reads its
|
||||
// switches; the table is used only when every visible CUDA device has the same compute capability.
|
||||
@@ -22,6 +25,8 @@ namespace strata::core {
|
||||
/// The environment settings that are the defaults of a card with compute capability major.minor (raw, as the driver reports it);
|
||||
/// empty for every other card and when its off switch is 0.
|
||||
std::vector<std::pair<std::string, std::string>> nv_default_env(int major, int minor);
|
||||
/// The same with the answer of STRATA_DEFAULTS=026 given (the sm_121 table is empty then), so a test can check both without the environment.
|
||||
std::vector<std::pair<std::string, std::string>> nv_default_env(int major, int minor, bool defaults026);
|
||||
|
||||
/// Sets `nv_default_env(major, minor)` in this process's environment where the user has not set the variable, prints one line to
|
||||
/// stderr when it set anything, and returns the names it set.
|
||||
|
||||
@@ -34,4 +34,15 @@ inline bool ple_auto_ram_allowed(const char* env_value, bool v026) {
|
||||
return !v026;
|
||||
}
|
||||
|
||||
/// #4518: the table was locked at start on the policy's word (table + arena + headroom fit), but what the rest of the engine takes after
|
||||
/// that (the KV pools, the pinned staging, the GPUs' host buffers) is not known then. On a 128 GB two-GPU Windows PC it left less than the
|
||||
/// 5 GiB the automatic conversation cache needs, and the cache turned itself off. The cache is worth more than the lock (parking a 100 K
|
||||
/// conversation is 1.1 s against 30 s to read it again, the lock was measured at 2-3% of prompt speed), so once the cache is to be sized:
|
||||
/// does giving the lock back make the automatic cache bigger? `available` is the RAM the cache sees now (the table locked), the table's
|
||||
/// pages count as available again once unlocked (clean file pages). Unknown telemetry: no.
|
||||
inline bool ple_lock_blocks_cache(std::optional<uint64_t> available, uint64_t table_bytes, int64_t min_free_mib) {
|
||||
if (!available || table_bytes == 0 || *available > UINT64_MAX - table_bytes) return false;
|
||||
return auto_conversation_cache_mib(*available + table_bytes, min_free_mib) > auto_conversation_cache_mib(available, min_free_mib);
|
||||
}
|
||||
|
||||
} // namespace strata::core
|
||||
@@ -49,16 +49,22 @@ inline bool sm120_default(int major, int minor) { return is_sm120(major, minor)
|
||||
// `total_bytes`: the card's memory (cudaDeviceProp::totalGlobalMem); `integrated`: a GPU that shares the PC's memory.
|
||||
constexpr double kSmallCardGiB = 13.0; // "a 12 GB card": 12 GB cards report 11.6 - 12.0 GiB, 16 GB ones 15.6 - 16.0
|
||||
|
||||
/// The residency-bias bonus a discrete CUDA card of 13 GiB or less gets when the user set nothing: THE one place the value lives. 1.5 since the
|
||||
/// owner decision of 2026-10-26 (it was 2.0 in the first 0.2.7 candidates): on the RTX 3060 12 GB with IQ3_XXS the copy and code check found real
|
||||
/// defects at 2.0 and none in 19 tasks at 1.5 or 1.0, and 1.5 keeps the speed (story +15%, code +37% against off). A longer 61-task check of
|
||||
/// 0 / 1.5 / 2.0 is still running; if it shows defects at 1.5 this falls back to 1.0.
|
||||
/// The residency-bias bonus the user can ask for on a discrete CUDA card of 13 GiB or less (STRATA_ROUTE_PRIOR=1.5; the app's Expert-settings row
|
||||
/// "Faster answers on 12 GB NVIDIA cards" writes it): THE one place the value lives. 1.5 since the owner decision of 2026-10-26 (2.0 had real defects in
|
||||
/// the copy and code check on the RTX 3060 12 GB with IQ3_XXS; none in 19 tasks at 1.5 or 1.0). Speed against off: story +15%, code +37%.
|
||||
/// It was the default on those cards in 0.2.7 only. Owner decision of 2026-10-29: OFF by default again (#4402: repetition loops and weaker results in long
|
||||
/// agent sessions; our own 0.2.7 agent check passed 4.5 of 10 tasks with it on against 5.6 with it off). It stays an opt-in.
|
||||
constexpr float kRoutePriorSmallCard = 1.5f;
|
||||
|
||||
/// STRATA_ROUTE_PRIOR when the user set nothing: kRoutePriorSmallCard (residency-biased routing, changes which experts run; owner-approved for 12 GB
|
||||
/// cards) on a discrete CUDA card of 13 GiB or less, 0 everywhere else. Measured on an RTX 3060 12 GB only.
|
||||
inline float route_prior_default(bool hip, bool integrated, double total_bytes, bool v026 = false) {
|
||||
return !v026 && !hip && !integrated && total_bytes > 0.0 && total_bytes / 1073741824.0 <= kSmallCardGiB ? kRoutePriorSmallCard : 0.0f;
|
||||
/// STRATA_ROUTE_PRIOR when the user set nothing: 0 on every card (residency-biased routing changes which experts run, so it is only on when asked for:
|
||||
/// STRATA_ROUTE_PRIOR=1.5, or the Expert-settings row). The same answer as v0.2.6 and as 0.2.7 with STRATA_ROUTE_PRIOR=0 or STRATA_DEFAULTS=026.
|
||||
/// The arguments are kept so a caller and the table test (arch_defaults_test) still ask the same question per card; none of them changes the answer.
|
||||
inline float route_prior_default(bool /*hip*/, bool /*integrated*/, double /*total_bytes*/, bool /*v026*/ = false) { return 0.0f; }
|
||||
|
||||
/// The lambda the engine uses given the text of STRATA_ROUTE_PRIOR (nullptr = unset): any set value is the user's own and wins on every card
|
||||
/// (atof; "0" and text that reads as 0 are off; "1.5" is what the Expert-settings row writes), unset is route_prior_default (0).
|
||||
inline float route_prior_resolve(const char* env, bool hip, bool integrated, double total_bytes, bool v026 = false) {
|
||||
return env != nullptr ? (float) std::atof(env) : route_prior_default(hip, integrated, total_bytes, v026);
|
||||
}
|
||||
|
||||
/// STRATA_GR_V3 (the stream-split hyper-connection read, another summation order) when the user set nothing: ON for a discrete compute capability
|
||||
|
||||
@@ -27,6 +27,7 @@
|
||||
#include "strata/core/layer.hpp"
|
||||
#include "strata/core/session.hpp"
|
||||
#include "strata/kernels/sampler.hpp"
|
||||
#include "strata/kernels/window_limits.hpp"
|
||||
|
||||
#include <cuda_runtime.h>
|
||||
|
||||
@@ -404,13 +405,13 @@ private:
|
||||
std::vector<SessionState*> slots_;
|
||||
bool batch_rec_ = false; ///< record_window is capturing a batch window
|
||||
int row_base_ = 0; ///< ... its hand-off rows start here (a pipeline group's own rows)
|
||||
int brow_[8] = {}; ///< ... and row t is slot brow_[t]
|
||||
int brow_[strata::kernels::kWindowMaxT] = {}; ///< ... and row t is slot brow_[t]
|
||||
bool last_batch_ = false; ///< the last run was a batch window (set_plan_slot: one group)
|
||||
std::map<std::vector<int>, cudaGraphExec_t> exec_bm_, commit_bm_; ///< full row layout avoids slot-ID collisions
|
||||
std::map<std::vector<int>, uint64_t> bm_used_; ///< last use of each captured layout (LRU)
|
||||
uint64_t bm_tick_ = 0;
|
||||
size_t batch_graph_limit_ = 0; ///< 0: keep every captured batch graph (0.1.39); N: LRU-evict beyond N layouts
|
||||
int last_rows_[8] = {}; ///< the slots of the last batch window's rows
|
||||
int last_rows_[strata::kernels::kWindowMaxT] = {}; ///< the slots of the last batch window's rows
|
||||
std::vector<int> bkey(const int* rows, int S, int hbase) const { // #871: the doorbell variant has its own graphs
|
||||
std::vector<int> k = batch_key(rows, S, hbase);
|
||||
k.push_back(ar_off_ ? 1 : 0);
|
||||
@@ -439,14 +440,14 @@ private:
|
||||
std::chrono::steady_clock::duration(b_last_.load(std::memory_order_acquire)));
|
||||
}
|
||||
std::chrono::steady_clock::time_point ext_checked_{}; ///< batch_poll's last check with external serving
|
||||
int32_t b_out_[8] = {};
|
||||
int32_t b_out_[strata::kernels::kWindowMaxT] = {};
|
||||
std::vector<strata::kernels::SamplerParams> slot_sp_;
|
||||
bool sample_rows(int S, std::string& err); ///< the sampled slots' rows of the last batch window
|
||||
int32_t* h_commitb_ = nullptr; int32_t* m_commitb_ = nullptr; // per slot [1, 0, pos, -1 ..], stride 2 + max_t
|
||||
int32_t* commitb_ = nullptr;
|
||||
float* tail_snap_b_ = nullptr; ///< per (slot, QSA layer) indexer tail snapshot
|
||||
void* arena_b_ = nullptr;
|
||||
int64_t last_pos_b_[8] = {};
|
||||
int64_t last_pos_b_[strata::kernels::kWindowMaxT] = {};
|
||||
bool capture_batch(const int* rows, int S, int hbase, std::string& err);
|
||||
// STRATA_PIPED_MTP (init_deferred_commit): the saved rows, laid out as the window scratch (global layer index) with
|
||||
// dc_rows_ rows per layer instead of max_t_; the slots' accepted prefixes (mapped and device, 2 + max_t_ per slot)
|
||||
@@ -534,7 +535,7 @@ private:
|
||||
unsigned* qcnt_ = nullptr; ///< S26 STRATA_QFUSE: the HC read's q8_1 group counters (n_embd / 32)
|
||||
unsigned long long* slot_off_d_ = nullptr; ///< E-6: the slot offsets on the device
|
||||
int64_t lb_ = 0, le_ = -1; ///< set_stage: the layers this verifier runs (-1: to the last)
|
||||
int handoff_rows_ = 0; ///< set_handoff_rows (0: kVerifyMaxT)
|
||||
int handoff_rows_ = 0; ///< set_handoff_rows (0: kVerifyNarrowT)
|
||||
const float* hand_in_ = nullptr;
|
||||
float* hand_out_ = nullptr;
|
||||
uint32_t* hflag_in_h_ = nullptr; ///< set_handoff_flags: the incoming hand-off's ready word (host / device)
|
||||
@@ -573,7 +574,7 @@ private:
|
||||
std::vector<unsigned long long> prof_h_;
|
||||
double prof_sum_[2][kProfPer] = {}; // [GDN / QSA layers][stage]
|
||||
int64_t prof_windows_ = 0;
|
||||
double prof_t_sum_[9][2][kProfPer] = {}; // the same per window size T (1..8), for profile_by_t_report
|
||||
double prof_t_sum_[9][2][kProfPer] = {}; // the same per window size T (1..8: wider windows are not profiled), for profile_by_t_report
|
||||
int64_t prof_t_win_[9] = {};
|
||||
bool rs_on_ = false; // STRATA_ROUTE_STATS
|
||||
unsigned long long* rs_dev_ = nullptr; // [9 T][n_layers][8 rows]: experts of row r that no earlier row of the window routed to
|
||||
@@ -589,7 +590,7 @@ private:
|
||||
float* ple_val_ = nullptr;
|
||||
int last_t_ = 0;
|
||||
int64_t last_pos0_ = 0;
|
||||
int32_t last_tokens_[8] = {};
|
||||
int32_t last_tokens_[strata::kernels::kWindowMaxT] = {};
|
||||
int64_t n_vocab_ = 0;
|
||||
cudaStream_t cs_ = nullptr;
|
||||
cudaStream_t sh_cs_ = nullptr;
|
||||
@@ -602,9 +603,10 @@ private:
|
||||
cudaEvent_t df_join_[2] = {};
|
||||
// [sel_long_][T]: 1 is the long-context QSA select (STRATA_SELECT_LONG=1: multi-CTA top-k, shuffle-free decode
|
||||
// scores; see run()); without it only [0] is ever captured
|
||||
cudaGraphExec_t exec_[2][9] = {};
|
||||
cudaGraphExec_t exec_nr_[2][9] = {}; // #871: the doorbell variant of a stage that is all-resident otherwise
|
||||
bool launched_[2][9] = {}, launched_nr_[2][9] = {}; ///< this graph has been launched once (early_ple_flag: a cold first launch raises the PLE flag first)
|
||||
// (per window size T = 1..kWindowMaxT: a solo window is at most 8 rows, but a merge window of a wide batch is not)
|
||||
cudaGraphExec_t exec_[2][strata::kernels::kWindowMaxT + 1] = {};
|
||||
cudaGraphExec_t exec_nr_[2][strata::kernels::kWindowMaxT + 1] = {}; // #871: the doorbell variant of a stage that is all-resident otherwise
|
||||
bool launched_[2][strata::kernels::kWindowMaxT + 1] = {}, launched_nr_[2][strata::kernels::kWindowMaxT + 1] = {}; ///< this graph has been launched once (early_ple_flag: a cold first launch raises the PLE flag first)
|
||||
int sel_long_ = 0;
|
||||
// fork 34a35db (STRATA_SIDE_MIXER=1): the GDN gates and the QSA indexer on a branch of the window's graph beside the
|
||||
// mixer's projections, joined before the recurrence / the attention
|
||||
@@ -672,7 +674,7 @@ private:
|
||||
void set_plan_slot(int grp);
|
||||
bool split_ = false; // opt-in (--spec-split): exact but slower, see the overlap study
|
||||
std::function<bool(cudaStream_t, std::string&)> pre_launch_; // set_pre_launch
|
||||
int groups_[9] = {};
|
||||
int groups_[strata::kernels::kWindowMaxT + 1] = {};
|
||||
float* h_ymiss_ = nullptr; float* m_ymiss_ = nullptr; // T * k * n_embd
|
||||
|
||||
// device
|
||||
|
||||
@@ -158,6 +158,11 @@ struct SetArgs {
|
||||
// their shared-memory tile anyway, so the same values come out; this only saves the F32 read of the same rows by
|
||||
// every column block of an expert (18-36 reads of 1.8-3.7 MB on a prompt) - MarLin's "small operand cheap".
|
||||
const uint16_t* x16 = nullptr;
|
||||
int skip_ge = 0; // (ds4's low-bit GGUF experts) > 0: experts of at least this many rows are the caller's (FP16 GEMMs)
|
||||
#if defined(STRATA_METAL) // (only on Metal: keeps the CUDA / HIP struct as it was)
|
||||
const int* hbounds = nullptr; // the same bounds on the host: [n_exp + 1], absolute rows
|
||||
int total_rows = 0; // the set's rows in all
|
||||
#endif
|
||||
};
|
||||
/// h[local row] = FP16 clamped swiglu of gate and up over x[row_idx[local row]] (F32 rows, ldx floats apart).
|
||||
void set_gate_up(const SetArgs& a, const float* x, int ldx, const int* row_idx, uint16_t* h16, int ldh, cudaStream_t s);
|
||||
|
||||
@@ -17,6 +17,8 @@
|
||||
// must be different buffers (every block reads the former while one block writes the latter).
|
||||
#pragma once
|
||||
|
||||
#include "strata/kernels/window_limits.hpp"
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
namespace strata::kernels {
|
||||
@@ -66,6 +68,9 @@ void fused_gr_quantize_q8_0(const uint16_t* bf16, uint8_t* out, int64_t n_blocks
|
||||
/// weight pointers and eps must be the same for every t); `xn_scratch` is n_tok * hc * n_embd floats. Every
|
||||
/// token's outputs are bitwise `fused_gr_read(a[t])`.
|
||||
constexpr int kFusedGrMaxT = 8;
|
||||
/// A window of more than kFusedGrMaxT tokens is read in slices of at most kFusedGrMaxT (every token's outputs are
|
||||
/// strictly its own, so the slices are bitwise the one read); `a` and `xn_scratch` hold up to kFusedGrWideT tokens.
|
||||
constexpr int kFusedGrWideT = kWindowMaxT;
|
||||
/// Returns true when it also wrote the q8_1 images (every token's q8_mixed set and this read supports it).
|
||||
bool fused_gr_read_multi(const FusedGrArgs* a, int n_tok, float* xn_scratch, void* stream,
|
||||
unsigned long long* stamp_buf = nullptr, int stamp_i0 = 0);
|
||||
|
||||
@@ -205,6 +205,10 @@ public:
|
||||
bool is_open() const;
|
||||
/// True when `PleIoOptions::lock` was asked for and the OS lock succeeded (false: pages only pre-touched).
|
||||
bool locked() const;
|
||||
/// Gives a resident lock back (Mmap mode): the pages stay mapped and cached, but the OS may reclaim them again, so the RAM counts as
|
||||
/// available. False when the table was not locked. Used by the residency policy when the table's lock would leave the
|
||||
/// conversation cache no room (#4518).
|
||||
bool unlock();
|
||||
uint64_t rows() const;
|
||||
/// Bytes of the whole table (rows x row bytes) while open; 0 when closed. The residency policy in
|
||||
/// generate.cpp sizes the table with this before it decides to keep it in RAM.
|
||||
|
||||
@@ -23,6 +23,8 @@
|
||||
// can keep one captured graph per layer with the maximum grid and a device-side count.
|
||||
#pragma once
|
||||
|
||||
#include "strata/kernels/window_limits.hpp"
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
namespace strata::kernels {
|
||||
@@ -100,6 +102,29 @@ void moe_hit_add(float* parts, const float* hit_out, const int32_t* dst, const i
|
||||
void moe_grouped_s2(const unsigned long long* grp_ptr, const int32_t* grp_start, const int32_t* n_groups,
|
||||
const int32_t* ent_dst, const int32_t* ent_tok, int64_t cap_groups, int64_t cap_entries,
|
||||
const uint8_t* x_q8_0, const float* x_scales, void* scratch, float* out, void* stream);
|
||||
#if STRATA_VERIFY_MAX_T > 8
|
||||
/// The same for a window of 9..16 rows (CUDA / HIP engines built with a wide window, window_limits.hpp): `max_group` is the
|
||||
/// most entries one group can hold = the window's rows (a token routes to an expert once). Up to 8 it is
|
||||
/// `moe_grouped_s2` (one pass of each kernel); 9..16 adds a second pass over the entries from the 9th on.
|
||||
void moe_grouped_s2_wide(const unsigned long long* grp_ptr, const int32_t* grp_start, const int32_t* n_groups,
|
||||
const int32_t* ent_dst, const int32_t* ent_tok, int64_t cap_groups, int64_t cap_entries,
|
||||
const uint8_t* x_q8_0, const float* x_scales, void* scratch, float* out, void* stream,
|
||||
int max_group);
|
||||
#endif
|
||||
/// What a verify window calls: `max_group` is the window's rows. A build with narrow windows (the default, and every SYCL / Metal
|
||||
/// build, whose kernel libraries have only `moe_grouped_s2`) runs `moe_grouped_s2`: its windows hold at most 8 rows.
|
||||
inline void moe_grouped_s2_window(const unsigned long long* grp_ptr, const int32_t* grp_start, const int32_t* n_groups,
|
||||
const int32_t* ent_dst, const int32_t* ent_tok, int64_t cap_groups, int64_t cap_entries,
|
||||
const uint8_t* x_q8_0, const float* x_scales, void* scratch, float* out, void* stream,
|
||||
int max_group) {
|
||||
#if STRATA_VERIFY_MAX_T > 8
|
||||
moe_grouped_s2_wide(grp_ptr, grp_start, n_groups, ent_dst, ent_tok, cap_groups, cap_entries, x_q8_0, x_scales, scratch, out,
|
||||
stream, max_group);
|
||||
#else
|
||||
(void) max_group;
|
||||
moe_grouped_s2(grp_ptr, grp_start, n_groups, ent_dst, ent_tok, cap_groups, cap_entries, x_q8_0, x_scales, scratch, out, stream);
|
||||
#endif
|
||||
}
|
||||
/// Plan v0.3 P6: the groups for `moe_grouped_s2` when every expert id is resident at base + id * blob (the MTP
|
||||
/// layer's 512 experts): counts[0] groups, counts[1] entries; entry dst = routing index, tok = index / k_per_tok.
|
||||
void moe_group_resident(const int32_t* ids, int n, int k_per_tok, const uint8_t* base, int64_t blob,
|
||||
|
||||
@@ -14,11 +14,15 @@
|
||||
// Everything that varies per window (token ids, `n_keep`) is read from DEVICE memory so the kernels can be captured.
|
||||
#pragma once
|
||||
|
||||
#include "strata/kernels/window_limits.hpp"
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
namespace strata::kernels {
|
||||
|
||||
inline constexpr int kVerifyMaxT = 8;
|
||||
/// The most rows one verify / batch window holds (8; 16 in the CUDA / HIP builds, window_limits.hpp). A window of up to
|
||||
/// kVerifyNarrowT rows runs the kernels it always ran.
|
||||
inline constexpr int kVerifyMaxT = kWindowMaxT;
|
||||
|
||||
/// For token t of T: conv over [history(3) | qkv_0 .. qkv_t] -> SiLU -> L2 norm of the q/k heads -> h[t].
|
||||
/// `history` is NOT written, but with `commit` (one token): it then keeps the token, as gdn_conv_commit. Bitwise
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
// include/strata/kernels/window_limits.hpp - how many rows one verify / batch window can hold.
|
||||
//
|
||||
// Until 0.2.7 a window held at most 8 rows. The CUDA and HIP engines now hold up to 16 (four pipelined groups of 16
|
||||
// slots = 64 slots on a four-card split: docs/BATCHING.md), so every compile-time maximum that follows the window
|
||||
// (the verifier's per-row arrays, the expert pool's per-token tables, the resident plan's one-thread-per-entry block)
|
||||
// is derived from ONE number. It is a macro because the SYCL and Metal builds compile the same shared headers and
|
||||
// must not change: they get the default, 8. The CUDA / HIP CMake sets STRATA_VERIFY_MAX_T from the cache variable
|
||||
// STRATA_WINDOW_MAX_T (8 or 16; -DSTRATA_WINDOW_MAX_T=8 builds an engine whose windows are the 0.2.7 ones).
|
||||
//
|
||||
// The rule every user of the number follows: a window of up to kVerifyNarrowT rows runs EXACTLY the code it ran at 8
|
||||
// (the same kernel instantiations, the same buffer sizes); the wider code is reached only by a window of 9..16 rows.
|
||||
#pragma once
|
||||
|
||||
#ifndef STRATA_VERIFY_MAX_T
|
||||
#define STRATA_VERIFY_MAX_T 8
|
||||
#endif
|
||||
|
||||
namespace strata::kernels {
|
||||
|
||||
/// The window every 0.2.7 kernel was built for: windows of up to this many rows keep their instantiations.
|
||||
inline constexpr int kVerifyNarrowT = 8;
|
||||
/// The most rows one window can hold in this build (8 or 16).
|
||||
inline constexpr int kWindowMaxT = STRATA_VERIFY_MAX_T;
|
||||
|
||||
static_assert(kWindowMaxT == 8 || kWindowMaxT == 16, "STRATA_VERIFY_MAX_T is 8 or 16 (the wide kernels are built for 16)");
|
||||
|
||||
} // namespace strata::kernels
|
||||
@@ -0,0 +1,148 @@
|
||||
// include/strata/program/pipe_probe.hpp - STRATA_P4_TIMING / STRATA_P4_TRACE: where the wall time of a pipelined batch window goes.
|
||||
//
|
||||
// Opt-in measurement for the layer-split batch loop (generate.cpp, `pump`). Off, every hook is one predictable branch and
|
||||
// reads no clock. On:
|
||||
// STRATA_P4_TIMING=1 per stage: the window's time from launch to done, the cycle between launches, the gap between a
|
||||
// window done and the next launch, the stage's busy share, the host launch cost; the pump's cost
|
||||
// per tick, the verdict work per window, how many groups were in flight, the ticks a free stage had
|
||||
// a group waiting or was starved. Printed to stderr with the batch summary line.
|
||||
// STRATA_P4_TRACE=PATH one line per window event ("L stage time_us group" at a launch, "D stage time_us 0" when the stage
|
||||
// is done), flushed on every line so a killed run keeps its trace. "1" or an empty value writes
|
||||
// ./strata-p4-trace.csv.
|
||||
// Written for the four-card R9700 investigation (docs/BATCHING.md): it found that the stages run 4 of 4 concurrently in the
|
||||
// decode phase and that the serial admissions were the idle time.
|
||||
#pragma once
|
||||
|
||||
#include <algorithm>
|
||||
#include <chrono>
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
|
||||
namespace strata::program {
|
||||
|
||||
class PipeProbe {
|
||||
public:
|
||||
using Clock = std::chrono::steady_clock;
|
||||
using Time = Clock::time_point;
|
||||
static constexpr int kMaxStages = 16;
|
||||
static constexpr int kMaxGroups = 64;
|
||||
|
||||
PipeProbe(int n_stages, size_t n_groups)
|
||||
: n_(std::min(n_stages, kMaxStages)), n_groups_(n_groups) {
|
||||
timing_ = std::getenv("STRATA_P4_TIMING") != nullptr;
|
||||
const char* tr = std::getenv("STRATA_P4_TRACE");
|
||||
if (tr != nullptr) {
|
||||
const char* path = (tr[0] == '\0' || std::strcmp(tr, "1") == 0) ? "strata-p4-trace.csv" : tr;
|
||||
trace_ = std::fopen(path, "w");
|
||||
if (trace_ != nullptr) std::setvbuf(trace_, nullptr, _IONBF, 0);
|
||||
else std::fprintf(stderr, "strata p4: cannot write the trace %s\n", path);
|
||||
}
|
||||
}
|
||||
~PipeProbe() { if (trace_ != nullptr) std::fclose(trace_); }
|
||||
PipeProbe(const PipeProbe&) = delete;
|
||||
PipeProbe& operator=(const PipeProbe&) = delete;
|
||||
|
||||
bool on() const { return timing_ || trace_ != nullptr; }
|
||||
bool timing() const { return timing_; }
|
||||
Time now() const { return on() ? Clock::now() : Time{}; }
|
||||
|
||||
/// The pump's cost: constructed at the top of `pump`, adds its life to the per-tick figure.
|
||||
struct PumpScope {
|
||||
PipeProbe* p = nullptr;
|
||||
Time t0{};
|
||||
explicit PumpScope(PipeProbe& probe) {
|
||||
++probe.ticks_;
|
||||
if (probe.on()) { p = &probe; t0 = Clock::now(); }
|
||||
}
|
||||
~PumpScope() { if (p != nullptr) p->pump_ms_ += ms(Clock::now() - t0); }
|
||||
};
|
||||
/// The verdict work of a window that reached the last stage.
|
||||
struct VerdictScope {
|
||||
PipeProbe* p = nullptr;
|
||||
Time t0{};
|
||||
explicit VerdictScope(PipeProbe& probe) {
|
||||
if (probe.timing_) { p = &probe; t0 = Clock::now(); ++probe.verdict_n_; }
|
||||
}
|
||||
~VerdictScope() { if (p != nullptr) p->verdict_ms_ += ms(Clock::now() - t0); }
|
||||
};
|
||||
|
||||
/// Per tick: stages with a window in flight, groups in flight.
|
||||
void busy(int stages, int groups) { busy_ += stages; inflight_ += groups; }
|
||||
/// A stage k >= 1 that is free: whether a group is waiting at it (else it is starved by the stage before).
|
||||
void free_stage(int k, bool group_waiting) {
|
||||
if (k < 0 || k >= n_) return;
|
||||
if (group_waiting) ++starve_idle_[k]; else ++starve_none_[k];
|
||||
}
|
||||
void span_start(Time t) { span_start_ = t; span_ms_ = 0; }
|
||||
void span_end(double wall_ms) { span_ms_ = wall_ms; }
|
||||
void polled(Time before) { if (on()) poll_ms_ += ms(Clock::now() - before); }
|
||||
/// Stage k reported its window done.
|
||||
void done(int k, int group) {
|
||||
if (!on() || k < 0 || k >= n_) return;
|
||||
const Time t = Clock::now();
|
||||
if (timing_ && launched_[k]) { win_ms_[k] += ms(t - at_[k]); ++win_n_[k]; }
|
||||
if (trace_ != nullptr) std::fprintf(trace_, "D %d %.3f 0\n", k, us(t));
|
||||
done_at_[k] = t;
|
||||
if (timing_ && group >= 0 && group < kMaxGroups && left_[group] != Time{}) { q_wait_ += ms(t - left_[group]); ++q_n_; }
|
||||
}
|
||||
/// Stage k launched `group`'s window; `before` is now() taken before the launch call.
|
||||
void launched(int k, int group, Time before) {
|
||||
if (!on() || k < 0 || k >= n_) return;
|
||||
const Time t = Clock::now();
|
||||
launch_ms_ += ms(t - before);
|
||||
at_[k] = t;
|
||||
launched_[k] = true;
|
||||
if (group >= 0 && group < kMaxGroups) left_[group] = t;
|
||||
if (trace_ != nullptr) std::fprintf(trace_, "L %d %.3f %d\n", k, us(t), group);
|
||||
if (timing_) {
|
||||
if (last_launch_[k] != Time{}) cycle_ms_[k] += ms(t - last_launch_[k]);
|
||||
if (done_at_[k] != Time{}) gap_ms_[k] += ms(t - done_at_[k]);
|
||||
last_launch_[k] = t;
|
||||
}
|
||||
}
|
||||
/// Print and reset (the batch summary line calls it when the loop goes idle).
|
||||
void flush() {
|
||||
if (!timing_) return;
|
||||
const double ticks = (double) std::max(1LL, ticks_);
|
||||
std::fprintf(stderr, "strata p4: pump %.3f ms/tick over %lld ticks; stage-busy %.2f of %d; group wait %.1f ms;\n",
|
||||
pump_ms_ / ticks, ticks_, busy_ / ticks, n_, q_n_ ? q_wait_ / (double) q_n_ : 0.0);
|
||||
for (int k = 0; k < n_; ++k) {
|
||||
const double n = win_n_[k] ? (double) win_n_[k] : 1.0;
|
||||
std::fprintf(stderr, "strata p4: s%d windows %lld win %.2f ms cycle %.2f ms gap %.2f ms busy %.0f%% launch %.2f ms\n",
|
||||
k, win_n_[k], win_ms_[k] / n, cycle_ms_[k] / n, gap_ms_[k] / n,
|
||||
100.0 * win_ms_[k] / std::max(1e-9, span_ms_), launch_ms_ / n);
|
||||
}
|
||||
std::fprintf(stderr, "strata p4: verdict work %.2f ms/window; inflight groups avg %.2f of %zu\n",
|
||||
verdict_ms_ / std::max(1.0, (double) verdict_n_), inflight_ / ticks, n_groups_);
|
||||
for (int k = 1; k < n_; ++k)
|
||||
std::fprintf(stderr, "strata p4: s%d free ticks: with a group waiting %lld, starved %lld\n", k, starve_idle_[k],
|
||||
starve_none_[k]);
|
||||
pump_ms_ = poll_ms_ = launch_ms_ = verdict_ms_ = busy_ = inflight_ = q_wait_ = 0;
|
||||
ticks_ = verdict_n_ = q_n_ = 0;
|
||||
for (int k = 0; k < kMaxStages; ++k) {
|
||||
win_ms_[k] = cycle_ms_[k] = gap_ms_[k] = 0;
|
||||
win_n_[k] = starve_idle_[k] = starve_none_[k] = 0;
|
||||
}
|
||||
span_ms_ = 0;
|
||||
}
|
||||
|
||||
private:
|
||||
static double ms(Clock::duration d) { return std::chrono::duration<double, std::milli>(d).count(); }
|
||||
static double us(Time t) { return std::chrono::duration<double, std::micro>(t.time_since_epoch()).count(); }
|
||||
|
||||
int n_ = 0;
|
||||
size_t n_groups_ = 0;
|
||||
bool timing_ = false;
|
||||
std::FILE* trace_ = nullptr;
|
||||
double pump_ms_ = 0, poll_ms_ = 0, launch_ms_ = 0, verdict_ms_ = 0, busy_ = 0, inflight_ = 0, q_wait_ = 0, span_ms_ = 0;
|
||||
long long ticks_ = 0, verdict_n_ = 0, q_n_ = 0;
|
||||
double win_ms_[kMaxStages] = {}, cycle_ms_[kMaxStages] = {}, gap_ms_[kMaxStages] = {};
|
||||
long long win_n_[kMaxStages] = {}, starve_idle_[kMaxStages] = {}, starve_none_[kMaxStages] = {};
|
||||
Time at_[kMaxStages] = {}, last_launch_[kMaxStages] = {}, done_at_[kMaxStages] = {};
|
||||
bool launched_[kMaxStages] = {};
|
||||
Time left_[kMaxGroups] = {};
|
||||
Time span_start_{};
|
||||
};
|
||||
|
||||
} // namespace strata::program
|
||||
+60
-2
@@ -99,7 +99,8 @@ set(STRATA_METAL_HOST_SOURCES
|
||||
${S}/spec/suffix_drafter.cpp ${S}/spec/controller.cpp ${S}/spec/draft_policy.cpp ${S}/spec/draft_source.cpp
|
||||
${S}/kernels/cpu/pool.cpp ${S}/kernels/cpu/expert_layout.cpp ${S}/kernels/cpu/native_expert.cpp
|
||||
${S}/kernels/cpu/hadamard.cpp ${S}/kernels/cpu/tq_cpu.cpp) # Gyro: the Hadamard rotation, the scalar trellis rows
|
||||
set_source_files_properties(${S}/core/device.cu ${S}/core/pinned.cu PROPERTIES LANGUAGE CXX)
|
||||
set_source_files_properties(${S}/core/device.cu ${S}/core/pinned.cu ${DS4}/ds4_vision.cu ${DS4}/vendor/exl3.cu ${DS4}/vendor/moe_win.cu ${DS4}/vendor/sampler.cu
|
||||
PROPERTIES LANGUAGE CXX)
|
||||
|
||||
# The kernel library: Metal implementations (src/engine/*.cpp) and, for every entry point not ported yet, a stub
|
||||
# that stops the engine with the entry point's name (src/engine/stubs.cpp, generated by tools/gen_stubs.py).
|
||||
@@ -108,7 +109,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.52-metal")
|
||||
target_compile_definitions(strata_metal_host PUBLIC STRATA_NATIVE_EXPERTS=1 STRATA_VERSION="0.1.54-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)
|
||||
|
||||
@@ -122,3 +123,60 @@ target_link_libraries(strata_metal_kernels PUBLIC strata_metal_host mtl_rt)
|
||||
|
||||
add_executable(strata ${S}/program/generate.cpp)
|
||||
target_link_libraries(strata PRIVATE strata_metal_kernels strata_metal_host strata_metal_kernels)
|
||||
|
||||
# --- the DeepSeek engine on this Metal runtime (M5MAX-DEEPSEEK-PLAN.md, steps 3+). The .cu sources are compiled as C++
|
||||
# through include/metal_compat, which gives them CUDA's names over the Metal runtime; the kernel entry points they call are
|
||||
# the Metal implementations in src/engine/* or the stubs from tools/gen_stubs.py that stop the engine with the name.
|
||||
set(DS4 ${STRATA_ROOT}/src/dsv41)
|
||||
# The DeepSeek engine's sources. Every .cu compiles as C++ through include/metal_compat; the EXL3 legacy path is left out
|
||||
# (the 2.25bpw text model never calls it) and the vision tower is included because ds4_model.cu references it.
|
||||
set(STRATA_DS4_SOURCES
|
||||
${DS4}/ds4_main.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/engine/ds4_finish.cpp ${DS4}/ds4_model.cu ${DS4}/ds4_gguf.cu ${DS4}/ds4_iq.cu ${DS4}/ds4_kernels.cu ${DS4}/ds4_tq.cu
|
||||
${DS4}/ds4_vision.cu
|
||||
${DS4}/vendor/exl3.cu ${DS4}/vendor/exl3_cpu.cpp ${DS4}/vendor/moe_win.cu ${DS4}/vendor/sampler.cu
|
||||
${DS4}/ds4_gg_cpu.cpp ${DS4}/ds4_gguf_cpu.cpp ${DS4}/ds4_iq_cpu.cpp ${DS4}/ds4_tq_cpu.cpp ${DS4}/ds4_packinfo.cpp)
|
||||
set_source_files_properties(
|
||||
${DS4}/ds4_model.cu ${DS4}/ds4_gguf.cu ${DS4}/ds4_iq.cu ${DS4}/ds4_kernels.cu ${DS4}/ds4_tq.cu ${DS4}/ds4_vision.cu
|
||||
${DS4}/vendor/exl3.cu ${DS4}/vendor/moe_win.cu ${DS4}/vendor/sampler.cu
|
||||
PROPERTIES LANGUAGE CXX)
|
||||
list(APPEND STRATA_DS4_SOURCES ${CMAKE_CURRENT_SOURCE_DIR}/src/engine/exl3_legacy_stub.cpp) # v1.1.8 EXL3: absent on Metal
|
||||
# The CPU lane for the experts the GPU does not hold (ggml-cpu's NEON row dots): without it every non-resident expert contributes
|
||||
# zero and the residual stream stays the embedding (measured: layers 0-3 changed nothing, then random tokens).
|
||||
set_source_files_properties(${DS4}/ds4_gg_cpu.cpp PROPERTIES COMPILE_DEFINITIONS "STRATA_DSV41_TQ_CPU=1")
|
||||
# The DeepSeek engine's queue, residency and barrier rules (deferred command buffers, the second queue, the residency set shared with
|
||||
# strata::gpu, ...) live in mtl_rt.mm and gpu/runtime.cpp behind STRATA_DS4_RUNTIME. strata_ds4 compiles its own copy of these two
|
||||
# files with it; the Qwen engine (strata) and every other target keep the code of 0.2.7 (the macro is off there, so the files read
|
||||
# exactly as they did). The copies are linked as objects, so they win over the same symbols in the libraries.
|
||||
add_library(ds4_rt_mm OBJECT src/mtl_rt.mm)
|
||||
target_include_directories(ds4_rt_mm PRIVATE src ${STRATA_ROOT}/third_party/ggml ${STRATA_ROOT}/include)
|
||||
target_compile_options(ds4_rt_mm PRIVATE $<$<COMPILE_LANGUAGE:OBJCXX>:-fobjc-arc>)
|
||||
target_compile_definitions(ds4_rt_mm PRIVATE STRATA_DS4_RUNTIME=1 STRATA_GGML_COMMON_DEFAULT="${GGML_COMMON}"
|
||||
STRATA_METAL_KERNELS_DEFAULT="${CMAKE_CURRENT_SOURCE_DIR}/kernels")
|
||||
add_library(ds4_rt_gpu OBJECT src/gpu/runtime.cpp)
|
||||
target_include_directories(ds4_rt_gpu BEFORE PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/include/metal_compat
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/include ${STRATA_ROOT}/include)
|
||||
target_include_directories(ds4_rt_gpu SYSTEM PRIVATE ${METAL_CPP_DIR})
|
||||
target_compile_definitions(ds4_rt_gpu PRIVATE STRATA_DS4_RUNTIME=1 STRATA_GPU_METAL=1)
|
||||
add_executable(strata_ds4 ${STRATA_DS4_SOURCES} $<TARGET_OBJECTS:ds4_rt_mm> $<TARGET_OBJECTS:ds4_rt_gpu>)
|
||||
target_include_directories(strata_ds4 PRIVATE
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/include/metal_compat ${STRATA_ROOT}/include ${DS4} ${CMAKE_CURRENT_SOURCE_DIR}/src
|
||||
${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_ds4 PRIVATE STRATA_METAL=1) # the real host CPU lane (ds4_gg_cpu.cpp)
|
||||
target_compile_options(strata_ds4 PRIVATE -Wno-unused-parameter -Wno-deprecated-declarations -Wno-implicit-function-declaration)
|
||||
target_link_libraries(strata_ds4 PRIVATE strata_metal_kernels strata_metal_host)
|
||||
# The commit the binary was built from, readable with --version (the root CMakeLists does the same for the CUDA and HIP builds):
|
||||
# -DSTRATA_BUILD_ID=..., else BUILD_ID beside the sources (a git archive has no .git), else git, else "unknown".
|
||||
if(NOT STRATA_BUILD_ID AND EXISTS ${STRATA_ROOT}/BUILD_ID)
|
||||
file(STRINGS ${STRATA_ROOT}/BUILD_ID STRATA_BUILD_ID LIMIT_COUNT 1)
|
||||
endif()
|
||||
if(NOT STRATA_BUILD_ID)
|
||||
execute_process(COMMAND git -C ${STRATA_ROOT} rev-parse --short=9 HEAD OUTPUT_VARIABLE STRATA_BUILD_ID OUTPUT_STRIP_TRAILING_WHITESPACE ERROR_QUIET)
|
||||
endif()
|
||||
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
|
||||
if(STRATA_DS4_VERSION)
|
||||
set_source_files_properties(${DS4}/ds4_main.cpp PROPERTIES COMPILE_OPTIONS "-USTRATA_VERSION;-DSTRATA_VERSION=\"${STRATA_DS4_VERSION}\"")
|
||||
endif()
|
||||
@@ -0,0 +1,72 @@
|
||||
// metal/include/metal_compat/cublas_v2.h - the cuBLAS surface the dsv41 sources use, over the Metal device runtime.
|
||||
//
|
||||
// The engine makes 9 GEMM calls (7 cublasGemmEx, 2 cublasSgemm) for the dense projections, the LM head and the vision
|
||||
// tower's matmuls. On Metal the dense path is kernels/dense_mm.metal; this header gives the calls the CUDA names so the
|
||||
// shared sources compile unchanged, and routes them to strata::gpu::dense_gemm(), which the metal host runtime provides.
|
||||
//
|
||||
// Compile-audit finding (M5 Max, 2026-10-23): the dsv41 core needs exactly cublasCreate/Destroy/SetStream/SetWorkspace,
|
||||
// cublasSgemm and cublasGemmEx, plus cublasStatus_t/cublasHandle_t/cublasOperation_t/cublasComputeType_t/cudaDataType_t.
|
||||
#pragma once
|
||||
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
|
||||
#include "cuda_runtime.h"
|
||||
#include "ds4_metal_host.h" // the kernels that have a Metal dispatch
|
||||
|
||||
typedef void* cublasHandle_t;
|
||||
|
||||
enum cublasStatus_t { CUBLAS_STATUS_SUCCESS = 0, CUBLAS_STATUS_NOT_INITIALIZED = 1, CUBLAS_STATUS_ALLOC_FAILED = 3,
|
||||
CUBLAS_STATUS_INVALID_VALUE = 7, CUBLAS_STATUS_ARCH_MISMATCH = 8, CUBLAS_STATUS_NOT_SUPPORTED = 14 };
|
||||
enum cublasOperation_t { CUBLAS_OP_N = 0, CUBLAS_OP_T = 1, CUBLAS_OP_C = 2 };
|
||||
enum cublasComputeType_t { CUBLAS_COMPUTE_16F = 64, CUBLAS_COMPUTE_32F = 68, CUBLAS_COMPUTE_32F_FAST_16F = 74 };
|
||||
enum cublasGemmAlgo_t { CUBLAS_GEMM_DEFAULT = -1, CUBLAS_GEMM_DEFAULT_TENSOR_OP = 99 };
|
||||
enum cudaDataType_t { CUDA_R_32F = 0, CUDA_R_16F = 2, CUDA_R_16BF = 14, CUDA_R_8F_E4M3 = 28 };
|
||||
using cudaDataType = cudaDataType_t; // the engine names it both ways
|
||||
|
||||
static inline cublasStatus_t cublasCreate(cublasHandle_t* h) {
|
||||
*h = (void*) 1; // the Metal runtime is a singleton: the handle carries no state
|
||||
return CUBLAS_STATUS_SUCCESS;
|
||||
}
|
||||
static inline cublasStatus_t cublasDestroy(cublasHandle_t) { return CUBLAS_STATUS_SUCCESS; }
|
||||
static inline cublasStatus_t cublasSetStream(cublasHandle_t, cudaStream_t) { return CUBLAS_STATUS_SUCCESS; }
|
||||
static inline cublasStatus_t cublasSetWorkspace(cublasHandle_t, void*, size_t) { return CUBLAS_STATUS_SUCCESS; }
|
||||
|
||||
// C = alpha * op(A) * op(B) + beta * C, all row-major floats.
|
||||
// NOT WIRED YET: the metal host runtime has no dense GEMM entry point. This aborts with a message instead of returning a
|
||||
// wrong result, so the shared sources compile and a run fails where the work is, not somewhere confusing. The real
|
||||
// implementation drives kernels/dense_mm.metal (the kernel exists and passes its parity test).
|
||||
[[noreturn]] static inline void strata_metal_dense_gemm_missing(const char* what) {
|
||||
std::fprintf(stderr, "strata metal: %s is not wired to kernels/dense_mm.metal yet\n", what);
|
||||
std::abort();
|
||||
}
|
||||
|
||||
static inline cublasStatus_t cublasSgemm(cublasHandle_t, cublasOperation_t opA, cublasOperation_t opB, int m, int n, int k,
|
||||
const float* alpha, const float* A, int lda, const float* B, int ldb,
|
||||
const float* beta, float* C, int ldc) {
|
||||
// the mHC mixes (ds4_model.cu:2737): C = A^T B, F32, alpha 1, beta 0
|
||||
if (opA == CUBLAS_OP_T && opB == CUBLAS_OP_N && alpha && beta && *alpha == 1.0f && *beta == 0.0f &&
|
||||
strata::dsv41::metal_host::sgemm_tn(A, B, C, m, n, k, lda, ldb, ldc)) {
|
||||
return CUBLAS_STATUS_SUCCESS;
|
||||
}
|
||||
strata_metal_dense_gemm_missing("cublasSgemm");
|
||||
}
|
||||
|
||||
// the mixed-precision form: any argument list, because the engine passes cuBLAS's full 19-argument spelling; the body
|
||||
// names kernels/dense_mm.metal, which is where the real implementation goes.
|
||||
// the chunk path's call (F16 weights [m][k] x F16 rows [n][k] -> F32 rows [n][m]): the MPP tile GEMM
|
||||
static inline cublasStatus_t cublasGemmEx(cublasHandle_t, cublasOperation_t ta, cublasOperation_t tb, int m, int n, int k,
|
||||
const void* alpha, const void* A, cudaDataType_t at, int lda, const void* B,
|
||||
cudaDataType_t bt, int ldb, const void* beta, void* C, cudaDataType_t ct, int ldc,
|
||||
cublasComputeType_t, cublasGemmAlgo_t) {
|
||||
if (ta == CUBLAS_OP_T && tb == CUBLAS_OP_N && at == CUDA_R_16F && bt == CUDA_R_16F && ct == CUDA_R_32F && lda == k &&
|
||||
strata::dsv41::metal_host::gemm_f16_nt((const uint16_t*) A, (const uint16_t*) B, ldb, (float*) C, ldc, n, m, k, *(const float*) alpha,
|
||||
*(const float*) beta))
|
||||
return CUBLAS_STATUS_SUCCESS;
|
||||
strata_metal_dense_gemm_missing("cublasGemmEx (this operand combination)");
|
||||
}
|
||||
|
||||
template <typename... A>
|
||||
static inline cublasStatus_t cublasGemmEx_unsupported(A...) {
|
||||
strata_metal_dense_gemm_missing("cublasGemmEx");
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
// metal/include/metal_compat/cuda_bf16.h - the handful of CUDA bf16/fp8 names the dsv41 sources use, on Metal.
|
||||
//
|
||||
// Only what the engine touches (found by compiling src/dsv41/*.cu against this directory, 2026-10-23): the storage typedef
|
||||
// and four conversions. On a Mac the bf16 storage is a uint16_t and the conversion is the standard round-to-nearest-even
|
||||
// truncation CUDA's __float2bfloat16_rn does; fp8 is E4M3 with the same rounding.
|
||||
#pragma once
|
||||
|
||||
#include "cuda_fp16.h" // __half_raw, which the fp8 conversions return
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
|
||||
typedef uint16_t __nv_bfloat16;
|
||||
typedef uint16_t __nv_bfloat162;
|
||||
typedef uint8_t __nv_fp8_storage_t;
|
||||
|
||||
static inline uint16_t strata_f32_to_bf16_rn(float f) {
|
||||
uint32_t u;
|
||||
std::memcpy(&u, &f, sizeof u);
|
||||
if ((u & 0x7fffffffu) > 0x7f800000u) return (uint16_t) ((u >> 16) | 0x0040u); // NaN keeps a payload
|
||||
uint32_t r = (u + 0x7fffu + ((u >> 16) & 1u)) >> 16;
|
||||
return (uint16_t) r;
|
||||
}
|
||||
|
||||
static inline __nv_bfloat16 __float2bfloat16_rn(float f) { return strata_f32_to_bf16_rn(f); }
|
||||
static inline uint16_t __bfloat16_as_ushort(__nv_bfloat16 v) { return v; }
|
||||
|
||||
// E4M3 (no infinities, max 448), round to nearest even - the inverse of the half conversion the engine also uses
|
||||
static inline __nv_fp8_storage_t __nv_cvt_float_to_fp8(float f, int, int) {
|
||||
if (f != f) return 0x7f;
|
||||
float a = f < 0 ? -f : f;
|
||||
int sign = f < 0 ? 0x80 : 0;
|
||||
if (a == 0.0f) return (__nv_fp8_storage_t) sign;
|
||||
if (a >= 448.0f) return (__nv_fp8_storage_t) (sign | 0x7e);
|
||||
int e = 0;
|
||||
while (a >= 2.0f) { a *= 0.5f; ++e; }
|
||||
while (a < 1.0f) { a *= 2.0f; --e; }
|
||||
// a in [1,2): 3 mantissa bits, bias 7
|
||||
float scaled = (a - 1.0f) * 8.0f;
|
||||
int m = (int) (scaled + 0.5f);
|
||||
int biased = e + 7;
|
||||
if (m == 8) { m = 0; ++biased; }
|
||||
if (biased <= 0) return (__nv_fp8_storage_t) sign; // subnormal -> zero here
|
||||
if (biased >= 15) return (__nv_fp8_storage_t) (sign | 0x7e); // clamp to 448
|
||||
return (__nv_fp8_storage_t) (sign | (biased << 3) | m);
|
||||
}
|
||||
static inline uint16_t __nv_cvt_fp8_to_halfraw(__nv_fp8_storage_t v, int) {
|
||||
// half (IEEE binary16) from E4M3: the engine only reads these back for the vision tower's tables
|
||||
const int sign = (v & 0x80) ? 0x8000 : 0;
|
||||
const int e = (v >> 3) & 0xf, m = v & 0x7;
|
||||
if (e == 0 && m == 0) return (uint16_t) sign;
|
||||
int he, hm;
|
||||
if (e == 0) { he = 0; hm = m << 7; }
|
||||
else { he = e - 7 + 15; hm = m << 7; }
|
||||
return (uint16_t) (sign | (he << 10) | hm);
|
||||
}
|
||||
|
||||
// the conversion modes __nv_cvt_float_to_fp8 / __nv_cvt_fp8_to_halfraw take (CUDA spells them as an enum)
|
||||
enum __nv_fp8_interpretation_t { __NV_E4M3 = 0, __NV_E5M2 = 1 };
|
||||
enum __nv_saturation_t { __NV_NOSAT = 0, __NV_SATFINITE = 1 };
|
||||
@@ -0,0 +1,165 @@
|
||||
#ifndef __grid_constant__
|
||||
#define __grid_constant__
|
||||
#endif
|
||||
#ifndef __device_builtin__
|
||||
#define __device_builtin__
|
||||
#endif
|
||||
|
||||
// cuda_device_builtins.h - the CUDA device built-ins, declared for the HOST compile only.
|
||||
//
|
||||
// The DeepSeek kernels are compiled twice in this port: as C++ here, to enumerate exactly which API the device code uses,
|
||||
// and as MSL (kernels/*.metal) where the real work happens. These declarations let the first pass run; nothing here is a
|
||||
// working kernel. Metal spells the same things thread_position_in_grid, threadgroup_barrier, simd_shuffle and so on.
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
struct dim3 {
|
||||
unsigned x, y, z;
|
||||
dim3(unsigned _x = 1, unsigned _y = 1, unsigned _z = 1) : x(_x), y(_y), z(_z) {}
|
||||
};
|
||||
struct strata_dim3 { unsigned x, y, z; };
|
||||
static strata_dim3 threadIdx{0, 0, 0}, blockIdx{0, 0, 0}, blockDim{1, 1, 1}, gridDim{1, 1, 1};
|
||||
|
||||
static inline void __syncthreads() {}
|
||||
static inline void __syncwarp(unsigned = 0xffffffffu) {}
|
||||
static inline unsigned __ballot_sync(unsigned, int) { return 0; }
|
||||
static inline int __popc(unsigned v) { return __builtin_popcount(v); }
|
||||
static inline int __ffs(unsigned v) { return __builtin_ffs((int) v); }
|
||||
|
||||
// the bit and integer intrinsics the block decoders use. Each is the exact CUDA semantics, computed on the host here; the
|
||||
// MSL kernels implement the same operations with Metal's own select/rotate/permute built-ins.
|
||||
static inline unsigned __byte_perm(unsigned x, unsigned y, unsigned s) {
|
||||
unsigned out = 0;
|
||||
const unsigned long long src = ((unsigned long long) y << 32) | x;
|
||||
for (int i = 0; i < 4; ++i) {
|
||||
const unsigned sel = (s >> (4 * i)) & 0x7u;
|
||||
out |= (unsigned) ((src >> (8 * sel)) & 0xffu) << (8 * i);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
static inline int __mul24(int a, int b) { return a * b; }
|
||||
static inline unsigned __umul24(unsigned a, unsigned b) { return a * b; }
|
||||
static inline unsigned __brev(unsigned v) { return __builtin_bitreverse32(v); }
|
||||
static inline int __clz(int v) { return v ? __builtin_clz((unsigned) v) : 32; }
|
||||
static inline int __clzll(long long v) { return v ? __builtin_clzll((unsigned long long) v) : 64; }
|
||||
static inline float __fdividef(float a, float b) { return a / b; }
|
||||
static inline float __saturatef(float a) { return a < 0.0f ? 0.0f : (a > 1.0f ? 1.0f : a); }
|
||||
static inline float __int_as_float(int v) { float f; __builtin_memcpy(&f, &v, 4); return f; }
|
||||
static inline int __float_as_int(float f) { int v; __builtin_memcpy(&v, &f, 4); return v; }
|
||||
static inline unsigned __float_as_uint(float f) { unsigned v; __builtin_memcpy(&v, &f, 4); return v; }
|
||||
static inline float __uint_as_float(unsigned v) { float f; __builtin_memcpy(&f, &v, 4); return f; }
|
||||
static inline void __threadfence() {}
|
||||
static inline void __threadfence_block() {}
|
||||
static inline float atomicAdd(float* p, float v) { const float o = *p; *p = o + v; return o; }
|
||||
static inline int atomicAdd(int* p, int v) { const int o = *p; *p = o + v; return o; }
|
||||
static inline unsigned atomicAdd(unsigned* p, unsigned v) { const unsigned o = *p; *p = o + v; return o; }
|
||||
static inline int atomicMax(int* p, int v) { const int o = *p; if (v > o) *p = v; return o; }
|
||||
static inline int atomicCAS(int* p, int c, int v) { const int o = *p; if (o == c) *p = v; return o; }
|
||||
|
||||
// the SIMD video instructions (packed 8-bit lanes) the i-quant block decoders use: on CUDA these are one instruction, on
|
||||
// Apple Silicon NEON has them directly (vsubq_u8, vpaddlq / vdotq_s32) and the MSL kernels use those. Here they are the
|
||||
// exact per-lane semantics, so the host compile reproduces the same numbers.
|
||||
static inline unsigned __vsub4(unsigned a, unsigned b) {
|
||||
unsigned out = 0;
|
||||
for (int i = 0; i < 4; ++i) {
|
||||
const int d = (int) ((a >> (8 * i)) & 0xffu) - (int) ((b >> (8 * i)) & 0xffu);
|
||||
out |= (unsigned) ((d < 0 ? 0 : d) & 0xff) << (8 * i); // unsigned saturated
|
||||
}
|
||||
return out;
|
||||
}
|
||||
static inline unsigned __vsubss4(unsigned a, unsigned b) {
|
||||
unsigned out = 0;
|
||||
for (int i = 0; i < 4; ++i) {
|
||||
int d = (int) ((a >> (8 * i)) & 0xffu) - (int) ((b >> (8 * i)) & 0xffu);
|
||||
if (d < -128) d = -128;
|
||||
if (d > 127) d = 127;
|
||||
out |= (unsigned) (d & 0xff) << (8 * i);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
static inline unsigned __vadd4(unsigned a, unsigned b) {
|
||||
unsigned out = 0;
|
||||
for (int i = 0; i < 4; ++i) {
|
||||
const int d = (int) ((a >> (8 * i)) & 0xffu) + (int) ((b >> (8 * i)) & 0xffu);
|
||||
out |= (unsigned) ((d > 255 ? 255 : d) & 0xff) << (8 * i);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
static inline unsigned __vaddss4(unsigned a, unsigned b) {
|
||||
unsigned out = 0;
|
||||
for (int i = 0; i < 4; ++i) {
|
||||
int d = (int) ((a >> (8 * i)) & 0xffu) + (int) ((b >> (8 * i)) & 0xffu);
|
||||
if (d > 127) d = 127;
|
||||
out |= (unsigned) (d & 0xff) << (8 * i);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
static inline unsigned __vcmpeq4(unsigned a, unsigned b) {
|
||||
unsigned out = 0;
|
||||
for (int i = 0; i < 4; ++i) {
|
||||
const unsigned ea = (a >> (8 * i)) & 0xffu, eb = (b >> (8 * i)) & 0xffu;
|
||||
out |= (ea == eb ? 0xffu : 0u) << (8 * i);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
static inline unsigned __vcmpgtu4(unsigned a, unsigned b) {
|
||||
unsigned out = 0;
|
||||
for (int i = 0; i < 4; ++i) {
|
||||
const unsigned ea = (a >> (8 * i)) & 0xffu, eb = (b >> (8 * i)) & 0xffu;
|
||||
out |= (ea > eb ? 0xffu : 0u) << (8 * i);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
static inline int __dp4a(unsigned a, unsigned b, int c) {
|
||||
for (int i = 0; i < 4; ++i) c += (int) (signed char) ((a >> (8 * i)) & 0xffu) * (int) (signed char) ((b >> (8 * i)) & 0xffu);
|
||||
return c;
|
||||
}
|
||||
|
||||
// the remaining scalar math helpers CUDA provides and the decoders use (round-to-nearest, plus min/max for integers - CUDA
|
||||
// brings those in from its own headers, the C++ standard library does not for every type)
|
||||
static inline float __fsub_rn(float a, float b) { return a - b; }
|
||||
static inline float __fdiv_rn(float a, float b) { return a / b; }
|
||||
static inline float __frcp_rn_(float a) { return 1.0f / a; }
|
||||
template <typename T> static inline T strata_min(T a, T b) { return a < b ? a : b; }
|
||||
template <typename T> static inline T strata_max(T a, T b) { return a > b ? a : b; }
|
||||
|
||||
// the last names the kernel sources use (a full-list compile found twelve families, 2026-10-23): the grid type above, a
|
||||
// system fence, the funnel shift, the hardware transcendentals and the read-only/volatile loads.
|
||||
static inline void __threadfence_system() {}
|
||||
static inline unsigned __funnelshift_r(unsigned lo, unsigned hi, unsigned shift) {
|
||||
const unsigned long long v = ((unsigned long long) hi << 32) | lo;
|
||||
return (unsigned) ((v >> (shift & 31)) & 0xffffffffu);
|
||||
}
|
||||
static inline unsigned __funnelshift_l(unsigned lo, unsigned hi, unsigned shift) {
|
||||
const unsigned long long v = ((unsigned long long) hi << 32) | lo;
|
||||
return (unsigned) (((v << (shift & 31)) >> 32) & 0xffffffffu);
|
||||
}
|
||||
static inline float rsqrtf(float a) { return 1.0f / __builtin_sqrtf(a); }
|
||||
static inline float __expf(float a) { return __builtin_expf(a); }
|
||||
static inline float __logf(float a) { return __builtin_logf(a); }
|
||||
static inline float __sinf(float a) { return __builtin_sinf(a); }
|
||||
static inline float __cosf(float a) { return __builtin_cosf(a); }
|
||||
static inline void sincosf(float a, float* s, float* c) { *s = __builtin_sinf(a); *c = __builtin_cosf(a); }
|
||||
static inline int __float2int_rn(float a) { return (int) __builtin_roundf(a); }
|
||||
static inline int __float2int_rz(float a) { return (int) a; }
|
||||
static inline int __float2int_rd(float a) { return (int) __builtin_floorf(a); }
|
||||
static inline int __float2int_ru(float a) { return (int) __builtin_ceilf(a); }
|
||||
template <typename T> static inline T __ldg(const T* p) { return *p; }
|
||||
template <typename T> static inline T __ldcv(const T* p) { return *p; }
|
||||
|
||||
// shuffles: one template each, so int/unsigned/float/short all bind (CUDA has overloads; a template is the same thing here)
|
||||
template <typename T> static inline T __shfl_xor_sync(unsigned, T v, int) { return v; }
|
||||
template <typename T> static inline T __shfl_down_sync(unsigned, T v, int) { return v; }
|
||||
template <typename T> static inline T __shfl_up_sync(unsigned, T v, int) { return v; }
|
||||
template <typename T> static inline T __shfl_sync(unsigned, T v, int) { return v; }
|
||||
|
||||
// the integer multiply-high and the bitwise atomics the sampler uses
|
||||
static inline unsigned __umulhi(unsigned a, unsigned b) { return (unsigned) (((unsigned long long) a * b) >> 32); }
|
||||
static inline int __mulhi(int a, int b) { return (int) (((long long) a * b) >> 32); }
|
||||
static inline unsigned atomicOr(unsigned* p, unsigned v) { const unsigned o = *p; *p = o | v; return o; }
|
||||
static inline unsigned atomicAnd(unsigned* p, unsigned v) { const unsigned o = *p; *p = o & v; return o; }
|
||||
static inline unsigned atomicXor(unsigned* p, unsigned v) { const unsigned o = *p; *p = o ^ v; return o; }
|
||||
static inline unsigned atomicExch(unsigned* p, unsigned v) { const unsigned o = *p; *p = v; return o; }
|
||||
static inline unsigned atomicInc(unsigned* p, unsigned v) { const unsigned o = *p; *p = (o >= v ? 0 : o + 1); return o; }
|
||||
static inline unsigned atomicDec(unsigned* p, unsigned v) { const unsigned o = *p; *p = (o == 0 || o > v ? v : o - 1); return o; }
|
||||
@@ -0,0 +1,99 @@
|
||||
// metal/include/metal_compat/cuda_fp16.h - the half-precision surface the dsv41 sources use, on a Mac.
|
||||
//
|
||||
// The engine touches exactly these (counted by compiling src/dsv41/*.cu against this directory, M5 Max 2026-10-23):
|
||||
// __half 16, __half2 11, __half_raw 3, __half_as_ushort 8,
|
||||
// __float2half_rn 17, __half2float 18, __half22float2 6,
|
||||
// the round-to-nearest float helpers (__fmul_rn, __fadd_rn, __fmaf_rn, __frcp_rn, __fsqrt_rn),
|
||||
// and the plain half/half2 typedefs ggml-common.h uses.
|
||||
// On Apple Silicon half is IEEE binary16 too, so the storage is a uint16_t and the conversions are the standard
|
||||
// round-to-nearest-even ones - the same bits CUDA produces.
|
||||
#pragma once
|
||||
|
||||
#include "vector_types.h"
|
||||
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
|
||||
struct __half_raw {
|
||||
uint16_t x;
|
||||
__half_raw() = default;
|
||||
constexpr __half_raw(uint16_t bits) : x(bits) {}
|
||||
};
|
||||
|
||||
struct __half {
|
||||
uint16_t x;
|
||||
__half() = default;
|
||||
__half(const __half_raw& r) : x(r.x) {}
|
||||
operator __half_raw() const { return __half_raw{x}; }
|
||||
};
|
||||
|
||||
struct __half2 {
|
||||
uint16_t x, y;
|
||||
};
|
||||
|
||||
typedef __half half;
|
||||
typedef __half2 half2;
|
||||
|
||||
static inline uint16_t strata_f32_to_f16_bits(float f) {
|
||||
uint32_t u;
|
||||
std::memcpy(&u, &f, sizeof u);
|
||||
const uint32_t sign = (u >> 16) & 0x8000u;
|
||||
const uint32_t mag = u & 0x7fffffffu;
|
||||
if (mag >= 0x7f800000u) return (uint16_t) (sign | (mag > 0x7f800000u ? 0x7e00u : 0x7c00u));
|
||||
if (mag >= 0x477ff000u) return (uint16_t) (sign | 0x7c00u);
|
||||
const uint32_t r = (mag + 0x0fffu + ((mag >> 13) & 1u)) >> 13;
|
||||
if (r >= 0x7c00u) return (uint16_t) (sign | 0x7c00u);
|
||||
return (uint16_t) (sign | r);
|
||||
}
|
||||
|
||||
static inline float strata_f16_bits_to_f32(uint16_t h) {
|
||||
const uint32_t sign = (uint32_t) (h & 0x8000u) << 16;
|
||||
const uint32_t e = (h >> 10) & 0x1fu, m = h & 0x3ffu;
|
||||
uint32_t bits;
|
||||
if (e == 0) {
|
||||
if (m == 0) bits = sign;
|
||||
else {
|
||||
const float v = (float) m * (1.0f / 16777216.0f);
|
||||
return sign ? -v : v;
|
||||
}
|
||||
} else if (e == 31) {
|
||||
bits = sign | 0x7f800000u | (m << 13);
|
||||
} else {
|
||||
bits = sign | ((e - 15 + 127) << 23) | (m << 13);
|
||||
}
|
||||
float out;
|
||||
std::memcpy(&out, &bits, sizeof out);
|
||||
return out;
|
||||
}
|
||||
|
||||
static inline __half __float2half_rn(float f) { __half h; h.x = strata_f32_to_f16_bits(f); return h; }
|
||||
static inline __half __float2half(float f) { return __float2half_rn(f); }
|
||||
static inline float __half2float(__half h) { return strata_f16_bits_to_f32(h.x); }
|
||||
static inline uint16_t __half_as_ushort(__half h) { return h.x; }
|
||||
static inline __half __ushort_as_half(uint16_t u) { __half h; h.x = u; return h; }
|
||||
static inline float2 __half22float2(__half2 h) { return make_float2(strata_f16_bits_to_f32(h.x), strata_f16_bits_to_f32(h.y)); }
|
||||
static inline __half2 __floats2half2_rn(float a, float b) {
|
||||
__half2 h;
|
||||
h.x = strata_f32_to_f16_bits(a);
|
||||
h.y = strata_f32_to_f16_bits(b);
|
||||
return h;
|
||||
}
|
||||
|
||||
// round-to-nearest float helpers: the FP ops here are IEEE and un-fused, which is what these mean
|
||||
static inline float __fmul_rn(float a, float b) { return a * b; }
|
||||
static inline float __fadd_rn(float a, float b) { return a + b; }
|
||||
static inline float __fmaf_rn(float a, float b, float c) { return a * b + c; }
|
||||
static inline float __frcp_rn(float a) { return 1.0f / a; }
|
||||
static inline float __fsqrt_rn(float a) { return __builtin_sqrtf(a); }
|
||||
|
||||
// the half2 pack/unpack and arithmetic helpers the trellis and vision paths use
|
||||
static inline __half2 __halves2half2(__half lo, __half hi) { __half2 h; h.x = lo.x; h.y = hi.x; return h; }
|
||||
static inline __half2 __half2half2(__half v) { __half2 h; h.x = v.x; h.y = v.x; return h; }
|
||||
static inline __half __low2half(__half2 h) { __half o; o.x = h.x; return o; }
|
||||
static inline __half __high2half(__half2 h) { __half o; o.x = h.y; return o; }
|
||||
static inline float __low2float(__half2 h) { return strata_f16_bits_to_f32(h.x); }
|
||||
static inline float __high2float(__half2 h) { return strata_f16_bits_to_f32(h.y); }
|
||||
static inline __half2 __hadd2(__half2 a, __half2 b) { return __floats2half2_rn(__half2float(__low2half(a)) + __half2float(__low2half(b)), __half2float(__high2half(a)) + __half2float(__high2half(b))); }
|
||||
static inline __half2 __hmul2(__half2 a, __half2 b) { return __floats2half2_rn(__half2float(__low2half(a)) * __half2float(__low2half(b)), __half2float(__high2half(a)) * __half2float(__high2half(b))); }
|
||||
static inline __half2 __hfma2(__half2 a, __half2 b, __half2 c) { return __floats2half2_rn(__half2float(__low2half(a)) * __half2float(__low2half(b)) + __half2float(__low2half(c)), __half2float(__high2half(a)) * __half2float(__high2half(b)) + __half2float(__high2half(c))); }
|
||||
static inline __half2 __hneg2(__half2 a) { __half2 h; h.x = a.x ^ 0x8000u; h.y = a.y ^ 0x8000u; return h; }
|
||||
@@ -0,0 +1,4 @@
|
||||
// metal/include/metal_compat/cuda_fp8.h - the fp8 names the engine includes; the definitions live beside the bf16 ones.
|
||||
#pragma once
|
||||
|
||||
#include "cuda_bf16.h"
|
||||
@@ -0,0 +1,31 @@
|
||||
// cuda_launch.h - the <<<grid, block, shared, stream>>> launch syntax, as a call.
|
||||
//
|
||||
// A C++ compiler cannot parse CUDA's launch syntax, and the Metal port replaces every launch with a dispatch anyway (the grid
|
||||
// becomes a threadgroup count, the block a threadgroup size). strata_ds4_launch() is that call: the kernel first, then the
|
||||
// launch shape, then the kernel's own arguments - every parameter generic, because the shapes vary (dim3, plain ints, sizes,
|
||||
// and the stream type), and the point of this header is to make the sources parse, not to type-check the port.
|
||||
//
|
||||
// Until a kernel has a Metal implementation the call stops the run naming the kernel, so the engine reports the first one it
|
||||
// needs and the port proceeds one kernel at a time.
|
||||
#pragma once
|
||||
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
|
||||
template <typename K, typename... A>
|
||||
static inline void strata_ds4_launch(K kernel, A&&...) {
|
||||
static_assert(sizeof(K) > 0, "kernel");
|
||||
std::fprintf(stderr, "strata metal: a dsv41 kernel launch has no Metal dispatch yet\n");
|
||||
std::abort();
|
||||
}
|
||||
|
||||
// the call sites sit inside namespaces whose first argument would win argument-dependent lookup: force the global one
|
||||
// the call sites carry their own location, so a run names the exact launch that needs a dispatch
|
||||
#define strata_ds4_launch(...) ::strata_ds4_launch_at(__FILE__, __LINE__, __VA_ARGS__)
|
||||
|
||||
// the macro adds the call site's location, so a run names the exact launch that still needs a Metal dispatch
|
||||
template <typename K, typename... A>
|
||||
static inline void strata_ds4_launch_at(const char* file, int line, K, A&&...) {
|
||||
std::fprintf(stderr, "strata metal: kernel launch with no Metal dispatch at %s:%d\n", file, line);
|
||||
std::abort();
|
||||
}
|
||||
@@ -1,3 +1,32 @@
|
||||
// The CUDA function-space qualifiers and the few launch/kernel spellings the shared sources use. On Metal they say
|
||||
#include "cuda_launch.h"
|
||||
// nothing: a "kernel" here is an MSL function compiled at run time and dispatched by the host runtime, so the qualifiers
|
||||
// are empty and __forceinline__ is plain inline. Part of the metal_compat layer (M5 Max, 2026-10-23).
|
||||
#ifndef __host__
|
||||
#define __host__
|
||||
#endif
|
||||
#ifndef __device__
|
||||
#define __device__
|
||||
#endif
|
||||
#ifndef __global__
|
||||
#define __global__
|
||||
#endif
|
||||
#ifndef __shared__
|
||||
#define __shared__
|
||||
#endif
|
||||
#ifndef __constant__
|
||||
#define __constant__
|
||||
#endif
|
||||
#ifndef __forceinline__
|
||||
#define __forceinline__ inline
|
||||
#endif
|
||||
#ifndef __launch_bounds__
|
||||
#define __launch_bounds__(...)
|
||||
#endif
|
||||
#ifndef __restrict__
|
||||
#define __restrict__
|
||||
#endif
|
||||
|
||||
// metal/include/metal_compat/cuda_runtime.h - the CUDA runtime names the host files use, on Metal.
|
||||
//
|
||||
// The HIP build's approach (include/strata/hip_compat/cuda_runtime.h maps every cudaX the host code calls to hipX),
|
||||
@@ -8,6 +37,27 @@
|
||||
// uses another one fails to compile on Metal, which is the point.
|
||||
#pragma once
|
||||
|
||||
// CUDA's alignment qualifier
|
||||
#ifndef __align__
|
||||
#define __align__(n) alignas(n)
|
||||
#endif
|
||||
|
||||
#include "cuda_device_builtins.h"
|
||||
|
||||
#include <cmath>
|
||||
|
||||
using std::fmaxf;
|
||||
using std::fminf;
|
||||
using std::sqrtf;
|
||||
using std::fabsf;
|
||||
using std::powf;
|
||||
using std::expf;
|
||||
using std::logf;
|
||||
|
||||
// CUDA's headers put min/max in scope for integers; the sources rely on it
|
||||
template <typename T> static inline T min(T a, T b) { return strata_min(a, b); }
|
||||
template <typename T> static inline T max(T a, T b) { return strata_max(a, b); }
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
|
||||
@@ -45,6 +95,12 @@ inline constexpr unsigned cudaEventDefault = strata::gpu::kEventDefault;
|
||||
inline constexpr unsigned cudaEventDisableTiming = strata::gpu::kEventDisableTiming;
|
||||
inline constexpr unsigned cudaEventBlockingSync = strata::gpu::kEventBlockingSync;
|
||||
inline constexpr unsigned cudaStreamNonBlocking = strata::gpu::kStreamNonBlocking;
|
||||
enum cudaStreamCaptureStatus { cudaStreamCaptureStatusNone = 0, cudaStreamCaptureStatusActive = 1, cudaStreamCaptureStatusInvalidated = 2 };
|
||||
static inline strata::gpu::Error cudaStreamIsCapturing(strata::gpu::Stream, cudaStreamCaptureStatus* st) {
|
||||
if (st) *st = cudaStreamCaptureStatusNone;
|
||||
return strata::gpu::kSuccess;
|
||||
}
|
||||
|
||||
enum cudaStreamCaptureMode { cudaStreamCaptureModeGlobal = 0, cudaStreamCaptureModeThreadLocal = 1, cudaStreamCaptureModeRelaxed = 2 };
|
||||
// (the compute capability is asked by the 0.1.45 Turing defaults in verify.cpp: no answer here, which reads as "not a Turing card")
|
||||
enum cudaDeviceAttr {
|
||||
@@ -79,6 +135,11 @@ inline cudaError_t cudaDeviceEnablePeerAccess(int p, unsigned f) { return strata
|
||||
inline cudaError_t cudaDeviceDisablePeerAccess(int) { return cudaSuccess; } // a Mac has one GPU: nothing to disable
|
||||
|
||||
// ---- memory and copies
|
||||
// the deferred command buffer mtl_rt batches the port's dispatches into: a copy (or any host read of a device result)
|
||||
// has to commit it first, or it reads memory the GPU has not written yet. Defined in ds4_finish.cpp.
|
||||
extern "C" void ds4_metal_flush();
|
||||
extern "C" void ds4_metal_kick();
|
||||
|
||||
template <class T> inline cudaError_t cudaMalloc(T** p, size_t n) { return strata::gpu::malloc_device(p, n); }
|
||||
inline cudaError_t cudaFree(void* p) { return strata::gpu::free_device(p); }
|
||||
template <class T> inline cudaError_t cudaHostAlloc(T** p, size_t n, unsigned f) { return strata::gpu::host_alloc(p, n, f); }
|
||||
@@ -141,3 +202,17 @@ inline cudaError_t cudaGraphLaunch(cudaGraphExec_t x, cudaStream_t s) { return s
|
||||
inline cudaError_t cudaGraphUpload(cudaGraphExec_t x, cudaStream_t s) { return strata::gpu::graph_upload(x, s); }
|
||||
inline cudaError_t cudaGraphDestroy(cudaGraph_t g) { return strata::gpu::graph_destroy(g); }
|
||||
inline cudaError_t cudaGraphExecDestroy(cudaGraphExec_t x) { return strata::gpu::graph_exec_destroy(x); }
|
||||
|
||||
// cudaMemcpy2D(dst, dpitch, src, spitch, width, height, kind): a strided copy, used by the vision tower's patch rows and
|
||||
// the Engram tables' column slices. The Metal runtime has no pitched copy yet, so this reports failure rather than
|
||||
// copying nothing silently.
|
||||
inline strata::gpu::Error cudaMemcpy2D(void*, size_t, const void*, size_t, size_t, size_t, strata::gpu::CopyKind) {
|
||||
return strata::gpu::kErrorUnsupported; // cudaMemcpy2D_unimplemented_marker
|
||||
}
|
||||
|
||||
// occupancy query: the Metal runtime decides its own threadgroup limits, so answer "one block" and let the caller size grids
|
||||
template <typename K, typename... A>
|
||||
static inline strata::gpu::Error cudaOccupancyMaxActiveBlocksPerMultiprocessor(int* n, K&&, int, A&&...) {
|
||||
if (n) *n = 1;
|
||||
return strata::gpu::kSuccess;
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
// metal/include/metal_compat/ds4_metal_host.h - the dsv41 kernels that have a Metal dispatch.
|
||||
//
|
||||
// GENERATED from metal/src/engine/ds4_host.cpp and ds4_finish.cpp, which are the source of truth: do not hand-edit the
|
||||
// declaration block, regenerate it (see RIGS/m1002.md).
|
||||
#pragma once
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <vector>
|
||||
|
||||
namespace strata::gpu {
|
||||
struct StreamImpl;
|
||||
using Stream = StreamImpl*;
|
||||
} // namespace strata::gpu
|
||||
|
||||
namespace strata::dsv41::metal_host {
|
||||
|
||||
// ds4_kernels.cu:1894 finish_kernel<false> - the engine's Finish, as ds4_kernels.cuh declares it
|
||||
struct FinishIn {
|
||||
const float* w;
|
||||
float eps;
|
||||
int rd, pos, inverse, vpp, pos_step;
|
||||
const float* freqs;
|
||||
int quant;
|
||||
const uint16_t* dst;
|
||||
long long dst_stride;
|
||||
uint8_t* pdst;
|
||||
long long pdst_stride;
|
||||
};
|
||||
|
||||
bool embed(const uint16_t* rows, int T, float* h, int dim, int hc, void*);
|
||||
bool engram_dequant_tok(const uint8_t* src, size_t per, int cols, int T, int hd, float* out, int fmt);
|
||||
bool ext_to_ring(const void* ext, void* ring, long long p0, int n, int T, int d, size_t row_bytes, void* backup);
|
||||
bool finish(float* x, int n_vec, int len, long long stride, const FinishIn& f);
|
||||
bool gemv_f32(const float* W, const float* x, int n, int k, int T, float* y, int ldy);
|
||||
bool hc_mix(const float* h, const float* fn, int hcdim, int n_mix, int T, float eps, float* mixes);
|
||||
bool hc_mix_scale(const float* h, int hcdim, int n_mix, int T, float eps, float* mixes);
|
||||
bool hc_post(const float* y, float* h, const float* post, const float* comb, int dim, int hc, int T);
|
||||
bool hc_post_v(const float* y, const float* res, const float* post, const float* comb, int dim, int hc, int T, float* out);
|
||||
bool hc_pre_norm(const float* h, const float* mix, const float* w, int dim, int hc, int T, float eps, float* x);
|
||||
bool hc_split(const float* mixes, const float* scale, const float* base, int hc, int iters, float hc_eps, int T, float* pre, float* post, float* comb);
|
||||
bool hc_mix_split(const float* h, const float* fn, int hcdim, int n_mix, int T, float eps, float* mixes, const float* scale, const float* base, int hc, int iters, float hc_eps, float* pre, float* post, float* comb);
|
||||
bool init_pre(float* pre, int T, void*);
|
||||
bool iq_mv(const void* jobs, int nj);
|
||||
bool moe_combine(void* acc, const void* y, const int* pos, const float* w_, int T, int k, int r0, int r1, int dim);
|
||||
bool moe_sort(const int* idx, int T, int k, const int* vs, int n_vs, int* bounds, int* row_tok, int* pos);
|
||||
bool moe_win_group_cpu(const void* plan_ptr, void* grp_ptr, void* grp_start, void* grp_ent, void* n_groups, int T, int k);
|
||||
bool mx_mv(const void* jobs, int nj, int max_n, size_t job_bytes, int fmt);
|
||||
bool q8_quant_rows(const float* x, size_t ldx, int rows, int k, int8_t* q, float* d);
|
||||
bool ring_to_ext(const void* ring, void* ext, long long p0, int n, int d, size_t row_bytes);
|
||||
bool route_publish(const float* logits, const float* bias, int n, int k, int T, float route_scale, const void* vram, int* idx, float* w, void* plan_ptr, const float* bias_vl, unsigned vl);
|
||||
bool sgemm_tn(const float* A, const float* B, float* C, int m, int nt, int k, int lda, int ldb, int ldc);
|
||||
bool rows_f16(const float* x, int ldx, const int* idx, int nrows, int ncols, uint16_t* out);
|
||||
uint64_t zc_map(const void* p, size_t bytes); // zero-copy weights: the GPU address of host memory, 0 if not mappable
|
||||
bool q8_quant_multi(const void* specs, int n); // {const float* x; uint64 ldx; uint32 rows, k; int8* q; float* d} each
|
||||
bool iq_set_gu(const uint8_t* wbase, size_t stride, const size_t* tr, const int* hbounds, int n_exp, int total_rows, int n_embd, int n_ff, float limit, const float* xf, int ldx, const int* idx, uint16_t* h, int ldh, int tg, int td);
|
||||
bool iq_set_dn(const uint8_t* wbase, size_t stride, const size_t* tr, const int* hbounds, int n_exp, int total_rows, int n_embd, int n_ff, float limit, const uint16_t* h16, int ldh, float* y, int ldy, int tg, int td);
|
||||
bool dequant_rows(int kind, const void* raw, size_t ld, int k, int n, uint16_t* out);
|
||||
bool gemm_f16_nt(const uint16_t* W16, const uint16_t* X16, int ldx, float* Y, int ldy, int rows, int N, int K, float alpha, float beta);
|
||||
bool swiglu(const float* g, const float* u, int n, float lim, float* h);
|
||||
bool unpack_sel(const void* ckv, const int* sel, int sel_stride, int gen, long long p0, int t0, int R, int kcap, const int* nsel, int cap, void* out, int T, int nsel_count, size_t row_bytes);
|
||||
|
||||
// generated from ds4_host.cpp and ds4_finish.cpp
|
||||
bool block_max(const float* score, long long stride, int T, const void* rows, int blk, float* bm, long long bm_stride, int nb_max);
|
||||
bool combine(const float* ed, const float* cpu, const float* sh, const void* c, int T, int dim, float* out);
|
||||
bool compress_win(const float* kv, const float* score, float* kv_state, float* score_state, int ratio, int p0, int T, int d, const float* w, float eps, float* latent);
|
||||
bool confidence(const float* x, const uint16_t* E, const int* tok, const float* w, int T, int dim, int R, float* out);
|
||||
bool embed(const uint16_t* rows, int T, float* h, int dim, int hc, void*);
|
||||
bool embed_addr(const uint16_t* rows, int T, float* h, int dim, int hc);
|
||||
bool engram_dequant(const uint8_t* rows, const uint8_t* scales, int n, int hd, float* out, int fmt);
|
||||
bool engram_dequant_tok(const uint8_t* src, size_t per, int cols, int T, int hd, float* out, int fmt);
|
||||
bool engram_gate(float* h, const float* kv, const float* w, int hc, int dim, int T, float eps);
|
||||
bool ext_to_ring(const void* ext, void* ring, long long p0, int n, int T, int d, size_t row_bytes, void* backup);
|
||||
bool finish(float* x, int n_vec, int len, long long stride, const FinishIn& f);
|
||||
bool gather_rows(const float* x, const void* g, int n, int cols, float* out);
|
||||
bool gemv_f16_rows(const uint16_t* W, const float* x, size_t ldx, int n, int k, int T, float alpha, float* y, size_t ldy);
|
||||
bool gemv_f32(const float* W, const float* x, int n, int k, int T, float* y, int ldy);
|
||||
bool hc_mean(const float* h, int T, int hc, int dim, float* out, long long out_stride);
|
||||
bool hc_mix(const float* h, const float* fn, int hcdim, int n_mix, int T, float eps, float* mixes);
|
||||
bool hc_mix_scale(const float* h, int hcdim, int n_mix, int T, float eps, float* mixes);
|
||||
bool hc_post(const float* y, float* h, const float* post, const float* comb, int dim, int hc, int T);
|
||||
bool hc_post_v(const float* y, const float* res, const float* post, const float* comb, int dim, int hc, int T, float* out);
|
||||
bool hc_pre_norm(const float* h, const float* mix, const float* w, int dim, int hc, int T, float eps, float* x);
|
||||
bool hc_split(const float* mixes, const float* scale, const float* base, int hc, int iters, float hc_eps, int T, float* pre, float* post, float* comb);
|
||||
bool hc_mix_split(const float* h, const float* fn, int hcdim, int n_mix, int T, float eps, float* mixes, const float* scale, const float* base, int hc, int iters, float hc_eps, float* pre, float* post, float* comb);
|
||||
bool index_score(const float* q, const float* wq, const void* kc, int heads, int hd, int T, int n, const int* cand, int cand_stride, int blk, const void* rows, float* out, long long out_stride, int kfmt);
|
||||
bool init_pre(float* pre, int T, void*);
|
||||
bool iota_rows(int* out, int T, int n, int stride);
|
||||
bool iq_dn(const unsigned long long* plan_ptr, const unsigned long long* grp_ptr, const int* grp_start, const int* grp_ent, const int* n_groups, const size_t* tr, int n_embd, int n_ff, const int8_t* hq, const float* hd, float* part, int type, int groups);
|
||||
bool iq_gu(const unsigned long long* plan_ptr, const unsigned long long* grp_ptr, const int* grp_start, const int* grp_ent, const int* n_groups, const size_t* tr, int n_embd, int n_ff, float limit, const int8_t* xq, const float* xd, float* h, int type, int groups);
|
||||
bool iq_mv(const void* jobs, int nj);
|
||||
bool markov_add(const float* logits, const uint16_t* E, const uint16_t* H, const int* tok, int V, int R);
|
||||
bool moe_combine(void* acc, const void* y, const int* pos, const float* w_, int T, int k, int r0, int r1, int dim);
|
||||
bool moe_sort(const int* idx, int T, int k, const int* vs, int n_vs, int* bounds, int* row_tok, int* pos);
|
||||
bool moe_win_group_cpu(const void* plan_ptr, void* grp_ptr, void* grp_start, void* grp_ent, void* n_groups, int T, int k);
|
||||
bool mx_combine(const void* plan_ptr, const void* plan_w, const void* part, const void* cpu_part, size_t cpu_stride, const int* cpu_flag, const void* sh_out, void* out, size_t out_stride, int T, int k, int n_embd);
|
||||
bool mx_dn(const unsigned long long* plan_ptr, const unsigned long long* grp_ptr, const int* grp_start, const int* grp_ent, const int* n_groups, const size_t* tr, int n_embd, int n_ff, const int8_t* hq, const float* hd, float* part, int groups);
|
||||
bool mx_gu(const unsigned long long* plan_ptr, const unsigned long long* grp_ptr, const int* grp_start, const int* grp_ent, const int* n_groups, const size_t* tr, int n_embd, int n_ff, float limit, const int8_t* xq, const float* xd, float* h, int groups);
|
||||
bool mx_mv(const void* jobs, int nj, int max_n, size_t job_bytes, int fmt);
|
||||
bool q8_quant_rows(const float* x, size_t ldx, int rows, int k, int8_t* q, float* d);
|
||||
bool ring_to_ext(const void* ring, void* ext, long long p0, int n, int d, size_t row_bytes);
|
||||
bool route(const float* logits, const float* bias, int n, int k, int T, float route_scale, int* idx, float* w, const float* bias_vl, const void* vl);
|
||||
bool route_publish(const float* logits, const float* bias, int n, int k, int T, float route_scale, const void* vram, int* idx, float* w, void* plan_ptr, const float* bias_vl, unsigned vl);
|
||||
bool sample_greedy(const float* logits, int n_tokens, int n_vocab, const int* history, int history_len, int penalty_last_n, float penalty_repeat, float penalty_freq, float penalty_present, const float* logit_bias, int* out);
|
||||
bool sgemm_tn(const float* A, const float* B, float* C, int m, int nt, int k, int lda, int ldb, int ldc);
|
||||
bool sparse_attn(const float* q, const uint16_t* win, const void* ckv, const int* sel, int sel_stride, const void* rows, const float* sink, int heads, int d, int T, float scale, float* out, int ckv_fmt);
|
||||
bool swiglu(const float* g, const float* u, int n, float lim, float* h);
|
||||
bool topk(const float* vals, long long stride, int T, const void* rows, const int* cand, int cand_stride, int blk, int* out, int out_stride);
|
||||
bool unpack_sel(const void* ckv, const int* sel, int sel_stride, int gen, long long p0, int t0, int R, int kcap, const int* nsel, int cap, void* out, int T, int nsel_count, size_t row_bytes);
|
||||
|
||||
} // namespace strata::dsv41::metal_host
|
||||
@@ -0,0 +1,38 @@
|
||||
// mma.h - the wmma surface on Metal: the names parse, and using one stops the run.
|
||||
//
|
||||
// The DeepSeek token path does not use tensor cores. The prompt path has an MMA variant written for CUDA/HIP warp matrices;
|
||||
// Metal's equivalent is the simdgroup matrix API, which is a different shape, so that variant is a later piece of the port.
|
||||
// These aliases exist so the shared sources compile (which is how the rest of the API surface is enumerated), and every
|
||||
// operation aborts with a message naming this file - an accidental call cannot return plausible garbage.
|
||||
#pragma once
|
||||
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
|
||||
namespace nvcuda {
|
||||
namespace wmma {
|
||||
|
||||
// CUDA 12 spells these as tags: fragment<accumulator, 16, 16, 16, float>
|
||||
struct matrix_a {};
|
||||
struct matrix_b {};
|
||||
struct accumulator {};
|
||||
struct row_major {};
|
||||
struct col_major {};
|
||||
enum layout_t { mem_row_major, mem_col_major };
|
||||
|
||||
template <typename Use, int M, int N, int K, typename T, typename Layout = void> struct fragment {
|
||||
T x[M * N];
|
||||
};
|
||||
|
||||
[[noreturn]] inline void strata_metal_wmma_missing(const char* what) {
|
||||
std::fprintf(stderr, "strata metal: wmma::%s has no Metal implementation (simdgroup matrices are the equivalent)\n", what);
|
||||
std::abort();
|
||||
}
|
||||
|
||||
template <typename... A> inline void fill_fragment(A&&...) { strata_metal_wmma_missing("fill_fragment"); }
|
||||
template <typename... A> inline void load_matrix_sync(A&&...) { strata_metal_wmma_missing("load_matrix_sync"); }
|
||||
template <typename... A> inline void mma_sync(A&&...) { strata_metal_wmma_missing("mma_sync"); } // variadic: the engine passes four fragments of different types
|
||||
template <typename... A> inline void store_matrix_sync(A&&...) { strata_metal_wmma_missing("store_matrix_sync"); }
|
||||
|
||||
} // namespace wmma
|
||||
} // namespace nvcuda
|
||||
@@ -0,0 +1,25 @@
|
||||
// metal/include/metal_compat/vector_types.h - the CUDA vector types the dsv41 sources name, on Metal.
|
||||
//
|
||||
// The compat directory had none, so the first half-precision header to use float2 failed. Only the shapes the engine
|
||||
// touches are here (float2/float4/int2/uint2 and their make_ helpers); the Metal runtime's own float4 is not involved.
|
||||
#pragma once
|
||||
|
||||
struct float2 { float x, y; };
|
||||
struct float4 { float x, y, z, w; };
|
||||
struct int2 { int x, y; };
|
||||
struct uint2 { unsigned x, y; };
|
||||
|
||||
static inline float2 make_float2(float x, float y) { return float2{x, y}; }
|
||||
static inline float4 make_float4(float x, float y, float z, float w) { return float4{x, y, z, w}; }
|
||||
static inline int2 make_int2(int x, int y) { return int2{x, y}; }
|
||||
static inline uint2 make_uint2(unsigned x, unsigned y) { return uint2{x, y}; }
|
||||
|
||||
struct int4 { int x, y, z, w; };
|
||||
struct uint4 { unsigned x, y, z, w; };
|
||||
struct char4 { signed char x, y, z, w; };
|
||||
struct uchar4 { unsigned char x, y, z, w; };
|
||||
struct short2 { short x, y; };
|
||||
struct int3 { int x, y, z; };
|
||||
|
||||
static inline int4 make_int4(int x, int y, int z, int w) { return int4{x, y, z, w}; }
|
||||
static inline uint4 make_uint4(unsigned x, unsigned y, unsigned z, unsigned w) { return uint4{x, y, z, w}; }
|
||||
@@ -38,6 +38,7 @@ struct DeviceProp { // the fields the host code reads
|
||||
char gcnArchName[256] = {};
|
||||
int major = 0, minor = 0, warpSize = 32, multiProcessorCount = 0;
|
||||
size_t totalGlobalMem = 0;
|
||||
int integrated = 0; // the DeepSeek engine reads it (a GPU on the PC's RAM: Strix Halo); 0 here: its rules for that part were not tuned for Apple GPUs
|
||||
};
|
||||
|
||||
inline constexpr unsigned kHostAllocDefault = 0;
|
||||
|
||||
@@ -635,12 +635,13 @@ template <> struct W8<T_IQ2_XXS> {
|
||||
device const uchar* bl = row + (k >> 8) * 66;
|
||||
const int ib = (k & 255) >> 5, l = (k & 31) >> 3, hh = (k & 7) >> 2;
|
||||
const uint a0 = rd32(bl + 2 + 8 * ib), a1 = rd32(bl + 2 + 8 * ib + 4);
|
||||
threadgroup const uchar* ks = (threadgroup const uchar*)(tab + 512);
|
||||
const float sc = rdh(bl) * (0.5f + float(a1 >> 28)) * 0.25f;
|
||||
const uint i0 = (a0 >> (8 * l)) & 255, i1 = (a0 >> (8 * (l + 2))) & 255;
|
||||
const uint s0 = ks[(a1 >> (7 * l)) & 127], s1 = ks[(a1 >> (7 * (l + 2))) & 127];
|
||||
a = w4_signed(tab[2 * i0 + hh], hh ? s0 >> 4 : s0, sc);
|
||||
b = w4_signed(tab[2 * i1 + hh], hh ? s1 >> 4 : s1, sc);
|
||||
// through the accessors, so QUANT_CTAB (the constant grids, no threadgroup copy) gives the same values: the
|
||||
// direct tab[] reads below skipped the ctab branch and read an unfilled table
|
||||
const uint s0 = ksg(tab, 512, (a1 >> (7 * l)) & 127), s1 = ksg(tab, 512, (a1 >> (7 * (l + 2))) & 127);
|
||||
a = w4_signed(g_iq2xxs(tab, i0, hh), hh ? s0 >> 4 : s0, sc);
|
||||
b = w4_signed(g_iq2xxs(tab, i1, hh), hh ? s1 >> 4 : s1, sc);
|
||||
}
|
||||
};
|
||||
template <> struct W8<T_IQ2_XS> {
|
||||
@@ -649,14 +650,14 @@ template <> struct W8<T_IQ2_XS> {
|
||||
const int ib = (k & 255) >> 5, l = (k & 31) >> 3, hh = (k & 7) >> 2;
|
||||
device const ushort* q = (device const ushort*)(bl + 2) + 4 * ib;
|
||||
const uint v0 = q[l], v1 = q[l + 2];
|
||||
threadgroup const uchar* ks = (threadgroup const uchar*)(tab + 1024);
|
||||
const uint scb = bl[66 + ib];
|
||||
const float d = rdh(bl);
|
||||
const float sc0 = d * (0.5f + float((scb >> (4 * (l >> 1))) & 15)) * 0.25f;
|
||||
const float sc1 = d * (0.5f + float((scb >> (4 * ((l + 2) >> 1))) & 15)) * 0.25f;
|
||||
const uint s0 = ks[v0 >> 9], s1 = ks[v1 >> 9];
|
||||
a = w4_signed(tab[2 * (v0 & 511) + hh], hh ? s0 >> 4 : s0, sc0);
|
||||
b = w4_signed(tab[2 * (v1 & 511) + hh], hh ? s1 >> 4 : s1, sc1);
|
||||
// through the accessors: the direct tab[]/ks[] reads ignored QUANT_CTAB, whose tables are then never filled
|
||||
const uint s0 = ksg(tab, 1024, v0 >> 9), s1 = ksg(tab, 1024, v1 >> 9);
|
||||
a = w4_signed(g_iq2xs(tab, v0 & 511, hh), hh ? s0 >> 4 : s0, sc0);
|
||||
b = w4_signed(g_iq2xs(tab, v1 & 511, hh), hh ? s1 >> 4 : s1, sc1);
|
||||
}
|
||||
};
|
||||
template <> struct W8<T_IQ2_S> {
|
||||
@@ -675,6 +676,42 @@ template <> struct W8<T_IQ2_S> {
|
||||
b = w4_signed(g_iq2s(tab, i1, hh), hh ? s1 >> 4 : s1, sc1);
|
||||
}
|
||||
};
|
||||
|
||||
// IQ1_M (the 2.00 bpw pack's 1.75-bit type), ggml's dequantize_row_iq1_m: the block's F16 scale sits in the top
|
||||
// nibbles of its four u16 scales; a 32-value sub-block has two 3-bit scales (groups 0-1 and 2-3, eight values each),
|
||||
// and each eight-value group carries a +-0.125 delta picked by a bit of its qh byte. The tile's two halves are the
|
||||
// four values at k and the four at k + 16, each decoded on its own (k + 16 crosses into the next sub-block at l >= 2).
|
||||
template <typename P>
|
||||
static inline half4 iq1m_w4(P row, int k) {
|
||||
const P bl = row + (k >> 8) * 56;
|
||||
const int ib = (k & 255) >> 5, g = (k & 31) >> 3, hh = (k & 7) >> 2;
|
||||
const uint s0 = uint(bl[48]) | (uint(bl[49]) << 8), s1 = uint(bl[50]) | (uint(bl[51]) << 8);
|
||||
const uint s2 = uint(bl[52]) | (uint(bl[53]) << 8), s3 = uint(bl[54]) | (uint(bl[55]) << 8);
|
||||
const float d = float(as_type<half>(ushort((s0 >> 12) | ((s1 >> 8) & 0x00f0u) | ((s2 >> 4) & 0x0f00u) | (s3 & 0xf000u))));
|
||||
const uint sc = uint(((const device ushort*) (bl + 48))[ib >> 1]);
|
||||
const float dl = d * float(2u * ((sc >> (6u * uint(ib & 1) + (g < 2 ? 0u : 3u))) & 7u) + 1u);
|
||||
const uint qh = (g < 2) ? uint(bl[32 + 2 * ib]) : uint(bl[33 + 2 * ib]);
|
||||
const uint idx = uint(bl[4 * ib + g]) | (((g & 1) != 0) ? ((qh << 4u) & 0x700u) : ((qh << 8u) & 0x700u));
|
||||
const bool neg = ((g & 1) != 0) ? ((qh & 0x80u) != 0u) : ((qh & 0x08u) != 0u);
|
||||
const ulong gr = iq1s_grid[idx];
|
||||
const float delta = neg ? -0.125f : 0.125f;
|
||||
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_M> {
|
||||
template <typename P>
|
||||
static inline void run(P row, int k, threadgroup const uint* tab, thread half4& a, thread half4& b) {
|
||||
(void) tab;
|
||||
a = iq1m_w4(row, k);
|
||||
b = iq1m_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) {
|
||||
|
||||
@@ -25,6 +25,7 @@
|
||||
#define T_IQ3_XXS 18
|
||||
#define T_IQ4_NL 20
|
||||
#define T_IQ3_S 21
|
||||
#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
|
||||
#define T_Q2_0 42
|
||||
@@ -39,6 +40,7 @@ template <> struct fmt<T_Q2_0> { enum { QK = 64, BB = 18, UNIT = 16, TAB =
|
||||
template <> struct fmt<T_Q2_0R> { enum { QK = 64, BB = 18, UNIT = 16, TAB = 1 }; };
|
||||
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_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 }; };
|
||||
@@ -389,6 +391,8 @@ template <> inline void load_tab<T_IQ2_XS>(threadgroup uint* tab, ushort tid, us
|
||||
for (int i = tid; i < 512; i += nth) { tab[2 * i] = uint(iq2xs_grid[i]); tab[2 * i + 1] = uint(iq2xs_grid[i] >> 32); }
|
||||
load_signs(tab, 1024, tid, nth);
|
||||
}
|
||||
// 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_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); }
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
// metal/src/engine/ds4_ctx.cpp - the one-Context allocator the dsv41 engine can opt into.
|
||||
//
|
||||
// Why: a Metal kernel must be handed the gpuAddress of a buffer that the SAME mtl::Context encodes the dispatch for (its
|
||||
// residency set). strata::gpu, which metal_compat's cudaMalloc maps to, is a CUDA-shaped facade with its own device state, so a
|
||||
// dsv41 launch would read a foreign residency set. This file gives metal_compat an alternative: allocate inside one Context and
|
||||
// remember gpuAddress -> Buffer, so wrap()/Arg::B can adopt the memory without a copy.
|
||||
//
|
||||
// Off by default: the engine's default path stays exactly as it was. Set STRATA_DS4_METAL_CTX=1 to route allocations here -
|
||||
// which becomes testable the moment a pack exists (the engine allocates only after it has read ds4.txt).
|
||||
#include "mtl_rt.h"
|
||||
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <mutex>
|
||||
#include <unordered_map>
|
||||
|
||||
namespace strata::dsv41::metal_ctx {
|
||||
|
||||
namespace {
|
||||
struct Entry {
|
||||
mtl::Buffer buf;
|
||||
size_t bytes = 0;
|
||||
};
|
||||
|
||||
std::mutex g_mu;
|
||||
std::unordered_map<void*, Entry>* g_map = nullptr; // gpuAddress -> buffer
|
||||
mtl::Context* g_cx = nullptr;
|
||||
|
||||
bool enabled() {
|
||||
static const bool on = std::getenv("STRATA_DS4_METAL_CTX") != nullptr;
|
||||
return on;
|
||||
}
|
||||
|
||||
mtl::Context& cx() {
|
||||
if (!g_cx) g_cx = new mtl::Context();
|
||||
return *g_cx;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
bool active() { return enabled(); }
|
||||
|
||||
void* alloc(size_t bytes) {
|
||||
if (!enabled()) return nullptr;
|
||||
const size_t page = 16384;
|
||||
const size_t rounded = (bytes + page - 1) / page * page;
|
||||
mtl::Buffer b = cx().alloc(rounded);
|
||||
if (!b.handle || !b.ptr) {
|
||||
std::fprintf(stderr, "dsv41 metal: Context::alloc(%zu) failed\n", bytes);
|
||||
return nullptr;
|
||||
}
|
||||
std::lock_guard<std::mutex> lk(g_mu);
|
||||
if (!g_map) g_map = new std::unordered_map<void*, Entry>();
|
||||
(*g_map)[b.ptr] = Entry{b, rounded};
|
||||
return b.ptr; // the CPU address doubles as the handle's key; the device pointer is b.handle's gpuAddress
|
||||
}
|
||||
|
||||
void* host_view_of(void* p) {
|
||||
std::lock_guard<std::mutex> lk(g_mu);
|
||||
if (!g_map) return nullptr;
|
||||
auto it = g_map->find(p);
|
||||
return it == g_map->end() ? nullptr : it->second.buf.ptr;
|
||||
}
|
||||
|
||||
const mtl::Buffer* buffer_of(void* p) {
|
||||
std::lock_guard<std::mutex> lk(g_mu);
|
||||
if (!g_map) return nullptr;
|
||||
auto it = g_map->find(p);
|
||||
return it == g_map->end() ? nullptr : &it->second.buf;
|
||||
}
|
||||
|
||||
void release(void* p) {
|
||||
std::lock_guard<std::mutex> lk(g_mu);
|
||||
if (!g_map) return;
|
||||
auto it = g_map->find(p);
|
||||
if (it == g_map->end()) return;
|
||||
cx().free(it->second.buf);
|
||||
g_map->erase(it);
|
||||
}
|
||||
|
||||
} // namespace strata::dsv41::metal_ctx
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,254 @@
|
||||
// metal/src/engine/ds4_host.cpp - host side of the ported dsv41 kernels: compile the MSL once, wrap the engine's own buffers,
|
||||
// dispatch. First entry point: the Q8 activation quantiser (ds4_gguf.cu:201).
|
||||
//
|
||||
// The two runtimes meet on pointers: strata::gpu owns the engine's device memory (a device pointer is its MTLBuffer's
|
||||
// gpuAddress) and exposes host_view(device_pointer); mtl::Context::wrap() adopts that address without copying and a
|
||||
// Dispatch names it through Arg::B(). See RIGS/m1002.md for the derivation.
|
||||
#include "mtl_rt.h"
|
||||
|
||||
#include <strata/gpu/runtime.hpp>
|
||||
|
||||
|
||||
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <string>
|
||||
|
||||
#include <strata/gpu/runtime.hpp>
|
||||
#include <vector>
|
||||
|
||||
namespace strata::dsv41::metal_host {
|
||||
mtl::Context& finish_cx(); // ds4_finish.cpp's Context: every kernel that orders with the chunk path's dispatches uses it
|
||||
int shared_lib(); // from ds4_finish.cpp: the library the shared Context compiled
|
||||
|
||||
|
||||
namespace {
|
||||
mtl::Context* g_cx = nullptr;
|
||||
int g_lib = -1;
|
||||
bool g_tried = false;
|
||||
|
||||
bool ensure(mtl::Context& cx) {
|
||||
if (g_cx == &cx && g_lib >= 0) return true;
|
||||
if (g_tried && g_lib < 0) return false;
|
||||
g_tried = true;
|
||||
// the kernel lives in its own file and is self-contained (metal_stdlib + ggml_grids.h)
|
||||
g_lib = cx.compile({"../src/engine/msl/ds4_quant.metal"}, "", false);
|
||||
if (g_lib < 0) {
|
||||
std::fprintf(stderr, "dsv41 metal: compiling ds4_quant.metal failed\n");
|
||||
return false;
|
||||
}
|
||||
g_cx = &cx;
|
||||
return true;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
// q8(x, ldx, n) -> q (int8, n bytes) and d (half, n/32); ldx is the stride in values between blocks of 32
|
||||
bool q8_quant(mtl::Context& cx, const float* x, int ldx, int n, void* q, void* d) {
|
||||
if (!ensure(cx)) return false;
|
||||
const size_t xb = (size_t) ldx * (size_t) (n / 32) * sizeof(float), qb = (size_t) n, db = (size_t) (n / 32) * 2;
|
||||
mtl::Buffer bx = cx.wrap(strata::gpu::host_view(x), xb);
|
||||
mtl::Buffer bq = cx.wrap(q, qb);
|
||||
mtl::Buffer bd = cx.wrap(d, db);
|
||||
mtl::Pipeline p;
|
||||
try {
|
||||
p = cx.pipeline(g_lib, "ds4_q8_quant");
|
||||
} catch (...) {
|
||||
return false;
|
||||
}
|
||||
mtl::Dispatch ds;
|
||||
ds.p = &p;
|
||||
ds.args = {mtl::Arg::B(bx), mtl::Arg::V((unsigned) ldx), mtl::Arg::B(bq), mtl::Arg::B(bd)};
|
||||
ds.grid[0] = (uint32_t) (n / 32);
|
||||
ds.grid[1] = ds.grid[2] = 1;
|
||||
ds.tg[0] = 32;
|
||||
ds.tg[1] = ds.tg[2] = 1;
|
||||
mtl::require_fit(p, 32, "ds4_q8_quant");
|
||||
cx.run({ds});
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace strata::dsv41::metal_host
|
||||
|
||||
namespace strata::dsv41::metal_host {
|
||||
namespace {
|
||||
mtl::Context* g_init_cx = nullptr;
|
||||
int g_lib_init = -1;
|
||||
|
||||
mtl::Context& shared_cx() {
|
||||
if (!g_init_cx) {
|
||||
g_init_cx = new mtl::Context();
|
||||
g_lib_init = g_init_cx->compile({"../src/engine/msl/ds4_init.metal"}, "", false);
|
||||
if (g_lib_init < 0) std::fprintf(stderr, "dsv41 metal: compiling ds4_init.metal failed\n");
|
||||
}
|
||||
return *g_init_cx;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
// ds4_kernels.cu:1477 - pre[i] = (i % 8) == 0 ? 1 : 0 for i < T*8
|
||||
bool init_pre(float* pre, int T, void*) {
|
||||
// cpu path (T*8 floats of a fixed pattern): the GPU route had to wrap a whole page to reach 32 bytes, and that aliasing
|
||||
// buffer overlapped the pages holding h
|
||||
void* host = strata::gpu::host_view(pre);
|
||||
if (!host) { std::fprintf(stderr, "dsv41 metal: init_pre has no host view\n"); return false; }
|
||||
float* p = (float*) host;
|
||||
for (int t = 0; t < T; ++t)
|
||||
for (int i = 0; i < 8; ++i) p[t * 8 + i] = (i == 0) ? 1.0f : 0.0f;
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
// ds4_kernels.cu:130 - the BF16 embedding gather, hc copies a token
|
||||
bool embed(const uint16_t* rows, int T, float* h, int dim, int hc, void*) {
|
||||
strata::dsv41::metal_host::shared_cx();
|
||||
if (g_lib_init < 0) { std::fprintf(stderr, "dsv41 metal: embed: no library\n"); return false; }
|
||||
// host_view() returns a different address from the device pointer on this runtime, so wrapping it wrote into a mapping the
|
||||
// engine never reads. On Apple silicon a device pointer is a valid CPU address for shared storage: wrap that instead.
|
||||
// DS4_HOSTVIEW=1 restores the old behaviour.
|
||||
const bool use_hv = std::getenv("DS4_HOSTVIEW") != nullptr;
|
||||
void* hrows = use_hv ? strata::gpu::host_view((void*) rows) : (void*) rows;
|
||||
void* hh = use_hv ? strata::gpu::host_view(h) : (void*) h;
|
||||
if (!hrows || !hh) { std::fprintf(stderr, "dsv41 metal: embed has no host view\n"); return false; }
|
||||
if (std::getenv("DS4_DBG_HV"))
|
||||
std::fprintf(stderr, "DBG hostview: rows %p -> %p h %p -> %p\n",
|
||||
(void*) rows, hrows, (void*) h, hh);
|
||||
const size_t page = 16384;
|
||||
auto wrap = [&](void* host, size_t bytes) {
|
||||
return strata::dsv41::metal_host::shared_cx().wrap((void*) (((uintptr_t) host) & ~(uintptr_t) (page - 1)), ((bytes + page - 1) / page) * page);
|
||||
};
|
||||
mtl::Buffer br = wrap(hrows, (size_t) T * dim * 2);
|
||||
mtl::Buffer bh = wrap(hh, (size_t) T * hc * dim * 4);
|
||||
if (!br.handle || !bh.handle) { std::fprintf(stderr, "dsv41 metal: embed wrap failed\n"); return false; }
|
||||
const size_t dro = (size_t) (((uintptr_t) hrows) - (((uintptr_t) hrows) & ~(uintptr_t) (page - 1)));
|
||||
const size_t dho = (size_t) (((uintptr_t) hh) - (((uintptr_t) hh) & ~(uintptr_t) (page - 1)));
|
||||
mtl::Pipeline pl;
|
||||
try { pl = strata::dsv41::metal_host::shared_cx().pipeline(g_lib_init, "ds4_embed"); } catch (...) { return false; }
|
||||
mtl::Dispatch d;
|
||||
d.p = &pl;
|
||||
d.args = {mtl::Arg::B(br, dro), mtl::Arg::B(bh, dho), mtl::Arg::V((unsigned) dim), mtl::Arg::V((unsigned) hc)};
|
||||
d.grid[0] = (uint32_t) ((dim + 255) / 256);
|
||||
d.grid[1] = (uint32_t) T;
|
||||
d.grid[2] = 1;
|
||||
d.tg[0] = 256; d.tg[1] = d.tg[2] = 1;
|
||||
strata::dsv41::metal_host::shared_cx().run({d});
|
||||
if (std::getenv("DS4_DBG_EMBWRITE")) {
|
||||
strata::dsv41::metal_host::shared_cx().flush_runs();
|
||||
const float* f0 = (const float*) hh;
|
||||
const uint16_t* r16 = (const uint16_t*) hrows;
|
||||
std::fprintf(stderr, "DBG embwrite: h=%p rows=%p h_off=%zu rows_off=%zu\n", hh, hrows, dho, dro);
|
||||
std::fprintf(stderr, "DBG embwrite: rows[0..3]=%04x %04x %04x %04x h[0..3]=%g %g %g %g\n",
|
||||
(unsigned) r16[0], (unsigned) r16[1], (unsigned) r16[2], (unsigned) r16[3],
|
||||
f0[0], f0[1], f0[2], f0[3]);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
// ds4_model.cu:2737 - C[t][m] = sum_k A[m][k] * B[t][k], F32 operands, alpha = 1, beta = 0
|
||||
bool sgemm_tn(const float* A, const float* B, float* C, int m, int nt, int k, int lda, int ldb, int ldc) {
|
||||
finish_cx();
|
||||
const int lib = shared_lib();
|
||||
if (lib < 0) return false;
|
||||
void* ha = strata::gpu::host_view((void*) A);
|
||||
void* hb = strata::gpu::host_view((void*) B);
|
||||
void* hc = strata::gpu::host_view((void*) C);
|
||||
if (!ha || !hb || !hc) { std::fprintf(stderr, "dsv41 metal: sgemm_tn has no host view\n"); return false; }
|
||||
const size_t page = 16384;
|
||||
auto wrap = [&](void* host, size_t bytes) {
|
||||
return finish_cx().wrap((void*) (((uintptr_t) host) & ~(uintptr_t) (page - 1)), ((bytes + page - 1) / page) * page);
|
||||
};
|
||||
auto off = [&](void* host) { return (size_t) ((uintptr_t) host - (((uintptr_t) host) & ~(uintptr_t) (page - 1))); };
|
||||
mtl::Buffer ba = wrap(ha, (size_t) m * lda * 4);
|
||||
mtl::Buffer bb = wrap(hb, (size_t) nt * ldb * 4);
|
||||
mtl::Buffer bc = wrap(hc, (size_t) nt * ldc * 4);
|
||||
if (!ba.handle || !bb.handle || !bc.handle) return false;
|
||||
mtl::Pipeline pl;
|
||||
try { pl = finish_cx().pipeline(lib, "ds4_sgemm_tn"); } catch (...) { return false; }
|
||||
mtl::Dispatch d;
|
||||
d.p = &pl;
|
||||
d.args = {mtl::Arg::B(ba, off(ha)), mtl::Arg::B(bb, off(hb)), mtl::Arg::B(bc, off(hc)),
|
||||
mtl::Arg::V((unsigned) m), mtl::Arg::V((unsigned) nt), mtl::Arg::V((unsigned) k),
|
||||
mtl::Arg::V((unsigned) lda), mtl::Arg::V((unsigned) ldb), mtl::Arg::V((unsigned) ldc)};
|
||||
const uint64_t total = (uint64_t) m * (uint64_t) nt;
|
||||
d.grid[0] = (uint32_t) ((total + 127) / 128);
|
||||
d.grid[1] = d.grid[2] = 1;
|
||||
d.tg[0] = 128; d.tg[1] = d.tg[2] = 1;
|
||||
finish_cx().run({d});
|
||||
if (std::getenv("DS4_DBG_SG")) {
|
||||
finish_cx().flush_runs();
|
||||
const float* fa = (const float*) ha; const float* fb = (const float*) hb; const float* fc = (const float*) hc;
|
||||
std::fprintf(stderr, "sgemm m %d nt %d k %d lda %d ldb %d ldc %d A %g %g B %g %g C %g %g %g\n", m, nt, k, lda, ldb, ldc, fa[0], fa[1], fb[0], fb[1], fc[0], fc[1], fc[2]);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
// embed through raw device addresses: the struct carries the gpuAddress of rows and h, which the runtime's residency set keeps
|
||||
// resident, so the kernel writes the engine's own memory instead of a CPU mirror.
|
||||
struct Ds4EmbedArgsHost {
|
||||
uint64_t rows;
|
||||
uint64_t h;
|
||||
uint32_t dim;
|
||||
uint32_t hc;
|
||||
};
|
||||
|
||||
bool embed_addr(const uint16_t* rows, int T, float* h, int dim, int hc) {
|
||||
// the engine uploads emb_rows on ITS stream; this kernel runs on mtl::Context queue. Order the two before reading.
|
||||
strata::gpu::stream_synchronize(nullptr);
|
||||
if (std::getenv("DS4_DBG_ROWS")) { // what is the engine actually handing us as the embedding rows?
|
||||
const uint16_t* rv = (const uint16_t*) strata::gpu::host_view((void*) rows);
|
||||
std::fprintf(stderr, "DBG rows_in: ptr=%p view=%p first=%04x %04x %04x %04x\n",
|
||||
(const void*) rows, (const void*) rv,
|
||||
rv ? (unsigned) rv[0] : 0u, rv ? (unsigned) rv[1] : 0u,
|
||||
rv ? (unsigned) rv[2] : 0u, rv ? (unsigned) rv[3] : 0u);
|
||||
}
|
||||
strata::dsv41::metal_host::shared_cx();
|
||||
const int lib = shared_lib();
|
||||
if (lib < 0) return false;
|
||||
Ds4EmbedArgsHost a{};
|
||||
a.rows = (uint64_t) (uintptr_t) rows;
|
||||
a.h = (uint64_t) (uintptr_t) h;
|
||||
a.dim = (uint32_t) dim;
|
||||
a.hc = (uint32_t) hc;
|
||||
mtl::Pipeline pl;
|
||||
try { pl = strata::dsv41::metal_host::shared_cx().pipeline(lib, "ds4_embed_j"); } catch (...) { return false; }
|
||||
mtl::Dispatch d;
|
||||
d.p = &pl;
|
||||
d.args = {mtl::Arg::V(a)};
|
||||
d.grid[0] = (uint32_t) ((dim + 255) / 256);
|
||||
d.grid[1] = (uint32_t) T;
|
||||
d.grid[2] = 1;
|
||||
d.tg[0] = 256; d.tg[1] = d.tg[2] = 1;
|
||||
strata::dsv41::metal_host::shared_cx().run({d});
|
||||
strata::dsv41::metal_host::shared_cx().flush_runs();
|
||||
if (std::getenv("DS4_DBG_CMP")) {
|
||||
const float* hv = (const float*) strata::gpu::host_view(h);
|
||||
{
|
||||
const size_t nv = (size_t) T * hc * dim;
|
||||
size_t nf = 0, nz = 0;
|
||||
for (size_t i = 0; i < nv; ++i) { const float v = hv[i]; if (!(v == v)) ++nf; else if (v == 0.0f) ++nz; }
|
||||
std::fprintf(stderr, "DBG cmp: after embed n=%.0f nonfinite=%zu zeros=%zu\n", (double) nv, nf, nz);
|
||||
}
|
||||
std::fprintf(stderr, "DBG cmp: view %g %g %g | h=%g h+1=%g\n",
|
||||
hv ? hv[0] : -1.0f, hv ? hv[1] : -1.0f, hv ? hv[2] : -1.0f,
|
||||
hv ? hv[(size_t) dim * hc] : -1.0f, hv ? hv[(size_t) dim * hc + 1] : -1.0f);
|
||||
}
|
||||
if (std::getenv("DS4_DBG_ARGS")) {
|
||||
mtl::Buffer bp = strata::dsv41::metal_host::shared_cx().wrap(
|
||||
(void*) (((uintptr_t) strata::gpu::host_view(h)) & ~(uintptr_t) 16383), 16384);
|
||||
mtl::Pipeline pp = strata::dsv41::metal_host::shared_cx().pipeline(lib, "ds4_args_probe");
|
||||
mtl::Dispatch pd;
|
||||
pd.p = &pp;
|
||||
pd.args = {mtl::Arg::B(bp, ((uintptr_t) strata::gpu::host_view(h)) & 16383),
|
||||
mtl::Arg::V((unsigned) dim), mtl::Arg::V((unsigned) hc)};
|
||||
pd.grid[0] = 1; pd.grid[1] = pd.grid[2] = 1; pd.tg[0] = 32; pd.tg[1] = pd.tg[2] = 1;
|
||||
strata::dsv41::metal_host::shared_cx().run({pd});
|
||||
strata::dsv41::metal_host::shared_cx().flush_runs();
|
||||
float got[3] = {0, 0, 0};
|
||||
{ const float* hv2 = (const float*) strata::gpu::host_view(h); if (hv2) { got[0] = hv2[0]; got[1] = hv2[1]; } }
|
||||
std::fprintf(stderr, "DBG args: kernel sees dim=%g hc=%g\n", got[0], got[1]);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace strata::dsv41::metal_host
|
||||
@@ -0,0 +1,29 @@
|
||||
// metal/src/engine/exl3_legacy_stub.cpp - v1.1.8's EXL3 decode kernels, absent on Metal.
|
||||
//
|
||||
// exl3_legacy.cu exists for NVIDIA GPUs before sm_80, which pick it over the newer tiles; nothing on a Mac can select it
|
||||
// (use_legacy() is false without CUDA), and the 2.25bpw text model does not use EXL3 at all. Leaving the file out of the
|
||||
// target would break the link for these seven entry points, so they are defined here and stop the run naming themselves -
|
||||
// the same contract as the stub generator, and a loud failure rather than a silent wrong decode.
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
|
||||
#include <strata/dsv41/exl3.hpp> // MvJob, MoeArgs, WinArgs
|
||||
|
||||
namespace strata::dsv41k::exl3::legacy {
|
||||
|
||||
[[noreturn]] static void missing(const char* what) {
|
||||
std::fprintf(stderr,
|
||||
"strata metal: exl3::legacy::%s has no Metal implementation (v1.1.8's EXL3 kernels; the 2.25bpw text model "
|
||||
"does not use EXL3)\n", what);
|
||||
std::abort();
|
||||
}
|
||||
|
||||
int launch_errors() { return 0; }
|
||||
bool prepare(cudaStream_t) { missing("prepare"); }
|
||||
bool several_ok() { return false; }
|
||||
bool mv(const MvJob*, int, cudaStream_t) { missing("mv"); }
|
||||
void moe_gate_up(const MoeArgs&, const float*, float*, cudaStream_t) { missing("moe_gate_up"); }
|
||||
void moe_down(const MoeArgs&, const float*, const float*, float*, cudaStream_t) { missing("moe_down"); }
|
||||
bool win_experts(const WinArgs&, cudaStream_t) { missing("win_experts"); }
|
||||
|
||||
} // namespace strata::dsv41k::exl3::legacy
|
||||
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,138 @@
|
||||
// metal/src/engine/msl/ds4_mm.metal - the DeepSeek prompt chunk's expert GEMMs: moe_mm.metal's mm_gu3 / mm_down3 (matmul2d on dequantized
|
||||
// weight tiles) over the engine's own expert slots and its UNPADDED sort order. Compiled after quant/units/mma/moe_mm.metal.
|
||||
// tiles[i] = (slot, first row, valid rows): a tile is TM rows of one expert; rows past `valid` (the next expert's) are computed and
|
||||
// dropped (the operand rows exist: the scratch carries TM rows of slack), the swiglu is DeepSeek's clamped one.
|
||||
|
||||
struct DsMmArgs {
|
||||
ulong gate_off;
|
||||
ulong up_off;
|
||||
ulong down_off;
|
||||
ulong stride; // bytes between slots
|
||||
uint gu_row; // bytes per gate/up row (n_embd values)
|
||||
uint d_row; // bytes per down row (n_ff values)
|
||||
uint n_embd;
|
||||
uint n_ff;
|
||||
float limit;
|
||||
uint pad0;
|
||||
};
|
||||
|
||||
// xs[r][c] = half(x[idx[r]][c]) for r < rows
|
||||
kernel void ds4_mm_gather(device const float* x [[buffer(0)]], constant ulong& ldx [[buffer(1)]], device const int* idx [[buffer(2)]],
|
||||
constant uint& n_embd [[buffer(3)]], device half* xs [[buffer(4)]],
|
||||
uint2 tgp [[threadgroup_position_in_grid]], uint tid [[thread_index_in_threadgroup]]) {
|
||||
const uint c = tgp.x * 256u + tid;
|
||||
if (c >= n_embd) return;
|
||||
xs[(ulong) tgp.y * n_embd + c] = half(x[(ulong) idx[tgp.y] * ldx + c]);
|
||||
}
|
||||
|
||||
template <int F, int TM, int TN, int TK, int SG>
|
||||
kernel void ds4_mm_gu(device const uchar* W [[buffer(0)]], device const half* XS [[buffer(1)]], device half* HS [[buffer(2)]],
|
||||
device const int4* tiles [[buffer(3)]], constant DsMmArgs& a [[buffer(4)]], constant uint& n_tiles [[buffer(5)]],
|
||||
uint3 tg [[threadgroup_position_in_grid]], ushort sg [[simdgroup_index_in_threadgroup]],
|
||||
ushort lane [[thread_index_in_simdgroup]], ushort tid [[thread_index_in_threadgroup]]) {
|
||||
if (tg.y >= n_tiles) return;
|
||||
threadgroup float ot[SG][TM * TN];
|
||||
threadgroup uint tab[fmt<F>::TAB];
|
||||
load_tab<F>(tab, tid, SG * 32);
|
||||
threadgroup_barrier(mem_flags::mem_threadgroup);
|
||||
const int4 tl = tiles[tg.y];
|
||||
const int e = tl.x, row0 = tl.y, valid = tl.z, n0 = (int(tg.x) * SG + sg) * TN;
|
||||
device const uchar* E = W + ulong(e) * a.stride;
|
||||
device const uchar* g0 = E + a.gate_off + ulong(n0) * a.gu_row;
|
||||
device const uchar* u0 = E + a.up_off + ulong(n0) * a.gu_row;
|
||||
constexpr auto desc = matmul2d_descriptor(TM, TN, TK, false, true, false, matmul2d_descriptor::mode::multiply_accumulate);
|
||||
matmul2d<desc, execution_simdgroup> op;
|
||||
tensor<device half, dextents<int32_t, 2>, tensor_inline> tX((device half*)XS, dextents<int32_t, 2>(a.n_embd, 1 << 30));
|
||||
auto rg = op.template get_right_input_cooperative_tensor<half, half, float>();
|
||||
auto ru = op.template get_right_input_cooperative_tensor<half, half, float>();
|
||||
auto tA = tX.template slice<TK, TM>(0, row0);
|
||||
auto accg = op.template get_destination_cooperative_tensor<__tensor_ops_detail::__remove_addrspace_t<decltype(tA)>, __tensor_ops_detail::__remove_addrspace_t<decltype(rg)>, float>();
|
||||
auto accu = op.template get_destination_cooperative_tensor<__tensor_ops_detail::__remove_addrspace_t<decltype(tA)>, __tensor_ops_detail::__remove_addrspace_t<decltype(rg)>, float>();
|
||||
for (uint16_t i = 0; i < accg.get_capacity(); ++i)
|
||||
if (accg.is_valid_element(i)) { accg[i] = 0.0f; accu[i] = 0.0f; }
|
||||
for (int k0 = 0; k0 < int(a.n_embd); k0 += TK) {
|
||||
const uint16_t cap = rg.get_capacity(), half_cap = cap / 2;
|
||||
#pragma clang loop unroll(full)
|
||||
for (uint16_t i = 0; i < half_cap; i += 4) {
|
||||
const auto ix = rg.get_multidimensional_index(i);
|
||||
const int k = k0 + ix[0], n = ix[1];
|
||||
device const uchar* rgw = g0 + ulong(n) * a.gu_row;
|
||||
device const uchar* ruw = u0 + ulong(n) * a.gu_row;
|
||||
half4 wg, wu, wg2, wu2;
|
||||
W8<F>::run(rgw, k, tab, wg, wg2);
|
||||
W8<F>::run(ruw, k, tab, wu, wu2);
|
||||
rg[i] = wg.x; rg[i + 1] = wg.y; rg[i + 2] = wg.z; rg[i + 3] = wg.w;
|
||||
ru[i] = wu.x; ru[i + 1] = wu.y; ru[i + 2] = wu.z; ru[i + 3] = wu.w;
|
||||
const uint16_t j = i + half_cap;
|
||||
rg[j] = wg2.x; rg[j + 1] = wg2.y; rg[j + 2] = wg2.z; rg[j + 3] = wg2.w;
|
||||
ru[j] = wu2.x; ru[j + 1] = wu2.y; ru[j + 2] = wu2.z; ru[j + 3] = wu2.w;
|
||||
}
|
||||
tA = tX.template slice<TK, TM>(k0, row0);
|
||||
op.run(tA, rg, accg);
|
||||
op.run(tA, ru, accu);
|
||||
}
|
||||
for (uint16_t i = 0; i < accg.get_capacity(); ++i)
|
||||
if (accg.is_valid_element(i)) {
|
||||
const float g = fmin(accg[i], a.limit), u = fmin(fmax(accu[i], -a.limit), a.limit);
|
||||
accg[i] = g / (1.0f + exp(-g)) * u;
|
||||
}
|
||||
tensor<threadgroup float, dextents<int32_t, 2>, tensor_inline> tO(ot[sg], dextents<int32_t, 2>(TN, TM));
|
||||
accg.store(tO);
|
||||
simdgroup_barrier(mem_flags::mem_threadgroup);
|
||||
for (int it = lane; it < TM * TN; it += 32) {
|
||||
const int m = it / TN, f = it - m * TN;
|
||||
if (m < valid) HS[ulong(row0 + m) * a.n_ff + n0 + f] = half(ot[sg][it]);
|
||||
}
|
||||
}
|
||||
|
||||
template <int F, int TM, int TN, int TK, int SG>
|
||||
kernel void ds4_mm_dn(device const uchar* W [[buffer(0)]], device half* YEH [[buffer(1)]], device const half* HS [[buffer(2)]],
|
||||
device const int4* tiles [[buffer(3)]], constant DsMmArgs& a [[buffer(4)]], constant uint& n_tiles [[buffer(5)]],
|
||||
uint3 tg [[threadgroup_position_in_grid]], ushort sg [[simdgroup_index_in_threadgroup]],
|
||||
ushort lane [[thread_index_in_simdgroup]], ushort tid [[thread_index_in_threadgroup]]) {
|
||||
if (tg.y >= n_tiles || a.pad0 == 3u) return;
|
||||
threadgroup float ot[SG][TM * TN];
|
||||
threadgroup uint tab[fmt<F>::TAB];
|
||||
load_tab<F>(tab, tid, SG * 32);
|
||||
threadgroup_barrier(mem_flags::mem_threadgroup);
|
||||
const int4 tl = tiles[tg.y];
|
||||
const int e = tl.x, row0 = tl.y, valid = tl.z, n0 = (int(tg.x) * SG + sg) * TN;
|
||||
device const uchar* w0 = W + ulong(e) * a.stride + a.down_off + ulong(n0) * a.d_row;
|
||||
constexpr auto desc = matmul2d_descriptor(TM, TN, TK, false, true, false, matmul2d_descriptor::mode::multiply_accumulate);
|
||||
matmul2d<desc, execution_simdgroup> op;
|
||||
tensor<device half, dextents<int32_t, 2>, tensor_inline> tH((device half*)HS, dextents<int32_t, 2>(a.n_ff, 1 << 30));
|
||||
auto rw = op.template get_right_input_cooperative_tensor<half, half, float>();
|
||||
auto tA = tH.template slice<TK, TM>(0, row0);
|
||||
auto acc = op.template get_destination_cooperative_tensor<__tensor_ops_detail::__remove_addrspace_t<decltype(tA)>, __tensor_ops_detail::__remove_addrspace_t<decltype(rw)>, float>();
|
||||
for (uint16_t i = 0; i < acc.get_capacity(); ++i)
|
||||
if (acc.is_valid_element(i)) acc[i] = 0.0f;
|
||||
for (int k0 = 0; k0 < int(a.n_ff); k0 += TK) {
|
||||
const uint16_t half_cap = rw.get_capacity() / 2;
|
||||
#pragma clang loop unroll(full)
|
||||
for (uint16_t i = 0; i < half_cap; i += 4) {
|
||||
const auto ix = rw.get_multidimensional_index(i);
|
||||
half4 w, w2;
|
||||
if (a.pad0 == 1u) { w = half4(0); w2 = half4(0); } else if (a.pad0 == 2u) { w = half4(1); w2 = half4(1); W8<F>::run(w0, 0, tab, w, w2); w = half4(0); } else
|
||||
W8<F>::run(w0 + ulong(ix[1]) * a.d_row, k0 + ix[0], tab, w, w2);
|
||||
rw[i] = w.x; rw[i + 1] = w.y; rw[i + 2] = w.z; rw[i + 3] = w.w;
|
||||
const uint16_t j = i + half_cap;
|
||||
rw[j] = w2.x; rw[j + 1] = w2.y; rw[j + 2] = w2.z; rw[j + 3] = w2.w;
|
||||
}
|
||||
tA = tH.template slice<TK, TM>(k0, row0);
|
||||
op.run(tA, rw, acc);
|
||||
}
|
||||
tensor<threadgroup float, dextents<int32_t, 2>, tensor_inline> tO(ot[sg], dextents<int32_t, 2>(TN, TM));
|
||||
acc.store(tO);
|
||||
simdgroup_barrier(mem_flags::mem_threadgroup);
|
||||
for (int it = lane; it < TM * TN; it += 32) {
|
||||
const int m = it / TN, f = it - m * TN;
|
||||
if (m < valid) ((device float*) YEH)[ulong(row0 + m) * a.n_embd + n0 + f] = ot[sg][it];
|
||||
}
|
||||
}
|
||||
|
||||
#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_IQ2_XXS, 16)
|
||||
DS4_MM_INST(T_IQ2_XS, 17)
|
||||
DS4_MM_INST(T_IQ2_S, 22)
|
||||
DS4_MM_INST(T_IQ3_XXS, 18)
|
||||
@@ -0,0 +1,32 @@
|
||||
// metal/src/engine/msl/ds4_quant.metal - the DeepSeek engine's activation quantiser, the first dsv41 kernel ported.
|
||||
//
|
||||
// CUDA's q8 path (ds4_gguf.cu:201, q8_quant_kernel): per 32 values d = max|x| / 127 as an f16, then q = round(x / d) clamped to
|
||||
// [-128, 127] as int8. Verified bit-exact against the same arithmetic on the CPU: 4096 blocks, 131,072 values, 0 differences,
|
||||
// worst scale error 4.6e-04 (half rounding) - metal/tests/dsv41/q8_quant_parity.mm.
|
||||
//
|
||||
// The block comes from the flat thread id (thread_position_in_grid / 32), not threadgroup_position_in_grid: the latter arrived
|
||||
// as 0 in every group on this Mac's driver and made every block recompute block 0.
|
||||
#include <metal_stdlib>
|
||||
|
||||
using namespace metal;
|
||||
|
||||
// x: n values; ldx: the stride in values between consecutive blocks of 32 (32 for a contiguous row)
|
||||
kernel void ds4_q8_quant(device const float* x [[buffer(0)]], constant uint& ldx [[buffer(1)]],
|
||||
device char* q [[buffer(2)]], device half* d [[buffer(3)]],
|
||||
uint gid [[thread_position_in_grid]], uint lane [[thread_index_in_threadgroup]]) {
|
||||
const uint b = gid / 32u;
|
||||
const int base = int(b) * int(ldx);
|
||||
const float v = x[base + int(lane)];
|
||||
float amax = fabs(v);
|
||||
amax = max(amax, simd_shuffle_xor(amax, 16));
|
||||
amax = max(amax, simd_shuffle_xor(amax, 8));
|
||||
amax = max(amax, simd_shuffle_xor(amax, 4));
|
||||
amax = max(amax, simd_shuffle_xor(amax, 2));
|
||||
amax = max(amax, simd_shuffle_xor(amax, 1));
|
||||
const float scale = amax / 127.0f;
|
||||
const float inv = scale > 0.0f ? 1.0f / scale : 0.0f;
|
||||
int iq = int(rint(v * inv));
|
||||
iq = clamp(iq, -128, 127);
|
||||
q[int(b) * 32 + int(lane)] = char(iq);
|
||||
if (lane == 0) d[int(b)] = half(scale);
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
// metal/src/engine/msl/ggml_grids.h - ggml's i-quant grid tables, for the MSL expert kernels.
|
||||
//
|
||||
// Generated from third_party/ggml/ggml-common.h (pinned 3cf03257f): the CUDA and Metal kernels must use bit-identical tables,
|
||||
// so they are copied from the same file rather than retyped. MSL cannot include a C header, so each array becomes a
|
||||
// `constant` in the address space the kernels need - same values, same order.
|
||||
#pragma once
|
||||
|
||||
|
||||
constant uchar kmask_iq2xs[8] = {1, 2, 4, 8, 16, 32, 64, 128};
|
||||
|
||||
constant uchar ksigns_iq2xs[128] = {0, 129, 130, 3, 132, 5, 6, 135, 136, 9, 10, 139, 12, 141, 142, 15, 144, 17, 18, 147, 20, 149, 150, 23, 24, 153, 154, 27, 156, 29, 30, 159, 160, 33, 34, 163, 36, 165, 166, 39, 40, 169, 170, 43, 172, 45, 46, 175, 48, 177, 178, 51, 180, 53, 54, 183, 184, 57, 58, 187, 60, 189, 190, 63, 192, 65, 66, 195, 68, 197, 198, 71, 72, 201, 202, 75, 204, 77, 78, 207, 80, 209, 210, 83, 212, 85, 86, 215, 216, 89, 90, 219, 92, 221, 222, 95, 96, 225, 226, 99, 228, 101, 102, 231, 232, 105, 106, 235, 108, 237, 238, 111, 240, 113, 114, 243, 116, 245, 246, 119, 120, 249, 250, 123, 252, 125, 126, 255,};
|
||||
|
||||
constant ulong iq2xxs_grid[256] = {0x0808080808080808, 0x080808080808082b, 0x0808080808081919, 0x0808080808082b08, 0x0808080808082b2b, 0x0808080808190819, 0x0808080808191908, 0x08080808082b0808, 0x08080808082b082b, 0x08080808082b2b08, 0x08080808082b2b2b, 0x0808080819080819, 0x0808080819081908, 0x0808080819190808, 0x0808080819192b08, 0x08080808192b0819, 0x08080808192b1908, 0x080808082b080808, 0x080808082b08082b, 0x080808082b082b2b, 0x080808082b2b082b, 0x0808081908080819, 0x0808081908081908, 0x0808081908190808, 0x0808081908191919, 0x0808081919080808, 0x080808192b081908, 0x080808192b192b08, 0x0808082b08080808, 0x0808082b0808082b, 0x0808082b082b082b, 0x0808082b2b08082b, 0x0808190808080819, 0x0808190808081908, 0x0808190808190808, 0x08081908082b0819, 0x08081908082b1908, 0x0808190819080808, 0x080819081908082b, 0x0808190819082b08, 0x08081908192b0808, 0x080819082b080819, 0x080819082b081908, 0x080819082b190808, 0x080819082b2b1908, 0x0808191908080808, 0x080819190808082b, 0x0808191908082b08, 0x08081919082b0808, 0x080819191908192b, 0x08081919192b2b19, 0x080819192b080808, 0x080819192b190819, 0x0808192b08082b19, 0x0808192b08190808, 0x0808192b19080808, 0x0808192b2b081908, 0x0808192b2b2b1908, 0x08082b0808080808, 0x08082b0808081919, 0x08082b0808082b08, 0x08082b0808191908, 0x08082b08082b2b08, 0x08082b0819080819, 0x08082b0819081908, 0x08082b0819190808, 0x08082b081919082b, 0x08082b082b082b08, 0x08082b1908081908, 0x08082b1919080808, 0x08082b2b0808082b, 0x08082b2b08191908, 0x0819080808080819, 0x0819080808081908, 0x0819080808190808, 0x08190808082b0819, 0x0819080819080808, 0x08190808192b0808, 0x081908082b081908, 0x081908082b190808, 0x081908082b191919, 0x0819081908080808, 0x0819081908082b08, 0x08190819082b0808, 0x0819081919190808, 0x0819081919192b2b, 0x081908192b080808, 0x0819082b082b1908, 0x0819082b19081919, 0x0819190808080808, 0x0819190808082b08, 0x08191908082b0808, 0x08191908082b1919, 0x0819190819082b19, 0x081919082b080808, 0x0819191908192b08, 0x08191919192b082b, 0x0819192b08080808, 0x0819192b0819192b, 0x08192b0808080819, 0x08192b0808081908, 0x08192b0808190808, 0x08192b0819080808, 0x08192b082b080819, 0x08192b1908080808, 0x08192b1908081919, 0x08192b192b2b0808, 0x08192b2b19190819, 0x082b080808080808, 0x082b08080808082b, 0x082b080808082b2b, 0x082b080819081908, 0x082b0808192b0819, 0x082b08082b080808, 0x082b08082b08082b, 0x082b0819082b2b19, 0x082b081919082b08, 0x082b082b08080808, 0x082b082b0808082b, 0x082b190808080819, 0x082b190808081908, 0x082b190808190808, 0x082b190819080808, 0x082b19081919192b, 0x082b191908080808, 0x082b191919080819, 0x082b1919192b1908, 0x082b192b2b190808, 0x082b2b0808082b08, 0x082b2b08082b0808, 0x082b2b082b191908, 0x082b2b2b19081908, 0x1908080808080819, 0x1908080808081908, 0x1908080808190808, 0x1908080808192b08, 0x19080808082b0819, 0x19080808082b1908, 0x1908080819080808, 0x1908080819082b08, 0x190808081919192b, 0x19080808192b0808, 0x190808082b080819, 0x190808082b081908, 0x190808082b190808, 0x1908081908080808, 0x19080819082b0808, 0x19080819192b0819, 0x190808192b080808, 0x190808192b081919, 0x1908082b08080819, 0x1908082b08190808, 0x1908082b19082b08, 0x1908082b1919192b, 0x1908082b192b2b08, 0x1908190808080808, 0x1908190808082b08, 0x19081908082b0808, 0x190819082b080808, 0x190819082b192b19, 0x190819190819082b, 0x19081919082b1908, 0x1908192b08080808, 0x19082b0808080819, 0x19082b0808081908, 0x19082b0808190808, 0x19082b0819080808, 0x19082b0819081919, 0x19082b1908080808, 0x19082b1919192b08, 0x19082b19192b0819, 0x19082b192b08082b, 0x19082b2b19081919, 0x19082b2b2b190808, 0x1919080808080808, 0x1919080808082b08, 0x1919080808190819, 0x1919080808192b19, 0x19190808082b0808, 0x191908082b080808, 0x191908082b082b08, 0x1919081908081908, 0x191908191908082b, 0x191908192b2b1908, 0x1919082b2b190819, 0x191919082b190808, 0x191919082b19082b, 0x1919191908082b2b, 0x1919192b08080819, 0x1919192b19191908, 0x19192b0808080808, 0x19192b0808190819, 0x19192b0808192b19, 0x19192b08192b1908, 0x19192b1919080808, 0x19192b2b08082b08, 0x192b080808081908, 0x192b080808190808, 0x192b080819080808, 0x192b0808192b2b08, 0x192b081908080808, 0x192b081919191919, 0x192b082b08192b08, 0x192b082b192b0808, 0x192b190808080808, 0x192b190808081919, 0x192b191908190808, 0x192b19190819082b, 0x192b19192b081908, 0x192b2b081908082b, 0x2b08080808080808, 0x2b0808080808082b, 0x2b08080808082b2b, 0x2b08080819080819, 0x2b0808082b08082b, 0x2b08081908081908, 0x2b08081908192b08, 0x2b08081919080808, 0x2b08082b08190819, 0x2b08190808080819, 0x2b08190808081908, 0x2b08190808190808, 0x2b08190808191919, 0x2b08190819080808, 0x2b081908192b0808, 0x2b08191908080808, 0x2b0819191908192b, 0x2b0819192b191908, 0x2b08192b08082b19, 0x2b08192b19080808, 0x2b08192b192b0808, 0x2b082b080808082b, 0x2b082b1908081908, 0x2b082b2b08190819, 0x2b19080808081908, 0x2b19080808190808, 0x2b190808082b1908, 0x2b19080819080808, 0x2b1908082b2b0819, 0x2b1908190819192b, 0x2b1908192b080808, 0x2b19082b19081919, 0x2b19190808080808, 0x2b191908082b082b, 0x2b19190819081908, 0x2b19191919190819, 0x2b192b082b080819, 0x2b192b19082b0808, 0x2bLine truncated
|
||||
|
||||
constant ulong iq2xs_grid[512] = {0x0808080808080808, 0x080808080808082b, 0x0808080808081919, 0x0808080808082b08, 0x0808080808082b2b, 0x0808080808190819, 0x0808080808191908, 0x080808080819192b, 0x0808080808192b19, 0x08080808082b0808, 0x08080808082b082b, 0x08080808082b1919, 0x08080808082b2b08, 0x0808080819080819, 0x0808080819081908, 0x080808081908192b, 0x0808080819082b19, 0x0808080819190808, 0x080808081919082b, 0x0808080819191919, 0x0808080819192b08, 0x08080808192b0819, 0x08080808192b1908, 0x080808082b080808, 0x080808082b08082b, 0x080808082b081919, 0x080808082b082b08, 0x080808082b190819, 0x080808082b191908, 0x080808082b192b19, 0x080808082b2b0808, 0x0808081908080819, 0x0808081908081908, 0x080808190808192b, 0x0808081908082b19, 0x0808081908190808, 0x080808190819082b, 0x0808081908191919, 0x0808081908192b08, 0x0808081908192b2b, 0x08080819082b0819, 0x08080819082b1908, 0x0808081919080808, 0x080808191908082b, 0x0808081919081919, 0x0808081919082b08, 0x0808081919190819, 0x0808081919191908, 0x08080819192b0808, 0x08080819192b2b08, 0x080808192b080819, 0x080808192b081908, 0x080808192b190808, 0x0808082b08080808, 0x0808082b0808082b, 0x0808082b08081919, 0x0808082b08082b08, 0x0808082b08190819, 0x0808082b08191908, 0x0808082b082b0808, 0x0808082b19080819, 0x0808082b19081908, 0x0808082b19190808, 0x0808082b19191919, 0x0808082b2b080808, 0x0808082b2b082b2b, 0x0808190808080819, 0x0808190808081908, 0x080819080808192b, 0x0808190808082b19, 0x0808190808190808, 0x080819080819082b, 0x0808190808191919, 0x0808190808192b08, 0x08081908082b0819, 0x08081908082b1908, 0x0808190819080808, 0x080819081908082b, 0x0808190819081919, 0x0808190819082b08, 0x0808190819190819, 0x0808190819191908, 0x080819081919192b, 0x08081908192b0808, 0x080819082b080819, 0x080819082b081908, 0x080819082b190808, 0x0808191908080808, 0x080819190808082b, 0x0808191908081919, 0x0808191908082b08, 0x0808191908190819, 0x0808191908191908, 0x08081919082b0808, 0x0808191919080819, 0x0808191919081908, 0x0808191919190808, 0x08081919192b0819, 0x080819192b080808, 0x0808192b08080819, 0x0808192b08081908, 0x0808192b08190808, 0x0808192b082b192b, 0x0808192b19080808, 0x0808192b1908082b, 0x0808192b2b081908, 0x08082b0808080808, 0x08082b080808082b, 0x08082b0808081919, 0x08082b0808082b08, 0x08082b0808082b2b, 0x08082b0808190819, 0x08082b0808191908, 0x08082b08082b0808, 0x08082b08082b1919, 0x08082b0819080819, 0x08082b0819081908, 0x08082b0819190808, 0x08082b0819192b08, 0x08082b082b080808, 0x08082b082b2b0808, 0x08082b082b2b2b2b, 0x08082b1908080819, 0x08082b1908081908, 0x08082b1908190808, 0x08082b1919080808, 0x08082b192b080819, 0x08082b192b082b19, 0x08082b2b08080808, 0x08082b2b082b0808, 0x08082b2b082b2b08, 0x08082b2b2b19192b, 0x08082b2b2b2b0808, 0x0819080808080819, 0x0819080808081908, 0x081908080808192b, 0x0819080808082b19, 0x0819080808190808, 0x081908080819082b, 0x0819080808191919, 0x0819080808192b08, 0x08190808082b0819, 0x08190808082b1908, 0x0819080819080808, 0x081908081908082b, 0x0819080819081919, 0x0819080819082b08, 0x0819080819190819, 0x0819080819191908, 0x08190808192b0808, 0x08190808192b2b2b, 0x081908082b080819, 0x081908082b081908, 0x081908082b190808, 0x0819081908080808, 0x081908190808082b, 0x0819081908081919, 0x0819081908082b08, 0x0819081908190819, 0x0819081908191908, 0x08190819082b0808, 0x0819081919080819, 0x0819081919081908, 0x0819081919190808, 0x081908192b080808, 0x081908192b191908, 0x081908192b19192b, 0x0819082b08080819, 0x0819082b08081908, 0x0819082b0808192b, 0x0819082b08190808, 0x0819082b19080808, 0x0819082b192b0808, 0x0819190808080808, 0x081919080808082b, 0x0819190808081919, 0x0819190808082b08, 0x0819190808190819, 0x0819190808191908, 0x08191908082b0808, 0x0819190819080819, 0x0819190819081908, 0x0819190819082b19, 0x0819190819190808, 0x08191908192b1908, 0x081919082b080808, 0x0819191908080819, 0x0819191908081908, 0x0819191908190808, 0x0819191919080808, 0x0819192b08080808, 0x0819192b08191908, 0x0819192b19082b19, 0x08192b0808080819, 0x08192b0808081908, 0x08192b0808190808, 0x08192b080819082b, 0x08192b0819080808, 0x08192b0819191908, 0x08192b082b08192b, 0x08192b1908080808, 0x08192b1908081919, 0x08192b19192b192b, 0x08192b2b19190819, 0x08192b2b2b2b2b19, 0x082b080808080808, 0x082b08080808082b, 0x082b080808081919, 0x082b080808082b08, 0x082b080808082b2b, 0x082b080808190819, 0x082b080808191908, 0x082b0808082b0808, 0x082b080819080819, 0x082b080819081908, 0x082b080819190808, 0x082b08082b080808, 0x082b08082b2b0808, 0x082b081908080819, 0x082b081908081908, 0x082b081908190808, 0x082b081919080808, 0x082b081919082b08, 0x082b0819192b1919, 0x082b082b08080808, 0x082b082b082b082b, 0x082b082b2b080808, 0x082b082b2b2b2b08, 0x082b190808080819, 0x082b190808081908, 0x082b190808190808, 0x082b1908082b2b19, 0x082b190819080808, 0x082b191908080808, 0x082b191919080819, 0x082b19191919082b, 0x082b19192b192b19, 0x082b192b08080819, 0x082b192b08192b2b, 0x082b192b2b2b192b, 0x082b2b0808080808, 0x082b2b0808082b08, 0x082b2b0808082b2b, 0x082b2b08082b0808, 0x082b2b0819191919, 0x082b2b082b082b08, 0x082b2b082b2b082b, 0x082b2b19192b2b08, 0x082Line truncated
|
||||
|
||||
constant ulong iq2s_grid[1024] = {0x0808080808080808, 0x080808080808082b, 0x0808080808081919, 0x0808080808082b08, 0x0808080808082b2b, 0x0808080808190819, 0x0808080808191908, 0x080808080819192b, 0x0808080808192b19, 0x08080808082b0808, 0x08080808082b082b, 0x08080808082b1919, 0x08080808082b2b08, 0x0808080819080819, 0x0808080819081908, 0x080808081908192b, 0x0808080819082b19, 0x0808080819190808, 0x080808081919082b, 0x0808080819191919, 0x0808080819192b08, 0x08080808192b0819, 0x08080808192b1908, 0x08080808192b192b, 0x08080808192b2b19, 0x080808082b080808, 0x080808082b08082b, 0x080808082b081919, 0x080808082b082b08, 0x080808082b190819, 0x080808082b191908, 0x080808082b2b0808, 0x080808082b2b1919, 0x080808082b2b2b2b, 0x0808081908080819, 0x0808081908081908, 0x080808190808192b, 0x0808081908082b19, 0x0808081908190808, 0x080808190819082b, 0x0808081908191919, 0x0808081908192b08, 0x08080819082b0819, 0x08080819082b1908, 0x0808081919080808, 0x080808191908082b, 0x0808081919081919, 0x0808081919082b08, 0x0808081919190819, 0x0808081919191908, 0x080808191919192b, 0x0808081919192b19, 0x08080819192b0808, 0x08080819192b1919, 0x08080819192b2b08, 0x080808192b080819, 0x080808192b081908, 0x080808192b190808, 0x080808192b19082b, 0x080808192b191919, 0x080808192b2b0819, 0x080808192b2b1908, 0x0808082b08080808, 0x0808082b0808082b, 0x0808082b08081919, 0x0808082b08082b08, 0x0808082b08190819, 0x0808082b08191908, 0x0808082b082b0808, 0x0808082b082b2b2b, 0x0808082b19080819, 0x0808082b19081908, 0x0808082b1908192b, 0x0808082b19082b19, 0x0808082b19190808, 0x0808082b19191919, 0x0808082b2b080808, 0x0808082b2b081919, 0x0808082b2b082b2b, 0x0808082b2b191908, 0x0808082b2b2b082b, 0x0808190808080819, 0x0808190808081908, 0x080819080808192b, 0x0808190808082b19, 0x0808190808190808, 0x080819080819082b, 0x0808190808191919, 0x0808190808192b08, 0x08081908082b0819, 0x08081908082b1908, 0x08081908082b192b, 0x08081908082b2b19, 0x0808190819080808, 0x080819081908082b, 0x0808190819081919, 0x0808190819082b08, 0x0808190819082b2b, 0x0808190819190819, 0x0808190819191908, 0x080819081919192b, 0x0808190819192b19, 0x08081908192b0808, 0x08081908192b082b, 0x08081908192b1919, 0x080819082b080819, 0x080819082b081908, 0x080819082b08192b, 0x080819082b082b19, 0x080819082b190808, 0x080819082b191919, 0x080819082b192b08, 0x080819082b2b0819, 0x080819082b2b1908, 0x0808191908080808, 0x080819190808082b, 0x0808191908081919, 0x0808191908082b08, 0x0808191908082b2b, 0x0808191908190819, 0x0808191908191908, 0x080819190819192b, 0x0808191908192b19, 0x08081919082b0808, 0x08081919082b1919, 0x08081919082b2b08, 0x0808191919080819, 0x0808191919081908, 0x080819191908192b, 0x0808191919082b19, 0x0808191919190808, 0x080819191919082b, 0x0808191919191919, 0x0808191919192b08, 0x08081919192b0819, 0x08081919192b1908, 0x080819192b080808, 0x080819192b08082b, 0x080819192b081919, 0x080819192b082b08, 0x080819192b190819, 0x080819192b191908, 0x080819192b2b0808, 0x0808192b08080819, 0x0808192b08081908, 0x0808192b0808192b, 0x0808192b08082b19, 0x0808192b08190808, 0x0808192b08191919, 0x0808192b19080808, 0x0808192b19081919, 0x0808192b19082b08, 0x0808192b19190819, 0x0808192b19191908, 0x0808192b192b0808, 0x0808192b2b080819, 0x0808192b2b081908, 0x0808192b2b190808, 0x08082b0808080808, 0x08082b080808082b, 0x08082b0808081919, 0x08082b0808082b08, 0x08082b0808190819, 0x08082b0808191908, 0x08082b080819192b, 0x08082b0808192b19, 0x08082b08082b0808, 0x08082b08082b1919, 0x08082b08082b2b2b, 0x08082b0819080819, 0x08082b0819081908, 0x08082b081908192b, 0x08082b0819082b19, 0x08082b0819190808, 0x08082b081919082b, 0x08082b0819191919, 0x08082b0819192b08, 0x08082b08192b0819, 0x08082b08192b1908, 0x08082b082b080808, 0x08082b082b081919, 0x08082b082b191908, 0x08082b082b2b2b2b, 0x08082b1908080819, 0x08082b1908081908, 0x08082b1908190808, 0x08082b190819082b, 0x08082b1908191919, 0x08082b1908192b08, 0x08082b19082b0819, 0x08082b1919080808, 0x08082b1919081919, 0x08082b1919082b08, 0x08082b1919190819, 0x08082b1919191908, 0x08082b19192b0808, 0x08082b192b080819, 0x08082b192b190808, 0x08082b2b08080808, 0x08082b2b08190819, 0x08082b2b08191908, 0x08082b2b082b082b, 0x08082b2b082b2b08, 0x08082b2b082b2b2b, 0x08082b2b19190808, 0x08082b2b2b192b19, 0x0819080808080819, 0x0819080808081908, 0x081908080808192b, 0x0819080808082b19, 0x0819080808190808, 0x081908080819082b, 0x0819080808191919, 0x0819080808192b08, 0x08190808082b0819, 0x08190808082b1908, 0x08190808082b192b, 0x0819080819080808, 0x081908081908082b, 0x0819080819081919, 0x0819080819082b08, 0x0819080819190819, 0x0819080819191908, 0x081908081919192b, 0x0819080819192b19, 0x08190808192b0808, 0x08190808192b082b, 0x08190808192b1919, 0x08190808192b2b08, 0x081908082b080819, 0x081908082b081908, 0x081908082b08192b, 0x081908082b190808, 0x081908082b191919, 0x081908082b192b08, 0x081908082b2b0819, 0x081908082b2b1908, 0x0819081908080808, 0x081908190808082b, 0x0819081908081919, 0x0819081908082b08, 0x0819081908082b2b, 0x0819081908190819, 0x0819081908191908, 0x081908190819192b, 0x0819081908192b19, 0x08190819082b0808, 0x08190819082b082b, 0x081Line truncated
|
||||
|
||||
constant uint iq3xxs_grid[256] = {0x04040404, 0x04040414, 0x04040424, 0x04040c0c, 0x04040c1c, 0x04040c3e, 0x04041404, 0x04041414, 0x04041c0c, 0x04042414, 0x04043e1c, 0x04043e2c, 0x040c040c, 0x040c041c, 0x040c0c04, 0x040c0c14, 0x040c140c, 0x040c142c, 0x040c1c04, 0x040c1c14, 0x040c240c, 0x040c2c24, 0x040c3e04, 0x04140404, 0x04140414, 0x04140424, 0x04140c0c, 0x04141404, 0x04141414, 0x04141c0c, 0x04141c1c, 0x04141c3e, 0x04142c0c, 0x04142c3e, 0x04143e2c, 0x041c040c, 0x041c043e, 0x041c0c04, 0x041c0c14, 0x041c142c, 0x041c3e04, 0x04240c1c, 0x04241c3e, 0x04242424, 0x04242c3e, 0x04243e1c, 0x04243e2c, 0x042c040c, 0x042c043e, 0x042c1c14, 0x042c2c14, 0x04341c2c, 0x04343424, 0x043e0c04, 0x043e0c24, 0x043e0c34, 0x043e241c, 0x043e340c, 0x0c04040c, 0x0c04041c, 0x0c040c04, 0x0c040c14, 0x0c04140c, 0x0c04141c, 0x0c041c04, 0x0c041c14, 0x0c041c24, 0x0c04243e, 0x0c042c04, 0x0c0c0404, 0x0c0c0414, 0x0c0c0c0c, 0x0c0c1404, 0x0c0c1414, 0x0c14040c, 0x0c14041c, 0x0c140c04, 0x0c140c14, 0x0c14140c, 0x0c141c04, 0x0c143e14, 0x0c1c0404, 0x0c1c0414, 0x0c1c1404, 0x0c1c1c0c, 0x0c1c2434, 0x0c1c3434, 0x0c24040c, 0x0c24042c, 0x0c242c04, 0x0c2c1404, 0x0c2c1424, 0x0c2c2434, 0x0c2c3e0c, 0x0c34042c, 0x0c3e1414, 0x0c3e2404, 0x14040404, 0x14040414, 0x14040c0c, 0x14040c1c, 0x14041404, 0x14041414, 0x14041434, 0x14041c0c, 0x14042414, 0x140c040c, 0x140c041c, 0x140c042c, 0x140c0c04, 0x140c0c14, 0x140c140c, 0x140c1c04, 0x140c341c, 0x140c343e, 0x140c3e04, 0x14140404, 0x14140414, 0x14140c0c, 0x14140c3e, 0x14141404, 0x14141414, 0x14141c3e, 0x14142404, 0x14142c2c, 0x141c040c, 0x141c0c04, 0x141c0c24, 0x141c3e04, 0x141c3e24, 0x14241c2c, 0x14242c1c, 0x142c041c, 0x142c143e, 0x142c240c, 0x142c3e24, 0x143e040c, 0x143e041c, 0x143e0c34, 0x143e242c, 0x1c04040c, 0x1c040c04, 0x1c040c14, 0x1c04140c, 0x1c04141c, 0x1c042c04, 0x1c04342c, 0x1c043e14, 0x1c0c0404, 0x1c0c0414, 0x1c0c1404, 0x1c0c1c0c, 0x1c0c2424, 0x1c0c2434, 0x1c14040c, 0x1c14041c, 0x1c140c04, 0x1c14142c, 0x1c142c14, 0x1c143e14, 0x1c1c0c0c, 0x1c1c1c1c, 0x1c241c04, 0x1c24243e, 0x1c243e14, 0x1c2c0404, 0x1c2c0434, 0x1c2c1414, 0x1c2c2c2c, 0x1c340c24, 0x1c341c34, 0x1c34341c, 0x1c3e1c1c, 0x1c3e3404, 0x24040424, 0x24040c3e, 0x24041c2c, 0x24041c3e, 0x24042c1c, 0x24042c3e, 0x240c3e24, 0x24141404, 0x24141c3e, 0x24142404, 0x24143404, 0x24143434, 0x241c043e, 0x241c242c, 0x24240424, 0x24242c0c, 0x24243424, 0x242c142c, 0x242c241c, 0x242c3e04, 0x243e042c, 0x243e0c04, 0x243e0c14, 0x243e1c04, 0x2c040c14, 0x2c04240c, 0x2c043e04, 0x2c0c0404, 0x2c0c0434, 0x2c0c1434, 0x2c0c2c2c, 0x2c140c24, 0x2c141c14, 0x2c143e14, 0x2c1c0414, 0x2c1c2c1c, 0x2c240c04, 0x2c24141c, 0x2c24143e, 0x2c243e14, 0x2c2c0414, 0x2c2c1c0c, 0x2c342c04, 0x2c3e1424, 0x2c3e2414, 0x34041424, 0x34042424, 0x34042434, 0x34043424, 0x340c140c, 0x340c340c, 0x34140c3e, 0x34143424, 0x341c1c04, 0x341c1c34, 0x34242424, 0x342c042c, 0x342c2c14, 0x34341c1c, 0x343e041c, 0x343e140c, 0x3e04041c, 0x3e04042c, 0x3e04043e, 0x3e040c04, 0x3e041c14, 0x3e042c14, 0x3e0c1434, 0x3e0c2404, 0x3e140c14, 0x3e14242c, 0x3e142c14, 0x3e1c0404, 0x3e1c0c2c, 0x3e1c1c1c, 0x3e1c3404, 0x3e24140c, 0x3e24240c, 0x3e2c0404, 0x3e2c0414, 0x3e2c1424, 0x3e341c04,};
|
||||
|
||||
constant uint iq3s_grid[512] = {0x01010101, 0x01010103, 0x01010105, 0x0101010b, 0x0101010f, 0x01010301, 0x01010303, 0x01010305, 0x01010309, 0x0101030d, 0x01010501, 0x01010503, 0x0101050b, 0x01010707, 0x01010901, 0x01010905, 0x0101090b, 0x0101090f, 0x01010b03, 0x01010b07, 0x01010d01, 0x01010d05, 0x01010f03, 0x01010f09, 0x01010f0f, 0x01030101, 0x01030103, 0x01030105, 0x01030109, 0x01030301, 0x01030303, 0x0103030b, 0x01030501, 0x01030507, 0x0103050f, 0x01030703, 0x0103070b, 0x01030909, 0x01030d03, 0x01030d0b, 0x01030f05, 0x01050101, 0x01050103, 0x0105010b, 0x0105010f, 0x01050301, 0x01050307, 0x0105030d, 0x01050503, 0x0105050b, 0x01050701, 0x01050709, 0x01050905, 0x0105090b, 0x0105090f, 0x01050b03, 0x01050b07, 0x01050f01, 0x01050f07, 0x01070107, 0x01070303, 0x0107030b, 0x01070501, 0x01070505, 0x01070703, 0x01070707, 0x0107070d, 0x01070909, 0x01070b01, 0x01070b05, 0x01070d0f, 0x01070f03, 0x01070f0b, 0x01090101, 0x01090307, 0x0109030f, 0x01090503, 0x01090509, 0x01090705, 0x01090901, 0x01090907, 0x01090b03, 0x01090f01, 0x010b0105, 0x010b0109, 0x010b0501, 0x010b0505, 0x010b050d, 0x010b0707, 0x010b0903, 0x010b090b, 0x010b090f, 0x010b0d0d, 0x010b0f07, 0x010d010d, 0x010d0303, 0x010d0307, 0x010d0703, 0x010d0b05, 0x010d0f03, 0x010f0101, 0x010f0105, 0x010f0109, 0x010f0501, 0x010f0505, 0x010f050d, 0x010f0707, 0x010f0b01, 0x010f0b09, 0x03010101, 0x03010103, 0x03010105, 0x03010109, 0x03010301, 0x03010303, 0x03010307, 0x0301030b, 0x0301030f, 0x03010501, 0x03010505, 0x03010703, 0x03010709, 0x0301070d, 0x03010b09, 0x03010b0d, 0x03010d03, 0x03010f05, 0x03030101, 0x03030103, 0x03030107, 0x0303010d, 0x03030301, 0x03030309, 0x03030503, 0x03030701, 0x03030707, 0x03030903, 0x03030b01, 0x03030b05, 0x03030f01, 0x03030f0d, 0x03050101, 0x03050305, 0x0305030b, 0x0305030f, 0x03050501, 0x03050509, 0x03050705, 0x03050901, 0x03050907, 0x03050b0b, 0x03050d01, 0x03050f05, 0x03070103, 0x03070109, 0x0307010f, 0x03070301, 0x03070307, 0x03070503, 0x0307050f, 0x03070701, 0x03070709, 0x03070903, 0x03070d05, 0x03070f01, 0x03090107, 0x0309010b, 0x03090305, 0x03090309, 0x03090703, 0x03090707, 0x03090905, 0x0309090d, 0x03090b01, 0x03090b09, 0x030b0103, 0x030b0301, 0x030b0307, 0x030b0503, 0x030b0701, 0x030b0705, 0x030b0b03, 0x030d0501, 0x030d0509, 0x030d050f, 0x030d0909, 0x030d090d, 0x030f0103, 0x030f0107, 0x030f0301, 0x030f0305, 0x030f0503, 0x030f070b, 0x030f0903, 0x030f0d05, 0x030f0f01, 0x05010101, 0x05010103, 0x05010107, 0x0501010b, 0x0501010f, 0x05010301, 0x05010305, 0x05010309, 0x0501030d, 0x05010503, 0x05010507, 0x0501050f, 0x05010701, 0x05010705, 0x05010903, 0x05010907, 0x0501090b, 0x05010b01, 0x05010b05, 0x05010d0f, 0x05010f01, 0x05010f07, 0x05010f0b, 0x05030101, 0x05030105, 0x05030301, 0x05030307, 0x0503030f, 0x05030505, 0x0503050b, 0x05030703, 0x05030709, 0x05030905, 0x05030b03, 0x05050103, 0x05050109, 0x0505010f, 0x05050503, 0x05050507, 0x05050701, 0x0505070f, 0x05050903, 0x05050b07, 0x05050b0f, 0x05050f03, 0x05050f09, 0x05070101, 0x05070105, 0x0507010b, 0x05070303, 0x05070505, 0x05070509, 0x05070703, 0x05070707, 0x05070905, 0x05070b01, 0x05070d0d, 0x05090103, 0x0509010f, 0x05090501, 0x05090507, 0x05090705, 0x0509070b, 0x05090903, 0x05090f05, 0x05090f0b, 0x050b0109, 0x050b0303, 0x050b0505, 0x050b070f, 0x050b0901, 0x050b0b07, 0x050b0f01, 0x050d0101, 0x050d0105, 0x050d010f, 0x050d0503, 0x050d0b0b, 0x050d0d03, 0x050f010b, 0x050f0303, 0x050f050d, 0x050f0701, 0x050f0907, 0x050f0b01, 0x07010105, 0x07010303, 0x07010307, 0x0701030b, 0x0701030f, 0x07010505, 0x07010703, 0x07010707, 0x0701070b, 0x07010905, 0x07010909, 0x0701090f, 0x07010b03, 0x07010d07, 0x07010f03, 0x07030103, 0x07030107, 0x0703010b, 0x07030309, 0x07030503, 0x07030507, 0x07030901, 0x07030d01, 0x07030f05, 0x07030f0d, 0x07050101, 0x07050305, 0x07050501, 0x07050705, 0x07050709, 0x07050b01, 0x07070103, 0x07070301, 0x07070309, 0x07070503, 0x07070507, 0x0707050f, 0x07070701, 0x07070903, 0x07070907, 0x0707090f, 0x07070b0b, 0x07070f07, 0x07090107, 0x07090303, 0x0709030d, 0x07090505, 0x07090703, 0x07090b05, 0x07090d01, 0x07090d09, 0x070b0103, 0x070b0301, 0x070b0305, 0x070b050b, 0x070b0705, 0x070b0909, 0x070b0b0d, 0x070b0f07, 0x070d030d, 0x070d0903, 0x070f0103, 0x070f0107, 0x070f0501, 0x070f0505, 0x070f070b, 0x09010101, 0x09010109, 0x09010305, 0x09010501, 0x09010509, 0x0901050f, 0x09010705, 0x09010903, 0x09010b01, 0x09010f01, 0x09030105, 0x0903010f, 0x09030303, 0x09030307, 0x09030505, 0x09030701, 0x0903070b, 0x09030907, 0x09030b03, 0x09030b0b, 0x09050103, 0x09050107, 0x09050301, 0x0905030b, 0x09050503, 0x09050707, 0x09050901, 0x09050b0f, 0x09050d05, 0x09050f01, 0x09070109, 0x09070303, 0x09070307, 0x09070501, 0x09070505, 0x09070703, 0x0907070b, 0x09090101, 0x09090105, 0x09090509, 0x0909070f, 0x09090901, 0x09090f03, 0x090b010b, 0x090b010f, 0x090b0503, 0x090b0d05, 0x090d0307, 0x090d0709, 0x090d0d01, 0x090f0301, 0x090f030b, 0x090f0701, 0x090f0907, 0x090f0b03, 0x0b010105, 0x0b010301, 0x0b010309, 0x0b010505, 0x0b010901, 0x0b010909, 0x0b01090f, 0x0b010b05, 0x0b010d0d, 0x0b010f09, 0x0b030103, 0x0b030107,Line truncated
|
||||
@@ -0,0 +1,16 @@
|
||||
// metal/src/engine/stubs_ds4.cpp - CPU-kernel entry points the DeepSeek engine (strata_ds4) references that have no Apple
|
||||
// implementation. strata::ds4::gguf::dots() asks iq256x_supported() before it uses iq256x_rows(); on arm64 the answer is no and the
|
||||
// CPU lane keeps ggml-cpu's rows, which have their own NEON kernels.
|
||||
#include "strata/kernels/cpu/iq_avx2.hpp"
|
||||
|
||||
#include <cstdlib>
|
||||
|
||||
namespace strata::kernels::cpu {
|
||||
|
||||
bool iq256x_supported(int) noexcept { return false; }
|
||||
|
||||
void iq256x_rows(int, const uint8_t*, size_t, int, const void* const*, int, float* const*, int, int) {
|
||||
std::abort(); // unreachable: iq256x_supported() is false here, and callers check it
|
||||
}
|
||||
|
||||
} // namespace strata::kernels::cpu
|
||||
+218
-1
@@ -24,11 +24,21 @@
|
||||
#include <atomic>
|
||||
#include <chrono>
|
||||
#include <cstdio>
|
||||
#if defined(STRATA_DS4_RUNTIME)
|
||||
#include <cstdlib>
|
||||
#endif
|
||||
#include <cstring>
|
||||
#include <iterator>
|
||||
#include <map>
|
||||
#if defined(STRATA_DS4_RUNTIME)
|
||||
#include <mutex> // recursive_mutex
|
||||
#else
|
||||
#include <mutex>
|
||||
#endif
|
||||
#include <thread>
|
||||
#if defined(STRATA_DS4_RUNTIME)
|
||||
#include <algorithm>
|
||||
#endif
|
||||
#include <vector>
|
||||
|
||||
namespace strata::gpu {
|
||||
@@ -66,6 +76,9 @@ struct Op {
|
||||
struct EventImpl {
|
||||
MTL::SharedEvent* ev = nullptr; // the recording stream's timeline
|
||||
uint64_t value = 0; // 0: never recorded (complete, as a fresh CUDA event)
|
||||
#if defined(STRATA_DS4_RUNTIME)
|
||||
void* token = nullptr; // a mark on the context's queue (ds4_ctx_mark), released by the wait
|
||||
#endif
|
||||
};
|
||||
|
||||
struct GraphImpl {
|
||||
@@ -118,8 +131,16 @@ Error fail(Error e) {
|
||||
return e;
|
||||
}
|
||||
|
||||
#if defined(STRATA_DS4_RUNTIME)
|
||||
static std::vector<StreamImpl*> g_all_streams; // every stream's queue holds pending copies until it is committed
|
||||
static std::mutex g_all_streams_mu;
|
||||
|
||||
#endif
|
||||
StreamImpl* make_stream() {
|
||||
auto* s = new StreamImpl;
|
||||
#if defined(STRATA_DS4_RUNTIME)
|
||||
{ std::lock_guard<std::mutex> lk(g_all_streams_mu); g_all_streams.push_back(s); }
|
||||
#endif
|
||||
s->q = rt().dev->newCommandQueue();
|
||||
s->q->addResidencySet(rt().rs);
|
||||
s->tl = rt().dev->newSharedEvent();
|
||||
@@ -154,30 +175,83 @@ const Alloc* find(const void* p, size_t* off) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
#if defined(STRATA_DS4_RUNTIME)
|
||||
// The engine's host threads (the prompt path, the loader, the expert cache) have no autorelease pool, and metal-cpp's
|
||||
// commandBuffer() / computeCommandEncoder() / blitCommandEncoder() hand back autoreleased objects: with no pool that last reference was
|
||||
// never given back, so every command buffer and encoder stayed alive with the buffers it had named (~800 command buffers a
|
||||
// long prompt). A pool around each creation (and, for the blits, around the whole encode) gives it back.
|
||||
struct ScopedPool {
|
||||
NS::AutoreleasePool* p;
|
||||
ScopedPool() : p(NS::AutoreleasePool::alloc()->init()) {}
|
||||
~ScopedPool() { p->release(); }
|
||||
};
|
||||
#endif
|
||||
MTL::CommandBuffer* open_cb(StreamImpl* s) {
|
||||
if (!s->cb) {
|
||||
#if defined(STRATA_DS4_RUNTIME)
|
||||
ScopedPool pool;
|
||||
#endif
|
||||
s->cb = s->q->commandBufferWithUnretainedReferences();
|
||||
s->cb->retain();
|
||||
++rt().stats.command_buffers;
|
||||
}
|
||||
return s->cb;
|
||||
}
|
||||
#if defined(STRATA_DS4_RUNTIME)
|
||||
// StreamImpl's encoder/command-buffer state is touched from the loader, the prefetcher and the compute path.
|
||||
// Without this lock two encoders can be open on one command buffer at once, which Metal asserts on.
|
||||
static std::recursive_mutex g_stream_mu; // encode_copy/flush nest
|
||||
|
||||
#endif
|
||||
void end_encoder(StreamImpl* s) {
|
||||
#if defined(STRATA_DS4_RUNTIME)
|
||||
if (s->enc) { if (std::getenv("STRATA_GPU_TRACE")) std::fprintf(stderr, "[gpu] close compute enc %p\n", (void*) s->enc);
|
||||
s->enc->endEncoding(); s->enc->release(); s->enc = nullptr; }
|
||||
#else
|
||||
if (s->enc) { s->enc->endEncoding(); s->enc = nullptr; }
|
||||
#endif
|
||||
}
|
||||
MTL::ComputeCommandEncoder* encoder(StreamImpl* s) {
|
||||
#if defined(STRATA_DS4_RUNTIME)
|
||||
std::lock_guard<std::recursive_mutex> lk(g_stream_mu);
|
||||
if (!s->enc) {
|
||||
MTL::CommandBuffer* cb = open_cb(s);
|
||||
{ ScopedPool pool; s->enc = cb->computeCommandEncoder(); s->enc->retain(); } // (released in end_encoder)
|
||||
if (std::getenv("STRATA_GPU_TRACE")) std::fprintf(stderr, "[gpu] open compute enc %p (cb %p)\n", (void*) s->enc, (void*) s->cb); } // serial dispatch: each dispatch sees the last
|
||||
#else
|
||||
if (!s->enc) s->enc = open_cb(s)->computeCommandEncoder(); // serial dispatch: each dispatch sees the last
|
||||
#endif
|
||||
return s->enc;
|
||||
}
|
||||
void flush(StreamImpl* s) {
|
||||
#if defined(STRATA_DS4_RUNTIME)
|
||||
std::lock_guard<std::recursive_mutex> lk(g_stream_mu);
|
||||
#endif
|
||||
if (!s->cb) return;
|
||||
end_encoder(s);
|
||||
#if defined(STRATA_DS4_RUNTIME)
|
||||
if (std::getenv("STRATA_GPU_TRACE")) std::fprintf(stderr, "[gpu] signal %llu on cb %p (enc %p)\n",
|
||||
(unsigned long long) (s->submitted + 1), (void*) s->cb, (void*) s->enc);
|
||||
#endif
|
||||
s->cb->encodeSignalEvent(s->tl, ++s->submitted);
|
||||
s->cb->commit();
|
||||
s->cb->release();
|
||||
s->cb = nullptr;
|
||||
s->pending = 0;
|
||||
}
|
||||
#if defined(STRATA_DS4_RUNTIME)
|
||||
extern "C" void ds4_metal_flush();
|
||||
extern "C" bool ds4_ctx_copy(void* dst, const void* src, size_t n) __attribute__((weak_import));
|
||||
extern "C" bool ds4_ctx_fill(void* dst, int value, size_t n) __attribute__((weak_import));
|
||||
extern "C" void* ds4_ctx_mark() __attribute__((weak_import));
|
||||
extern "C" void ds4_ctx_wait(void* token) __attribute__((weak_import));
|
||||
extern "C" void ds4_ctx_drop(void* token) __attribute__((weak_import));
|
||||
static bool ctx_queue_ops() { // DS4_STREAM_COPIES=1: the old way (a copy on a stream's own queue, a full barrier each)
|
||||
static const bool on = ds4_ctx_copy != nullptr && std::getenv("DS4_STREAM_COPIES") == nullptr;
|
||||
return on;
|
||||
}
|
||||
|
||||
#endif
|
||||
Error wait_value(MTL::SharedEvent* ev, uint64_t v) {
|
||||
while (ev->signaledValue() < v) std::this_thread::yield();
|
||||
++rt().stats.host_syncs;
|
||||
@@ -195,6 +269,9 @@ void check_purged(const Alloc* a, const char* what) {
|
||||
}
|
||||
|
||||
Error encode_copy(StreamImpl* s, void* dst, const void* src, size_t n) {
|
||||
#if defined(STRATA_DS4_RUNTIME)
|
||||
std::lock_guard<std::recursive_mutex> lk(g_stream_mu);
|
||||
#endif
|
||||
size_t od = 0, os = 0;
|
||||
const Alloc* ad = find(dst, &od);
|
||||
const Alloc* as = find(src, &os);
|
||||
@@ -202,7 +279,13 @@ Error encode_copy(StreamImpl* s, void* dst, const void* src, size_t n) {
|
||||
check_purged(as, "copy");
|
||||
if (ad && as) { // buffer to buffer: a blit, in stream order
|
||||
end_encoder(s);
|
||||
#if defined(STRATA_DS4_RUNTIME)
|
||||
ScopedPool pool;
|
||||
#endif
|
||||
MTL::BlitCommandEncoder* b = open_cb(s)->blitCommandEncoder();
|
||||
#if defined(STRATA_DS4_RUNTIME)
|
||||
if (std::getenv("STRATA_GPU_TRACE")) std::fprintf(stderr, "[gpu] blit copy %zu\n", n);
|
||||
#endif
|
||||
b->copyFromBuffer(as->buf, os, ad->buf, od, n);
|
||||
b->endEncoding();
|
||||
++rt().stats.blits;
|
||||
@@ -219,11 +302,17 @@ Error encode_copy(StreamImpl* s, void* dst, const void* src, size_t n) {
|
||||
}
|
||||
|
||||
Error encode_fill(StreamImpl* s, void* p, int value, size_t n) {
|
||||
#if defined(STRATA_DS4_RUNTIME)
|
||||
std::lock_guard<std::recursive_mutex> lk(g_stream_mu);
|
||||
#endif
|
||||
size_t off = 0;
|
||||
const Alloc* a = find(p, &off);
|
||||
if (!a) return fail(kErrorInvalidValue);
|
||||
check_purged(a, "fill");
|
||||
end_encoder(s);
|
||||
#if defined(STRATA_DS4_RUNTIME)
|
||||
ScopedPool pool;
|
||||
#endif
|
||||
MTL::BlitCommandEncoder* b = open_cb(s)->blitCommandEncoder();
|
||||
b->fillBuffer(a->buf, NS::Range::Make(off, n), (uint8_t) value);
|
||||
b->endEncoding();
|
||||
@@ -232,6 +321,9 @@ Error encode_fill(StreamImpl* s, void* p, int value, size_t n) {
|
||||
}
|
||||
|
||||
Error encode_dispatch(StreamImpl* s, const Op& o) {
|
||||
#if defined(STRATA_DS4_RUNTIME)
|
||||
std::lock_guard<std::recursive_mutex> lk(g_stream_mu);
|
||||
#endif
|
||||
MTL::ComputeCommandEncoder* e = encoder(s);
|
||||
e->setComputePipelineState(o.pipe);
|
||||
if (o.big_args.empty()) e->setBytes(o.args, o.arg_bytes, 0);
|
||||
@@ -277,7 +369,12 @@ void alloc_log(const char* what, size_t n) {
|
||||
|
||||
Error new_alloc(void** p, size_t bytes, bool host) {
|
||||
Runtime& r = rt();
|
||||
#if defined(STRATA_DS4_RUNTIME)
|
||||
const size_t pg = 16384, want = bytes ? bytes : 1;
|
||||
const size_t n = std::getenv("DS4_NO_PAD") ? want : ((want + pg - 1) / pg) * pg;
|
||||
#else
|
||||
const size_t n = bytes ? bytes : 1;
|
||||
#endif
|
||||
MTL::Buffer* b = r.dev->newBuffer(n, MTL::ResourceStorageModeShared | MTL::ResourceHazardTrackingModeUntracked);
|
||||
if (!b) return fail(kErrorMemoryAllocation);
|
||||
Alloc a{b, b->gpuAddress(), (uint8_t*) b->contents(), n, host};
|
||||
@@ -299,6 +396,9 @@ Error free_alloc(void* p) {
|
||||
size_t off = 0;
|
||||
const Alloc* a = find(p, &off);
|
||||
if (!a || off != 0) return fail(kErrorInvalidValue);
|
||||
#if defined(STRATA_DS4_RUNTIME)
|
||||
if (std::getenv("DS4_TRACE_FREE")) std::fprintf(stderr, "gpu free %zu bytes (host=%d) at %p\n", a->bytes, (int) a->host, p);
|
||||
#endif
|
||||
device_synchronize(); // as cudaFree: the GPU is done with it
|
||||
std::lock_guard<std::mutex> lk(r.mu);
|
||||
MTL::Buffer* b = a->buf;
|
||||
@@ -314,6 +414,22 @@ Error free_alloc(void* p) {
|
||||
} // namespace
|
||||
|
||||
// ---------------------------------------------------------------------------------------------------- runtime.hpp
|
||||
#if defined(STRATA_DS4_RUNTIME)
|
||||
void* buffer_for_cpu_impl(const void* cpu, size_t bytes, size_t* off) {
|
||||
Runtime& r = rt();
|
||||
std::lock_guard<std::mutex> lk(r.mu);
|
||||
auto ic = r.by_cpu.upper_bound((uintptr_t) cpu);
|
||||
if (ic == r.by_cpu.begin()) return nullptr;
|
||||
--ic;
|
||||
const Alloc& a = r.by_gpu[ic->second];
|
||||
if ((uintptr_t) cpu < ic->first || (uintptr_t) cpu + bytes > ic->first + a.bytes) return nullptr;
|
||||
*off = (uintptr_t) cpu - ic->first;
|
||||
return (void*) a.buf;
|
||||
}
|
||||
|
||||
MTL::ResidencySet* residency_set_impl() { return rt().rs; }
|
||||
|
||||
#endif
|
||||
void* host_view(const void* p) {
|
||||
size_t off = 0;
|
||||
const Alloc* a = find(p, &off);
|
||||
@@ -358,6 +474,9 @@ Error get_device(int* dev) { *dev = 0; return kSuccess; }
|
||||
Error set_device(int dev) { return dev == 0 ? kSuccess : fail(kErrorInvalidValue); }
|
||||
Error device_count(int* n) { *n = rt().dev ? 1 : 0; return kSuccess; }
|
||||
Error device_synchronize() {
|
||||
#if defined(STRATA_DS4_RUNTIME)
|
||||
ds4_metal_flush();
|
||||
#endif
|
||||
Runtime& r = rt();
|
||||
StreamImpl* s = S(nullptr);
|
||||
flush(s);
|
||||
@@ -427,7 +546,30 @@ Error host_device_pointer_raw(void** dev, void* host) {
|
||||
*dev = (void*) (uintptr_t) (a->gpu + off);
|
||||
return kSuccess;
|
||||
}
|
||||
#if defined(STRATA_DS4_RUNTIME)
|
||||
// A synchronising call is a barrier for BOTH queues: the dispatches deferred in mtl_rt's command buffer have to run before
|
||||
// the host reads or overwrites what they touch (a window's last layers still pending while the next window's embedding rows
|
||||
// are copied over the buffer its embed reads, measured: window 2's residual stream was window 1's stale tail).
|
||||
// commit every stream's pending copies/fills, then wait for all of them (commit first: a stream may wait on another's event)
|
||||
extern "C" void ds4_gpu_sync_all_streams() {
|
||||
std::vector<StreamImpl*> v;
|
||||
{ std::lock_guard<std::mutex> lk(g_all_streams_mu); v = g_all_streams; }
|
||||
for (StreamImpl* s : v) if (!s->rec) flush(s);
|
||||
for (StreamImpl* s : v) if (!s->rec) wait_value(s->tl, s->submitted);
|
||||
}
|
||||
static thread_local bool t_in_barrier = false;
|
||||
static void both_queues_barrier() {
|
||||
if (t_in_barrier) return;
|
||||
t_in_barrier = true;
|
||||
ds4_metal_flush();
|
||||
t_in_barrier = false;
|
||||
}
|
||||
|
||||
#endif
|
||||
Error memset_device(void* p, int value, size_t bytes) {
|
||||
#if defined(STRATA_DS4_RUNTIME)
|
||||
both_queues_barrier();
|
||||
#endif
|
||||
StreamImpl* s = S(nullptr);
|
||||
const Error e = encode_fill(s, p, value, bytes);
|
||||
flush(s);
|
||||
@@ -435,6 +577,13 @@ Error memset_device(void* p, int value, size_t bytes) {
|
||||
return e;
|
||||
}
|
||||
Error memset_async(void* p, int value, size_t bytes, Stream st) {
|
||||
#if defined(STRATA_DS4_RUNTIME)
|
||||
if (!S(st)->rec && ctx_queue_ops() && bytes > 0) {
|
||||
size_t o1 = 0;
|
||||
if (find(p, &o1) && ds4_ctx_fill(p, value, bytes)) return kSuccess;
|
||||
}
|
||||
if (!S(st)->rec && !std::getenv("DS4_NO_QBARRIER")) both_queues_barrier();
|
||||
#endif
|
||||
StreamImpl* s = S(st);
|
||||
if (s->rec) {
|
||||
Op o;
|
||||
@@ -446,6 +595,9 @@ Error memset_async(void* p, int value, size_t bytes, Stream st) {
|
||||
}
|
||||
|
||||
Error memcpy_sync(void* dst, const void* src, size_t bytes, CopyKind) {
|
||||
#if defined(STRATA_DS4_RUNTIME)
|
||||
both_queues_barrier();
|
||||
#endif
|
||||
StreamImpl* s = S(nullptr);
|
||||
const Error e = encode_copy(s, dst, src, bytes);
|
||||
flush(s);
|
||||
@@ -464,6 +616,13 @@ Error memcpy_peer_async(void* dst, int dst_dev, const void* src, int src_dev, si
|
||||
return memcpy_async(dst, src, bytes, kDeviceToDevice, st);
|
||||
}
|
||||
Error memcpy_async(void* dst, const void* src, size_t bytes, CopyKind, Stream st) {
|
||||
#if defined(STRATA_DS4_RUNTIME)
|
||||
if (!S(st)->rec && ctx_queue_ops() && bytes > 0) {
|
||||
size_t o1 = 0, o2 = 0;
|
||||
if (find(dst, &o1) && find(src, &o2) && ds4_ctx_copy(dst, src, bytes)) return kSuccess;
|
||||
}
|
||||
if (!S(st)->rec && !std::getenv("DS4_NO_QBARRIER")) both_queues_barrier();
|
||||
#endif
|
||||
StreamImpl* s = S(st);
|
||||
if (s->rec) {
|
||||
Op o;
|
||||
@@ -479,12 +638,19 @@ Error stream_destroy(Stream s) {
|
||||
if (!s) return kSuccess;
|
||||
flush(s);
|
||||
wait_value(s->tl, s->submitted);
|
||||
#if defined(STRATA_DS4_RUNTIME)
|
||||
{ std::lock_guard<std::mutex> lk(g_all_streams_mu);
|
||||
g_all_streams.erase(std::remove(g_all_streams.begin(), g_all_streams.end(), s), g_all_streams.end()); }
|
||||
#endif
|
||||
s->tl->release();
|
||||
s->q->release();
|
||||
delete s;
|
||||
return kSuccess;
|
||||
}
|
||||
Error stream_synchronize(Stream st) {
|
||||
#if defined(STRATA_DS4_RUNTIME)
|
||||
both_queues_barrier();
|
||||
#endif
|
||||
StreamImpl* s = S(st);
|
||||
flush(s);
|
||||
return wait_value(s->tl, s->submitted);
|
||||
@@ -516,8 +682,23 @@ Error launch_host_func(Stream st, HostFn fn, void* arg) {
|
||||
}
|
||||
|
||||
Error event_create(Event* e, unsigned) { *e = new EventImpl; return kSuccess; }
|
||||
Error event_destroy(Event e) { delete e; return kSuccess; }
|
||||
Error event_destroy(Event e) {
|
||||
#if defined(STRATA_DS4_RUNTIME)
|
||||
if (e && e->token && ds4_ctx_drop) { ds4_ctx_drop(e->token); e->token = nullptr; } // (a mark nobody waited for holds its command buffer)
|
||||
#endif
|
||||
delete e;
|
||||
return kSuccess;
|
||||
}
|
||||
Error event_record(Event e, Stream st) {
|
||||
#if defined(STRATA_DS4_RUNTIME)
|
||||
if (ctx_queue_ops() && !S(st)->rec) { // the work the event follows is in the context's command buffer: end it, wait later
|
||||
if (e->token && ds4_ctx_drop) ds4_ctx_drop(e->token); // (recorded again before it was waited for: the earlier mark would leak its command buffer)
|
||||
e->token = ds4_ctx_mark();
|
||||
e->ev = nullptr;
|
||||
e->value = 0;
|
||||
return kSuccess;
|
||||
}
|
||||
#endif
|
||||
StreamImpl* s = S(st);
|
||||
if (s->rec) {
|
||||
Op o;
|
||||
@@ -527,8 +708,24 @@ Error event_record(Event e, Stream st) {
|
||||
}
|
||||
return record_event(e, s);
|
||||
}
|
||||
#if defined(STRATA_DS4_RUNTIME)
|
||||
Error event_query(Event e) {
|
||||
if (e->token) return kNotReady; // (a mark waits for its wait)
|
||||
return !e->ev || e->ev->signaledValue() >= e->value ? kSuccess : kNotReady;
|
||||
}
|
||||
Error event_synchronize(Event e) {
|
||||
if (e->token) {
|
||||
void* t = e->token;
|
||||
e->token = nullptr;
|
||||
ds4_ctx_wait(t);
|
||||
return kSuccess;
|
||||
}
|
||||
return e->ev ? wait_value(e->ev, e->value) : kSuccess;
|
||||
}
|
||||
#else
|
||||
Error event_query(Event e) { return !e->ev || e->ev->signaledValue() >= e->value ? kSuccess : kNotReady; }
|
||||
Error event_synchronize(Event e) { return e->ev ? wait_value(e->ev, e->value) : kSuccess; }
|
||||
#endif
|
||||
Error event_elapsed_ms(float* ms, Event, Event) { *ms = 0.0f; return fail(kErrorUnsupported); }
|
||||
|
||||
Error begin_capture(Stream st) {
|
||||
@@ -638,3 +835,23 @@ Stats stats() { return rt().stats; }
|
||||
|
||||
} // namespace metal
|
||||
} // namespace strata::gpu
|
||||
|
||||
#if defined(STRATA_DS4_RUNTIME)
|
||||
extern "C" void* ds4_gpu_buffer_for_cpu(const void* cpu, size_t bytes, size_t* off) {
|
||||
return strata::gpu::buffer_for_cpu_impl(cpu, bytes, off);
|
||||
}
|
||||
|
||||
// The residency set every strata::gpu stream queue carries: the Context's queue (mtl_rt) needs it too, because kernels
|
||||
// dereference raw gpuAddresses (weights, ring buffers, job arrays) that no setBuffer names, and memory that is not resident
|
||||
// reads as zeros.
|
||||
extern "C" void* ds4_gpu_residency_set() { return (void*) strata::gpu::residency_set_impl(); }
|
||||
|
||||
// bytes from `cpu` to the end of the allocation that holds it (0: not an allocation)
|
||||
extern "C" size_t ds4_gpu_alloc_remaining(const void* cpu) {
|
||||
size_t off = 0;
|
||||
void* b = strata::gpu::buffer_for_cpu_impl(cpu, 1, &off);
|
||||
if (!b) return 0;
|
||||
return ((MTL::Buffer*) b)->length() - off;
|
||||
}
|
||||
|
||||
#endif
|
||||
@@ -22,6 +22,7 @@ struct Buffer {
|
||||
// a buffer longer than maxBufferLength: overlapping chunks every `stride` bytes (see Context::wrap)
|
||||
std::shared_ptr<std::vector<void*>> chunks;
|
||||
size_t stride = 0;
|
||||
size_t bias = 0; // bytes from the MTLBuffer's start to ptr (a view onto an allocation the runtime owns)
|
||||
template <class T> T* as() const { return static_cast<T*>(ptr); }
|
||||
};
|
||||
|
||||
@@ -82,6 +83,16 @@ public:
|
||||
bool supports_metal4() const;
|
||||
uint64_t recommended_working_set() const;
|
||||
uint64_t max_buffer_length() const;
|
||||
// Keeps the GPU out of its low-clock idle state while dispatches keep coming (a lone kernel in a quiet GPU runs 2-3x slower:
|
||||
// measured 205 vs 69 us for the same matvec): a spin kernel of one threadgroup on a second queue, stopped 50 ms after the
|
||||
// last dispatch. DS4_NO_KEEPALIVE=1: off.
|
||||
void start_keepalive();
|
||||
// Zero-copy weights: the GPU address of host memory [p, p + bytes) (file-backed pages of the model, RAM slots) through a
|
||||
// no-copy buffer over the pages it spans, made resident. 0: not mappable (the cap on buffers is reached, or the pages are not
|
||||
// ours). The first dispatch that reads cold pages waits for them (the buffer is wired on use).
|
||||
uint64_t zc_map(const void* p, size_t bytes);
|
||||
Pipeline pipeline_uncached(int lib, const std::string& fn, const Constants& c);
|
||||
size_t zc_count() const;
|
||||
uint64_t allocated() const { return allocated_; } // bytes of alloc() + wrap() buffers so far
|
||||
|
||||
// Compiles a library from MSL source files (paths), prepended by `prelude` (defines) and, if include_ggml,
|
||||
@@ -100,6 +111,11 @@ public:
|
||||
|
||||
// Encodes `ds` into ONE compute encoder (serial or concurrent dispatch type) and ONE command buffer, `reps`
|
||||
// times over; commits and waits. Returns the GPU time (GPUEndTime - GPUStartTime) in seconds.
|
||||
void* mark(); // split_runs() that returns a token for the buffer just committed (nullptr: nothing was queued)
|
||||
void wait_mark(void* token); // block until that buffer has completed, then drop the token
|
||||
void drop_mark(void* token); // drop a token without waiting (an event recorded again, or destroyed, before it was waited on)
|
||||
void split_runs(); // commit the batched dispatches as their own command buffer, without waiting
|
||||
void flush_runs(); // commit and wait for the dispatches batched since the last flush
|
||||
double run(const std::vector<Dispatch>& ds, int reps = 1, bool concurrent = false, bool barrier_between = false);
|
||||
// A dependency graph in ONE concurrent encoder: a memory barrier only before dispatches with `barrier` set, so
|
||||
// the dispatches between two barriers may overlap. Returns the GPU time in seconds.
|
||||
|
||||
@@ -6,6 +6,18 @@
|
||||
#include "resource_path.h"
|
||||
|
||||
#include <chrono>
|
||||
#if defined(STRATA_DS4_RUNTIME)
|
||||
#include <execinfo.h>
|
||||
#include <algorithm>
|
||||
#include <atomic>
|
||||
#include <mutex>
|
||||
#include <unordered_map>
|
||||
#include <string>
|
||||
#include <deque>
|
||||
#include <mach/mach_time.h>
|
||||
#include <map>
|
||||
#include <thread>
|
||||
#endif
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <fstream>
|
||||
@@ -28,7 +40,21 @@ std::string read_file(const std::string& path) {
|
||||
return ss.str();
|
||||
}
|
||||
|
||||
#if defined(STRATA_DS4_RUNTIME)
|
||||
extern "C" void* ds4_gpu_residency_set() __attribute__((weak_import));
|
||||
#endif
|
||||
struct Context::Impl {
|
||||
#if defined(STRATA_DS4_RUNTIME)
|
||||
id<MTLCommandBuffer> cb_def = nil; // the open command buffer single dispatches accumulate in
|
||||
id<MTLComputeCommandEncoder> enc_def = nil;
|
||||
int def_n = 0;
|
||||
std::string def_names, inflight_names; // (DS4_ERRINFO) the kernels of the open / the committed command buffer
|
||||
id<MTLSharedEvent> qev = nil; // consecutive deferred buffers are chained through it (untracked buffers: the queue alone does not order them)
|
||||
uint64_t qev_v = 0;
|
||||
id<MTLCommandBuffer> inflight = nil; // the last committed-but-not-waited deferred buffer (the queue runs them in order)
|
||||
std::vector<void*> graveyard; // buffers freed while the deferred command buffer still references them (it holds none)
|
||||
std::recursive_mutex mu; // helper threads (the stager, the expert cache) flush too: one writer on the open encoder
|
||||
#endif
|
||||
id<MTLDevice> dev;
|
||||
id<MTLCommandQueue> q;
|
||||
std::vector<id<MTLLibrary>> libs;
|
||||
@@ -39,7 +65,11 @@ 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
|
||||
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];
|
||||
@@ -47,13 +77,243 @@ static inline id<MTLBuffer> BO(const Buffer& b, size_t off, size_t& o) {
|
||||
static inline id<MTLComputePipelineState> P(const Pipeline* p) { return (__bridge id<MTLComputePipelineState>)p->handle; }
|
||||
static std::string g_device_name = "unknown GPU";
|
||||
|
||||
#if defined(STRATA_DS4_RUNTIME)
|
||||
|
||||
// ---- DS4_TIMELINE=1: every deferred dispatch in its own encoder with start / end GPU timestamps; the report at exit gives, per kernel,
|
||||
// its GPU time and the gap the GPU sat idle before it (host waits, event waits, launch). DS4_TL_FROM=N: only from dispatch N on.
|
||||
static std::atomic<uint64_t> g_disp_count{0}; // dispatches encoded
|
||||
namespace {
|
||||
struct Timeline {
|
||||
bool on = false;
|
||||
id<MTLCounterSet> tset = nil;
|
||||
std::vector<id<MTLCounterSampleBuffer>> bufs; // a sample buffer holds only ~1500 encoders: they rotate
|
||||
size_t per = 0, cap = 0, n = 0; // encoders per buffer / in all (the buffers' limit) / recorded
|
||||
long after = 0; // DS4_TL_AFTER: record from the Nth dispatch on
|
||||
std::vector<std::string> names; // per encoder
|
||||
uint64_t cpu0 = 0, gpu0 = 0;
|
||||
bool reg = false;
|
||||
bool gate = true; // ds4_timeline_gate: record only while open
|
||||
bool att = false; // the encoder just made carries a sample slot
|
||||
} g_tl;
|
||||
|
||||
id<MTLComputeCommandEncoder> tl_make_encoder(id<MTLDevice> dev, id<MTLCommandBuffer> cb, MTLDispatchType dt) {
|
||||
g_tl.att = false;
|
||||
if (!g_tl.on) return [cb computeCommandEncoderWithDispatchType:dt];
|
||||
if (g_tl.tset == nil) {
|
||||
for (id<MTLCounterSet> cs in dev.counterSets)
|
||||
if ([cs.name isEqualToString:MTLCommonCounterSetTimestamp]) g_tl.tset = cs;
|
||||
if (g_tl.tset == nil) {
|
||||
std::fprintf(stderr, "mtl: DS4_TIMELINE: no timestamp counter set\n");
|
||||
g_tl.on = false;
|
||||
return [cb computeCommandEncoderWithDispatchType:dt];
|
||||
}
|
||||
for (size_t c = 4096; c >= 64 && g_tl.per == 0; c /= 2) {
|
||||
MTLCounterSampleBufferDescriptor* d = [MTLCounterSampleBufferDescriptor new];
|
||||
d.counterSet = g_tl.tset;
|
||||
d.storageMode = MTLStorageModeShared;
|
||||
d.sampleCount = c * 2;
|
||||
NSError* err = nil;
|
||||
id<MTLCounterSampleBuffer> b = [dev newCounterSampleBufferWithDescriptor:d error:&err];
|
||||
if (b != nil) {
|
||||
g_tl.per = c;
|
||||
g_tl.bufs.push_back(b);
|
||||
}
|
||||
}
|
||||
if (g_tl.per == 0) {
|
||||
std::fprintf(stderr, "mtl: DS4_TIMELINE: no counter sample buffer\n");
|
||||
g_tl.on = false;
|
||||
return [cb computeCommandEncoderWithDispatchType:dt];
|
||||
}
|
||||
g_tl.cap = g_tl.per * 120;
|
||||
MTLTimestamp c0 = 0, g0 = 0;
|
||||
[dev sampleTimestamps:&c0 gpuTimestamp:&g0];
|
||||
g_tl.cpu0 = c0;
|
||||
g_tl.gpu0 = g0;
|
||||
g_tl.names.assign(g_tl.cap, std::string());
|
||||
g_tl.after = std::getenv("DS4_TL_AFTER") ? std::atol(std::getenv("DS4_TL_AFTER")) : 0;
|
||||
if (std::getenv("DS4_TL_DRAFT")) g_tl.gate = false;
|
||||
}
|
||||
if (!g_tl.gate || g_tl.n >= g_tl.cap || (long) g_disp_count.load() < g_tl.after) return [cb computeCommandEncoderWithDispatchType:dt];
|
||||
const size_t bi = g_tl.n / g_tl.per, si = g_tl.n % g_tl.per;
|
||||
if (bi >= g_tl.bufs.size()) {
|
||||
MTLCounterSampleBufferDescriptor* d = [MTLCounterSampleBufferDescriptor new];
|
||||
d.counterSet = g_tl.tset;
|
||||
d.storageMode = MTLStorageModeShared;
|
||||
d.sampleCount = g_tl.per * 2;
|
||||
NSError* err = nil;
|
||||
id<MTLCounterSampleBuffer> b = [dev newCounterSampleBufferWithDescriptor:d error:&err];
|
||||
if (b == nil) {
|
||||
g_tl.cap = g_tl.n;
|
||||
return [cb computeCommandEncoderWithDispatchType:dt];
|
||||
}
|
||||
g_tl.bufs.push_back(b);
|
||||
}
|
||||
MTLComputePassDescriptor* pd = [MTLComputePassDescriptor computePassDescriptor];
|
||||
pd.dispatchType = dt;
|
||||
pd.sampleBufferAttachments[0].sampleBuffer = g_tl.bufs[bi];
|
||||
pd.sampleBufferAttachments[0].startOfEncoderSampleIndex = 2 * si;
|
||||
pd.sampleBufferAttachments[0].endOfEncoderSampleIndex = 2 * si + 1;
|
||||
g_tl.att = true;
|
||||
return [cb computeCommandEncoderWithDescriptor:pd];
|
||||
}
|
||||
|
||||
void tl_report(id<MTLDevice> dev) {
|
||||
if (!g_tl.on && g_tl.bufs.empty()) return;
|
||||
const size_t n = std::min(g_tl.n, g_tl.cap);
|
||||
if (n < 2) return;
|
||||
std::vector<MTLCounterResultTimestamp> tsv(2 * n);
|
||||
for (size_t b = 0; b * g_tl.per < n; ++b) {
|
||||
const size_t cnt = std::min(g_tl.per, n - b * g_tl.per);
|
||||
NSData* data = [g_tl.bufs[b] resolveCounterRange:NSMakeRange(0, 2 * cnt)];
|
||||
if (data == nil) { std::fprintf(stderr, "mtl: DS4_TIMELINE: resolve failed\n"); return; }
|
||||
std::memcpy(&tsv[2 * b * g_tl.per], [data bytes], 2 * cnt * sizeof(MTLCounterResultTimestamp));
|
||||
}
|
||||
const MTLCounterResultTimestamp* ts = tsv.data();
|
||||
MTLTimestamp c1 = 0, g1 = 0;
|
||||
[dev sampleTimestamps:&c1 gpuTimestamp:&g1];
|
||||
mach_timebase_info_data_t tb;
|
||||
mach_timebase_info(&tb);
|
||||
const double cpu_ns_per_tick = (double) tb.numer / (double) tb.denom;
|
||||
(void) cpu_ns_per_tick; // (sampleTimestamps' CPU value is in ns already)
|
||||
const double ns_per_gpu = (g1 > g_tl.gpu0) ? (double) (c1 - g_tl.cpu0) / (double) (g1 - g_tl.gpu0) : 1.0;
|
||||
std::fprintf(stderr, "timeline: cap %zu n %zu scale %.6f ns/tick first ts %llu %llu last %llu\n", g_tl.cap, g_tl.n, ns_per_gpu, (unsigned long long) ts[0].timestamp, (unsigned long long) ts[1].timestamp, (unsigned long long) ts[2 * n - 1].timestamp);
|
||||
size_t from = std::getenv("DS4_TL_FROM") ? (size_t) std::atoll(std::getenv("DS4_TL_FROM")) : 0;
|
||||
struct Acc { double busy = 0, gap = 0, gap_big = 0; size_t n = 0, n_big = 0; };
|
||||
std::map<std::string, Acc> acc;
|
||||
double t_first = 0, t_last = 0, total_busy = 0, total_gap = 0, total_big = 0;
|
||||
double prev_end = -1, ovl_us = 0;
|
||||
size_t used = 0, n_ovl = 0;
|
||||
for (size_t k = from; k < n; ++k) {
|
||||
if (g_tl.names[k].empty()) continue;
|
||||
const double st = (double) ts[2 * k].timestamp * ns_per_gpu * 1e-3, en = (double) ts[2 * k + 1].timestamp * ns_per_gpu * 1e-3; // us
|
||||
if (!(en >= st)) continue;
|
||||
if (prev_end < 0) t_first = st;
|
||||
Acc& a = acc[g_tl.names[k]];
|
||||
a.busy += en - st;
|
||||
++a.n;
|
||||
total_busy += en - st;
|
||||
if (prev_end >= 0 && st < prev_end) { ++n_ovl; ovl_us += prev_end - st; }
|
||||
if (prev_end >= 0 && st >= prev_end) {
|
||||
const double gp = st - prev_end;
|
||||
a.gap += gp;
|
||||
total_gap += gp;
|
||||
if (gp > 300.0) { a.gap_big += gp; ++a.n_big; total_big += gp; }
|
||||
}
|
||||
prev_end = en;
|
||||
t_last = en;
|
||||
++used;
|
||||
}
|
||||
std::vector<std::pair<double, std::string>> v;
|
||||
for (auto& kv : acc) v.emplace_back(kv.second.busy + kv.second.gap, kv.first);
|
||||
std::sort(v.begin(), v.end(), [](auto& x, auto& y) { return x.first > y.first; });
|
||||
std::fprintf(stderr, "timeline: %zu dispatches, wall %.1f ms, GPU busy %.1f ms, idle gaps %.1f ms (of it > 300 us: %.1f ms)\n", used,
|
||||
(t_last - t_first) / 1000.0, total_busy / 1000.0, total_gap / 1000.0, total_big / 1000.0);
|
||||
std::fprintf(stderr, "timeline: %zu dispatches started before the previous one ended (%.1f ms overlapped)\n", n_ovl, ovl_us / 1000.0);
|
||||
std::fprintf(stderr, "timeline: kernel n busy ms us/disp gap ms gap/disp big-gap ms (n)\n");
|
||||
for (size_t i = 0; i < v.size() && i < 40; ++i) {
|
||||
const Acc& a = acc[v[i].second];
|
||||
std::fprintf(stderr, "timeline: %-28s %6zu %9.1f %8.1f %8.1f %8.1f %10.1f (%zu)\n", v[i].second.c_str(), a.n, a.busy / 1000.0, a.busy / (double) a.n,
|
||||
a.gap / 1000.0, a.gap / (double) a.n, a.gap_big / 1000.0, a.n_big);
|
||||
}
|
||||
}
|
||||
} // namespace
|
||||
|
||||
extern "C" void ds4_timeline_gate(int open) { g_tl.gate = open != 0; }
|
||||
|
||||
|
||||
// ---- ds4_busy_begin / ds4_busy_end: a one-threadgroup kernel on a second queue that spins on a shared flag while the host works
|
||||
// (the CPU lane), so the GPU is not idle between two bursts (idle -> the next kernels run 2-3x slower); the host sets the flag the
|
||||
// moment it is done and the kernel leaves within microseconds. DS4_BUSY=0 turns it off.
|
||||
namespace {
|
||||
struct Busy {
|
||||
bool init = false, ok = false;
|
||||
id<MTLCommandQueue> q = nil;
|
||||
id<MTLComputePipelineState> ps = nil;
|
||||
id<MTLBuffer> flag = nil;
|
||||
id<MTLCommandBuffer> last = nil;
|
||||
bool running = false;
|
||||
};
|
||||
Busy g_busy;
|
||||
} // namespace
|
||||
|
||||
extern "C" void ds4_busy_begin() {
|
||||
static const bool off = std::getenv("DS4_BUSY") && std::string(std::getenv("DS4_BUSY")) == "0";
|
||||
if (off) return;
|
||||
@autoreleasepool {
|
||||
if (!g_busy.init) {
|
||||
g_busy.init = true;
|
||||
id<MTLDevice> dev = MTLCreateSystemDefaultDevice();
|
||||
static const char* kSrc = R"MSL(
|
||||
#include <metal_stdlib>
|
||||
using namespace metal;
|
||||
kernel void busy_spin(device volatile atomic_uint* flag [[buffer(0)]], constant uint& cap [[buffer(1)]], uint t [[thread_position_in_grid]]) {
|
||||
uint x = t;
|
||||
float f = (float) t * 1e-9f;
|
||||
for (uint i = 0; i < cap; ++i) {
|
||||
for (uint j = 0; j < 64; ++j) x = x * 1664525u + 1013904223u;
|
||||
for (uint j = 0; j < 16; ++j) f = fma(f, 1.0000001f, 1e-7f); // (a real FP chain: keeps the ALU clock up)
|
||||
x ^= as_type<uint>(f);
|
||||
if (atomic_load_explicit(flag, memory_order_relaxed) != 0u) break;
|
||||
}
|
||||
if (x == 0xdeadbeefu) atomic_store_explicit(flag, 2u, memory_order_relaxed);
|
||||
}
|
||||
)MSL";
|
||||
NSError* err = nil;
|
||||
id<MTLLibrary> lib = [dev newLibraryWithSource:[NSString stringWithUTF8String:kSrc] options:nil error:&err];
|
||||
if (lib) g_busy.ps = [dev newComputePipelineStateWithFunction:[lib newFunctionWithName:@"busy_spin"] error:&err];
|
||||
if (g_busy.ps) {
|
||||
g_busy.q = [dev newCommandQueue];
|
||||
g_busy.flag = [dev newBufferWithLength:64 options:MTLResourceStorageModeShared];
|
||||
g_busy.ok = true;
|
||||
}
|
||||
}
|
||||
if (!g_busy.ok || g_busy.running) return;
|
||||
g_busy.running = true;
|
||||
if (g_busy.last != nil) [g_busy.last waitUntilCompleted]; // (the previous spin has left)
|
||||
*(volatile uint32_t*) [g_busy.flag contents] = 0;
|
||||
uint32_t cap = std::getenv("DS4_BUSY_CAP") ? (uint32_t) std::atoi(std::getenv("DS4_BUSY_CAP")) : 20000u; // ~ tens of ms at most
|
||||
// the spin's width: one 256-thread threadgroup leaves most of the GPU idle and the clock drops anyway; DS4_BUSY_W
|
||||
// threadgroups (256 threads each) keep more of the card's ALUs fed, which is what holds the performance state.
|
||||
static const uint32_t width = std::getenv("DS4_BUSY_W") ? (uint32_t) std::max(1, std::atoi(std::getenv("DS4_BUSY_W"))) : 1u;
|
||||
id<MTLCommandBuffer> cb = [g_busy.q commandBuffer];
|
||||
id<MTLComputeCommandEncoder> enc = [cb computeCommandEncoder];
|
||||
[enc setComputePipelineState:g_busy.ps];
|
||||
[enc setBuffer:g_busy.flag offset:0 atIndex:0];
|
||||
[enc setBytes:&cap length:4 atIndex:1];
|
||||
[enc dispatchThreads:MTLSizeMake(256 * width, 1, 1) threadsPerThreadgroup:MTLSizeMake(256, 1, 1)];
|
||||
[enc endEncoding];
|
||||
[cb commit];
|
||||
g_busy.last = cb;
|
||||
}
|
||||
}
|
||||
extern "C" void ds4_busy_end() {
|
||||
if (!g_busy.ok || g_busy.last == nil || !g_busy.running) return;
|
||||
*(volatile uint32_t*) [g_busy.flag contents] = 1;
|
||||
g_busy.running = false;
|
||||
}
|
||||
|
||||
#endif
|
||||
Context::Context() : impl_(new Impl) {
|
||||
impl_->dev = MTLCreateSystemDefaultDevice();
|
||||
#if defined(STRATA_DS4_RUNTIME)
|
||||
if (std::getenv("DS4_TIMELINE") && !g_tl.reg) {
|
||||
g_tl.on = true;
|
||||
g_tl.reg = true;
|
||||
static id<MTLDevice> tl_dev = impl_->dev;
|
||||
std::atexit([] { tl_report(tl_dev); });
|
||||
}
|
||||
#endif
|
||||
if (!impl_->dev) {
|
||||
std::fprintf(stderr, "mtl: no Metal device\n");
|
||||
std::exit(2);
|
||||
}
|
||||
impl_->q = [impl_->dev newCommandQueue];
|
||||
#if defined(STRATA_DS4_RUNTIME)
|
||||
if (ds4_gpu_residency_set && !std::getenv("DS4_NO_RESIDENCY")) {
|
||||
id<MTLResidencySet> rs = (__bridge id<MTLResidencySet>)ds4_gpu_residency_set();
|
||||
if (rs) [impl_->q addResidencySet:rs];
|
||||
}
|
||||
#endif
|
||||
// env, then <exe dir>/metal/ (a component package), then the compiled-in source-tree path
|
||||
ggml_common_path = strata_metal::resource("STRATA_GGML_COMMON", "ggml-common.h", STRATA_GGML_COMMON_DEFAULT);
|
||||
kernel_dir = strata_metal::resource("STRATA_METAL_KERNELS", "kernels", STRATA_METAL_KERNELS_DEFAULT);
|
||||
@@ -105,7 +365,116 @@ int Context::compile(const std::vector<std::string>& files, const std::string& p
|
||||
return (int)impl_->libs.size() - 1;
|
||||
}
|
||||
|
||||
#if defined(STRATA_DS4_RUNTIME)
|
||||
|
||||
// ---- the keep-alive spin (see mtl_rt.h)
|
||||
static std::atomic<int64_t> g_last_act_ns{0};
|
||||
static inline int64_t ka_now_ns() { return std::chrono::duration_cast<std::chrono::nanoseconds>(std::chrono::steady_clock::now().time_since_epoch()).count(); }
|
||||
static std::atomic<bool> g_ka_on{false};
|
||||
|
||||
void Context::start_keepalive() {
|
||||
if (!std::getenv("DS4_KEEPALIVE") || g_ka_on.exchange(true)) return; // (measured slower in the decode loop)
|
||||
id<MTLDevice> dev = impl_->dev;
|
||||
std::thread([dev]() {
|
||||
@autoreleasepool {
|
||||
static const char* kSpin = R"MSL(
|
||||
#include <metal_stdlib>
|
||||
using namespace metal;
|
||||
kernel void ka_spin(device atomic_uint* c [[buffer(0)]], constant uint& n [[buffer(1)]], uint t [[thread_position_in_grid]]) {
|
||||
uint x = t;
|
||||
for (uint i = 0; i < n; ++i) x = x * 1664525u + 1013904223u;
|
||||
if (x == 0xdeadbeefu) atomic_fetch_add_explicit(c, 1u, memory_order_relaxed);
|
||||
}
|
||||
)MSL";
|
||||
NSError* err = nil;
|
||||
id<MTLLibrary> lib = [dev newLibraryWithSource:[NSString stringWithUTF8String:kSpin] options:nil error:&err];
|
||||
if (!lib) return;
|
||||
id<MTLComputePipelineState> ps = [dev newComputePipelineStateWithFunction:[lib newFunctionWithName:@"ka_spin"] error:&err];
|
||||
if (!ps) return;
|
||||
id<MTLCommandQueue> q = [dev newCommandQueue];
|
||||
id<MTLBuffer> cbuf = [dev newBufferWithLength:64 options:MTLResourceStorageModeShared];
|
||||
uint32_t iters = std::getenv("DS4_KEEPALIVE_ITERS") ? (uint32_t) std::atoi(std::getenv("DS4_KEEPALIVE_ITERS")) : 20000u;
|
||||
for (;;) {
|
||||
if (ka_now_ns() - g_last_act_ns.load(std::memory_order_relaxed) > 50000000LL) {
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(2));
|
||||
continue;
|
||||
}
|
||||
@autoreleasepool {
|
||||
id<MTLCommandBuffer> cb = [q commandBuffer];
|
||||
id<MTLComputeCommandEncoder> enc = [cb computeCommandEncoder];
|
||||
[enc setComputePipelineState:ps];
|
||||
[enc setBuffer:cbuf offset:0 atIndex:0];
|
||||
[enc setBytes:&iters length:4 atIndex:1];
|
||||
[enc dispatchThreads:MTLSizeMake(256, 1, 1) threadsPerThreadgroup:MTLSizeMake(256, 1, 1)];
|
||||
[enc endEncoding];
|
||||
[cb commit];
|
||||
[cb waitUntilCompleted];
|
||||
}
|
||||
}
|
||||
}
|
||||
}).detach();
|
||||
}
|
||||
|
||||
|
||||
// ---- zero-copy weights (see mtl_rt.h)
|
||||
namespace {
|
||||
struct ZcBuf { size_t len; void* handle; uint64_t addr; };
|
||||
std::map<uintptr_t, ZcBuf> g_zc;
|
||||
std::mutex g_zc_mu;
|
||||
} // namespace
|
||||
|
||||
uint64_t Context::zc_map(const void* p, size_t bytes) {
|
||||
const size_t pg = 16384;
|
||||
const uintptr_t lo = (uintptr_t) p & ~(uintptr_t) (pg - 1);
|
||||
const uintptr_t hi = ((uintptr_t) p + bytes + pg - 1) & ~(uintptr_t) (pg - 1);
|
||||
std::lock_guard<std::mutex> lk(g_zc_mu);
|
||||
auto it = g_zc.upper_bound(lo);
|
||||
if (it != g_zc.begin()) {
|
||||
--it;
|
||||
if (it->first <= lo && hi <= it->first + it->second.len) return it->second.addr + ((uintptr_t) p - it->first);
|
||||
}
|
||||
static const size_t cap = std::getenv("DS4_ZC_MAX") ? (size_t) std::atoll(std::getenv("DS4_ZC_MAX")) : 9000;
|
||||
if (g_zc.size() >= cap) return 0;
|
||||
id<MTLBuffer> b = [impl_->dev newBufferWithBytesNoCopy:(void*) lo length:hi - lo options:MTLResourceStorageModeShared deallocator:nil];
|
||||
if (!b) return 0;
|
||||
if (ds4_gpu_residency_set) {
|
||||
id<MTLResidencySet> rs = (__bridge id<MTLResidencySet>)ds4_gpu_residency_set();
|
||||
if (rs) {
|
||||
[rs addAllocation:b];
|
||||
[rs commit];
|
||||
}
|
||||
}
|
||||
ZcBuf z{hi - lo, (__bridge_retained void*) b, (uint64_t) [b gpuAddress]};
|
||||
g_zc[lo] = z;
|
||||
return z.addr + ((uintptr_t) p - lo);
|
||||
}
|
||||
size_t Context::zc_count() const { std::lock_guard<std::mutex> lk(g_zc_mu); return g_zc.size(); }
|
||||
|
||||
#endif
|
||||
Pipeline Context::pipeline(int lib, const std::string& fn, const Constants& c) {
|
||||
#if defined(STRATA_DS4_RUNTIME)
|
||||
// (cached: the host helpers ask for the same pipeline a dispatch at a time; newComputePipelineState costs ~0.3 ms and
|
||||
// [MTLDevice name] reads the preferences - the prompt path's expert copies (3 a streamed expert) spent seconds there)
|
||||
static std::mutex cache_mu;
|
||||
static std::map<std::string, Pipeline> cache;
|
||||
std::string key = std::to_string(lib) + "|" + fn;
|
||||
for (const auto& v : c.vals) key += "|" + std::to_string(v.index) + ":" + std::to_string(v.type) + ":" + std::to_string(v.v);
|
||||
key += "|" + std::to_string((uintptr_t) this);
|
||||
{
|
||||
std::lock_guard<std::mutex> lk(cache_mu);
|
||||
auto it = cache.find(key);
|
||||
if (it != cache.end()) return it->second;
|
||||
}
|
||||
Pipeline made = pipeline_uncached(lib, fn, c);
|
||||
{
|
||||
std::lock_guard<std::mutex> lk(cache_mu);
|
||||
cache[key] = made;
|
||||
}
|
||||
return made;
|
||||
}
|
||||
|
||||
Pipeline Context::pipeline_uncached(int lib, const std::string& fn, const Constants& c) {
|
||||
#endif
|
||||
NSError* err = nil;
|
||||
id<MTLFunction> f;
|
||||
NSString* name = [NSString stringWithUTF8String:fn.c_str()];
|
||||
@@ -159,7 +528,11 @@ Pipeline Context::pipeline(int lib, const std::string& fn, const Constants& c) {
|
||||
p.name += (i ? "," : "") + std::to_string(c.vals[i].index) + "=" + std::to_string((int)c.vals[i].v);
|
||||
p.name += "}";
|
||||
}
|
||||
#if defined(STRATA_DS4_RUNTIME)
|
||||
if (g_device_name == "unknown GPU") g_device_name = [[impl_->dev name] UTF8String];
|
||||
#else
|
||||
g_device_name = [[impl_->dev name] UTF8String];
|
||||
#endif
|
||||
return p;
|
||||
}
|
||||
|
||||
@@ -176,6 +549,26 @@ void require_fit(const Pipeline& p, uint32_t threads, const char* where) {
|
||||
}
|
||||
|
||||
Buffer Context::alloc(size_t bytes) {
|
||||
#if defined(STRATA_DS4_RUNTIME)
|
||||
// Metal returns the new buffer with one more reference parked in the calling thread's autorelease pool. The engine's host
|
||||
// threads have no pool (only Context::run opens one), so that reference was never given back: every buffer made here lived for
|
||||
// the rest of the process, free() or not (a 16 KB job array per expert matvec, ~90K a request: +1.3 GB and a slower queue every
|
||||
// call, 16 -> 3 tok/s after a few requests on a big Mac).
|
||||
Buffer r;
|
||||
@autoreleasepool {
|
||||
id<MTLBuffer> b = [impl_->dev newBufferWithLength:bytes options:MTLResourceStorageModeShared];
|
||||
if (!b) {
|
||||
std::fprintf(stderr, "mtl: alloc %zu failed\n", bytes);
|
||||
std::exit(4);
|
||||
}
|
||||
std::memset([b contents], 0, bytes);
|
||||
r.handle = (__bridge_retained void*)b;
|
||||
r.ptr = [b contents];
|
||||
}
|
||||
allocated_ += bytes;
|
||||
r.bytes = bytes;
|
||||
return r;
|
||||
#else
|
||||
id<MTLBuffer> b = [impl_->dev newBufferWithLength:bytes options:MTLResourceStorageModeShared];
|
||||
if (!b) {
|
||||
std::fprintf(stderr, "mtl: alloc %zu failed\n", bytes);
|
||||
@@ -188,6 +581,7 @@ Buffer Context::alloc(size_t bytes) {
|
||||
r.ptr = [b contents];
|
||||
r.bytes = bytes;
|
||||
return r;
|
||||
#endif
|
||||
}
|
||||
|
||||
Buffer Context::wrap(void* p, size_t bytes, size_t max_span) {
|
||||
@@ -222,7 +616,47 @@ Buffer Context::wrap(void* p, size_t bytes, size_t max_span) {
|
||||
return r;
|
||||
}
|
||||
|
||||
#if defined(STRATA_DS4_RUNTIME)
|
||||
// The runtime's own MTLBuffer under a CPU address (strata::gpu's allocations), when [p, p + bytes) lies inside one. Binding it
|
||||
// - the same MTLBuffer, so the same GPU address, as every other kernel that touches this memory - instead of a fresh no-copy
|
||||
// buffer per call. (Each fresh wrapper is a second GPU mapping of the same pages: the quantiser read the activations through
|
||||
// one mapping while the norm that produced them had written through another, and saw the previous layer's.)
|
||||
extern "C" void* ds4_gpu_buffer_for_cpu(const void* cpu, size_t bytes, size_t* off) __attribute__((weak_import));
|
||||
extern "C" size_t ds4_gpu_alloc_remaining(const void* cpu) __attribute__((weak_import));
|
||||
#endif
|
||||
Buffer Context::wrap(void* p, size_t bytes) {
|
||||
#if defined(STRATA_DS4_RUNTIME)
|
||||
if (ds4_gpu_buffer_for_cpu && !std::getenv("DS4_WRAP_COPY")) {
|
||||
size_t off = 0;
|
||||
if (void* mb = ds4_gpu_buffer_for_cpu(p, bytes, &off)) {
|
||||
Buffer r;
|
||||
r.handle = (__bridge_retained void*)((__bridge id<MTLBuffer>)mb);
|
||||
r.ptr = p;
|
||||
r.bytes = bytes;
|
||||
r.bias = off;
|
||||
return r;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
#if defined(STRATA_DS4_RUNTIME)
|
||||
if (std::getenv("DS4_WRAP_LOG")) // debugging: a wrap that is not inside one runtime allocation gets a second GPU mapping of the pages
|
||||
std::fprintf(stderr, "mtl: wrap FALLBACK p=%p bytes=%zu alloc_remaining=%zu\n", p, bytes, ds4_gpu_alloc_remaining ? ds4_gpu_alloc_remaining(p) : (size_t) 0);
|
||||
#endif
|
||||
#if defined(STRATA_DS4_RUNTIME)
|
||||
Buffer r;
|
||||
@autoreleasepool { // (see alloc: the engine's host threads have no pool of their own)
|
||||
id<MTLBuffer> b = [impl_->dev newBufferWithBytesNoCopy:p length:bytes options:MTLResourceStorageModeShared deallocator:nil];
|
||||
if (!b) {
|
||||
std::fprintf(stderr, "mtl: wrap %zu failed\n", bytes);
|
||||
std::exit(4);
|
||||
}
|
||||
r.handle = (__bridge_retained void*)b;
|
||||
}
|
||||
allocated_ += bytes;
|
||||
r.ptr = p;
|
||||
r.bytes = bytes;
|
||||
return r;
|
||||
#else
|
||||
id<MTLBuffer> b = [impl_->dev newBufferWithBytesNoCopy:p length:bytes options:MTLResourceStorageModeShared deallocator:nil];
|
||||
if (!b) {
|
||||
std::fprintf(stderr, "mtl: wrap %zu failed\n", bytes);
|
||||
@@ -234,34 +668,75 @@ Buffer Context::wrap(void* p, size_t bytes) {
|
||||
r.ptr = p;
|
||||
r.bytes = bytes;
|
||||
return r;
|
||||
#endif
|
||||
}
|
||||
|
||||
void Context::free(Buffer& b) {
|
||||
#if defined(STRATA_DS4_RUNTIME)
|
||||
// The deferred command buffer is made WITHOUT retained references: a buffer released here while a dispatch that names it
|
||||
// is still queued is read after it is gone (or after another alloc took its memory: the job arrays of iq_mv read the
|
||||
// NEXT call's jobs, measured as rows of zeros and different tokens every run). Hold it until the buffer has completed.
|
||||
std::lock_guard<std::recursive_mutex> lk_(impl_->mu);
|
||||
const bool hold = impl_->cb_def != nil || impl_->inflight != nil;
|
||||
#endif
|
||||
if (b.chunks) {
|
||||
#if defined(STRATA_DS4_RUNTIME)
|
||||
for (void* h : *b.chunks) {
|
||||
if (hold) impl_->graveyard.push_back(h);
|
||||
else { id<MTLBuffer> x = (__bridge_transfer id<MTLBuffer>)h; x = nil; }
|
||||
}
|
||||
#else
|
||||
for (void* h : *b.chunks) { id<MTLBuffer> x = (__bridge_transfer id<MTLBuffer>)h; x = nil; }
|
||||
#endif
|
||||
b = Buffer{};
|
||||
return;
|
||||
}
|
||||
if (b.handle) {
|
||||
#if defined(STRATA_DS4_RUNTIME)
|
||||
if (hold) impl_->graveyard.push_back(b.handle);
|
||||
else { id<MTLBuffer> x = (__bridge_transfer id<MTLBuffer>)b.handle; x = nil; }
|
||||
#else
|
||||
id<MTLBuffer> x = (__bridge_transfer id<MTLBuffer>)b.handle;
|
||||
x = nil;
|
||||
#endif
|
||||
}
|
||||
b = Buffer{};
|
||||
}
|
||||
|
||||
#if defined(STRATA_DS4_RUNTIME)
|
||||
static std::mutex g_hist_mu;
|
||||
static std::unordered_map<void*, std::string> g_pipe_names;
|
||||
static std::unordered_map<std::string, uint64_t> g_kernel_counts;
|
||||
// DS4_ENC_TIMES: where a dispatch's encoding time goes - the pipeline state, the arguments, the dispatch call
|
||||
static std::atomic<uint64_t> g_t_pipe{0}, g_t_args{0}, g_t_disp{0};
|
||||
#endif
|
||||
static void encode(id<MTLComputeCommandEncoder> enc, const Dispatch& d) {
|
||||
require_fit(d, "dispatch");
|
||||
#if defined(STRATA_DS4_RUNTIME)
|
||||
static const bool enc_times = std::getenv("DS4_ENC_TIMES") != nullptr;
|
||||
const auto t_a = std::chrono::steady_clock::now();
|
||||
#endif
|
||||
[enc setComputePipelineState:P(d.p)];
|
||||
#if defined(STRATA_DS4_RUNTIME)
|
||||
const auto t_b = std::chrono::steady_clock::now();
|
||||
#endif
|
||||
for (size_t i = 0; i < d.args.size(); ++i) {
|
||||
const Arg& a = d.args[i];
|
||||
if (a.buf) {
|
||||
size_t o;
|
||||
id<MTLBuffer> mb = BO(*a.buf, a.off, o);
|
||||
#if defined(STRATA_DS4_RUNTIME)
|
||||
static const bool chkbuf = std::getenv("DS4_CHECK_BUF") != nullptr;
|
||||
if (chkbuf && o >= [mb length]) std::fprintf(stderr, "mtl: %s arg %zu: offset %zu past the buffer (%zu bytes; Buffer.bytes %zu bias %zu off %zu)\n", d.p->name.c_str(), i, o, (size_t) [mb length], a.buf->bytes, a.buf->bias, (size_t) a.off);
|
||||
#endif
|
||||
[enc setBuffer:mb offset:o atIndex:i];
|
||||
}
|
||||
else
|
||||
[enc setBytes:a.bytes.data() length:a.bytes.size() atIndex:i];
|
||||
}
|
||||
#if defined(STRATA_DS4_RUNTIME)
|
||||
const auto t_c = std::chrono::steady_clock::now();
|
||||
#endif
|
||||
if (d.tg_mem) [enc setThreadgroupMemoryLength:(d.tg_mem + 15) / 16 * 16 atIndex:0];
|
||||
if (d.indirect)
|
||||
[enc dispatchThreadgroupsWithIndirectBuffer:B(*d.indirect)
|
||||
@@ -270,16 +745,232 @@ static void encode(id<MTLComputeCommandEncoder> enc, const Dispatch& d) {
|
||||
else
|
||||
[enc dispatchThreadgroups:MTLSizeMake(d.grid[0], d.grid[1], d.grid[2])
|
||||
threadsPerThreadgroup:MTLSizeMake(d.tg[0], d.tg[1], d.tg[2])];
|
||||
#if defined(STRATA_DS4_RUNTIME)
|
||||
if (enc_times) {
|
||||
const auto t_d = std::chrono::steady_clock::now();
|
||||
g_t_pipe.fetch_add((uint64_t) std::chrono::duration_cast<std::chrono::nanoseconds>(t_b - t_a).count(), std::memory_order_relaxed);
|
||||
g_t_args.fetch_add((uint64_t) std::chrono::duration_cast<std::chrono::nanoseconds>(t_c - t_b).count(), std::memory_order_relaxed);
|
||||
g_t_disp.fetch_add((uint64_t) std::chrono::duration_cast<std::chrono::nanoseconds>(t_d - t_c).count(), std::memory_order_relaxed);
|
||||
}
|
||||
}
|
||||
|
||||
static std::deque<std::string> g_recent; // the last dispatches encoded (printed with a command buffer failure)
|
||||
static std::string recent_str() { std::string r; for (auto& n : g_recent) r += n + " "; return r; }
|
||||
static double g_gpu_ms = 0.0;
|
||||
static std::atomic<uint64_t> g_gpu_busy_us{0}; // DS4_GPU_TOTAL: the GPU time of every committed command buffer
|
||||
static void add_busy_handler(id<MTLCommandBuffer> cb) {
|
||||
static const bool on = std::getenv("DS4_GPU_TOTAL") != nullptr;
|
||||
if (on) [cb addCompletedHandler:^(id<MTLCommandBuffer> c) { const double d = c.GPUEndTime - c.GPUStartTime; if (d > 0) g_gpu_busy_us += (uint64_t) (d * 1e6); }];
|
||||
} // GPU time of every committed command buffer, summed
|
||||
static std::atomic<uint64_t> g_wait_ns{0}, g_enc_ns{0}, g_wait_n{0}; // host time: waiting on command buffers; encoding dispatches
|
||||
static std::mutex g_prof_mu;
|
||||
static std::unordered_map<std::string, std::pair<double, uint64_t>> g_prof; // DS4_PROF: kernel -> (GPU us, dispatches)
|
||||
static std::string g_prof_last;
|
||||
static std::atomic<uint64_t> g_cb_count{0};
|
||||
static bool g_count_reg = false;
|
||||
static void ds4_report_counts() {
|
||||
if (g_gpu_busy_us.load()) std::fprintf(stderr, "mtl: GPU busy (every command buffer) %.1f ms\n", g_gpu_busy_us.load() / 1000.0);
|
||||
std::fprintf(stderr, "mtl: %.1f ms GPU, %llu dispatches, %llu command buffers\n", g_gpu_ms,
|
||||
(unsigned long long) g_disp_count.load(), (unsigned long long) g_cb_count.load());
|
||||
std::fprintf(stderr, "mtl: host waited %.1f ms for the GPU in %llu waits; encoding the dispatches took %.1f ms\n", g_wait_ns.load() / 1e6, (unsigned long long) g_wait_n.load(), g_enc_ns.load() / 1e6);
|
||||
if (g_t_pipe.load() || g_t_args.load() || g_t_disp.load())
|
||||
std::fprintf(stderr, "mtl: of the encoding: pipeline %.1f ms, arguments %.1f ms, dispatch %.1f ms\n",
|
||||
g_t_pipe.load() / 1e6, g_t_args.load() / 1e6, g_t_disp.load() / 1e6);
|
||||
if (!g_prof.empty()) {
|
||||
std::vector<std::pair<double, std::string>> pv;
|
||||
double tot = 0;
|
||||
for (auto& kv : g_prof) { pv.emplace_back(kv.second.first, kv.first); tot += kv.second.first; }
|
||||
std::sort(pv.begin(), pv.end(), [](auto& a, auto& b) { return a.first > b.first; });
|
||||
std::fprintf(stderr, "prof: kernel GPU ms n us/dispatch (total %.1f ms)\n", tot / 1000.0);
|
||||
for (size_t i = 0; i < pv.size() && i < 30; ++i) {
|
||||
auto& e = g_prof[pv[i].second];
|
||||
std::fprintf(stderr, "prof: %-30s %8.1f %6llu %8.1f\n", pv[i].second.c_str(), e.first / 1000.0, (unsigned long long) e.second, e.first / (double) e.second);
|
||||
}
|
||||
}
|
||||
std::vector<std::pair<uint64_t, std::string>> v;
|
||||
{ std::lock_guard<std::mutex> lk(g_hist_mu); for (auto& kv : g_kernel_counts) v.emplace_back(kv.second, kv.first); }
|
||||
std::sort(v.begin(), v.end(), [](auto& a, auto& b) { return a.first > b.first; });
|
||||
for (size_t i = 0; i < v.size() && i < 14; ++i)
|
||||
std::fprintf(stderr, " %6llu %s\n", (unsigned long long) v[i].first, v[i].second.c_str());
|
||||
}
|
||||
|
||||
extern "C" unsigned long long ds4_dispatch_seq() { return g_disp_count.load(); }
|
||||
|
||||
extern "C" void ds4_gpu_sync_all_streams_weak() __asm("_ds4_gpu_sync_all_streams") __attribute__((weak_import));
|
||||
void Context::split_runs() {
|
||||
// (the streams' pending copies come first, as in a full flush: they are queued on another MTLCommandQueue and would
|
||||
// otherwise not have run when the commands committed here read what they write)
|
||||
if (ds4_gpu_sync_all_streams_weak) ds4_gpu_sync_all_streams_weak();
|
||||
// Commit the batched dispatches as their own command buffer WITHOUT waiting: the queue runs buffers in order and makes each
|
||||
// one's writes visible to the next. (Measured: a producer kernel's output was read stale by the next dispatch of the same
|
||||
// buffer for two kernels in a layer; ending the buffer after them is enough, no wait needed.)
|
||||
std::lock_guard<std::recursive_mutex> lk_(impl_->mu);
|
||||
if (impl_->cb_def == nil) return;
|
||||
++g_cb_count;
|
||||
[impl_->enc_def endEncoding];
|
||||
if (impl_->qev == nil) impl_->qev = [impl_->dev newSharedEvent];
|
||||
[impl_->cb_def encodeSignalEvent:impl_->qev value:++impl_->qev_v];
|
||||
add_busy_handler(impl_->cb_def);
|
||||
[impl_->cb_def commit];
|
||||
impl_->inflight = impl_->cb_def;
|
||||
impl_->inflight_names = std::move(impl_->def_names); impl_->def_names.clear();
|
||||
impl_->cb_def = nil;
|
||||
impl_->enc_def = nil;
|
||||
impl_->def_n = 0;
|
||||
}
|
||||
|
||||
// Waiting for a committed command buffer: spin on its status for a while (the thread wake of waitUntilCompleted costs
|
||||
// ~1 ms a wait; the decode token waits ~100 times), then block. DS4_NO_SPIN=1: always block.
|
||||
static void wait_spin(id<MTLCommandBuffer> cb) {
|
||||
static const bool no_spin = std::getenv("DS4_NO_SPIN") != nullptr;
|
||||
static const double budget = std::getenv("DS4_SPIN_US") ? std::atof(std::getenv("DS4_SPIN_US")) * 1e-6 : 20e-3;
|
||||
if (!no_spin) {
|
||||
const auto t0 = std::chrono::steady_clock::now();
|
||||
while (cb.status < MTLCommandBufferStatusCompleted) {
|
||||
if (std::chrono::duration<double>(std::chrono::steady_clock::now() - t0).count() > budget) break;
|
||||
}
|
||||
}
|
||||
if (cb.status < MTLCommandBufferStatusCompleted) [cb waitUntilCompleted];
|
||||
}
|
||||
|
||||
void* Context::mark() {
|
||||
std::lock_guard<std::recursive_mutex> lk_(impl_->mu);
|
||||
if (impl_->cb_def != nil) split_runs(); // (the streams' pending work goes first, inside)
|
||||
if (impl_->inflight == nil) return nullptr;
|
||||
return (__bridge_retained void*) impl_->inflight;
|
||||
}
|
||||
|
||||
void Context::drop_mark(void* token) {
|
||||
// The token holds a +1 on a committed command buffer; one nobody waits for is never freed.
|
||||
if (token) { id<MTLCommandBuffer> cb = (__bridge_transfer id<MTLCommandBuffer>) token; cb = nil; }
|
||||
}
|
||||
|
||||
void Context::wait_mark(void* token) {
|
||||
id<MTLCommandBuffer> cb = (__bridge_transfer id<MTLCommandBuffer>) token;
|
||||
const auto tw0 = std::chrono::steady_clock::now();
|
||||
wait_spin(cb);
|
||||
g_wait_ns += (uint64_t) std::chrono::duration_cast<std::chrono::nanoseconds>(std::chrono::steady_clock::now() - tw0).count();
|
||||
++g_wait_n;
|
||||
if (cb.status != MTLCommandBufferStatusCompleted)
|
||||
std::fprintf(stderr, "mtl: deferred command buffer failed: %s\n", [[cb.error localizedDescription] UTF8String]);
|
||||
if (cb.status != MTLCommandBufferStatusCompleted && std::getenv("DS4_ERRINFO")) std::fprintf(stderr, "mtl: recent: %s\n", recent_str().c_str()), std::fprintf(stderr, "mtl: info: %s\n", [[[cb.error userInfo] description] UTF8String]);
|
||||
}
|
||||
|
||||
void Context::flush_runs() {
|
||||
if (std::getenv("DS4_TRACE_DISP") && impl_->cb_def != nil) std::fprintf(stderr, "TRACE flush_runs (%d queued)\n", impl_->def_n);
|
||||
static const bool cb_log = std::getenv("DS4_CB_LOG") != nullptr; // debugging: one line a command buffer (what it held)
|
||||
if (cb_log && impl_->cb_def != nil) {
|
||||
std::string first, last;
|
||||
if (!impl_->def_names.empty()) {
|
||||
size_t sp = impl_->def_names.find(' ');
|
||||
first = impl_->def_names.substr(0, sp);
|
||||
size_t e = impl_->def_names.find_last_not_of(' ');
|
||||
size_t b = impl_->def_names.find_last_of(' ', e);
|
||||
last = impl_->def_names.substr(b == std::string::npos ? 0 : b + 1, e - (b == std::string::npos ? 0 : b));
|
||||
}
|
||||
std::fprintf(stderr, "CB %d disp first=%-24s last=%s\n", impl_->def_n, first.c_str(), last.c_str());
|
||||
}
|
||||
std::lock_guard<std::recursive_mutex> lk_(impl_->mu);
|
||||
if (!g_count_reg) { g_count_reg = true; std::atexit(ds4_report_counts); }
|
||||
if (impl_->cb_def != nil) {
|
||||
++g_cb_count;
|
||||
[impl_->enc_def endEncoding];
|
||||
if (impl_->qev == nil) impl_->qev = [impl_->dev newSharedEvent];
|
||||
[impl_->cb_def encodeSignalEvent:impl_->qev value:++impl_->qev_v];
|
||||
add_busy_handler(impl_->cb_def);
|
||||
[impl_->cb_def commit];
|
||||
impl_->inflight = impl_->cb_def;
|
||||
impl_->inflight_names = std::move(impl_->def_names); impl_->def_names.clear();
|
||||
impl_->cb_def = nil;
|
||||
impl_->enc_def = nil;
|
||||
impl_->def_n = 0;
|
||||
}
|
||||
if (impl_->inflight != nil) {
|
||||
if (std::getenv("DS4_TRACE_WAIT")) { void* fr[8]; const int nf = backtrace(fr, 8); std::fprintf(stderr, "WAIT flush_runs:\n"); backtrace_symbols_fd(fr + 1, nf - 1, 2); }
|
||||
const auto tw0 = std::chrono::steady_clock::now();
|
||||
wait_spin(impl_->inflight);
|
||||
g_wait_ns += (uint64_t) std::chrono::duration_cast<std::chrono::nanoseconds>(std::chrono::steady_clock::now() - tw0).count();
|
||||
++g_wait_n;
|
||||
{ const double d = impl_->inflight.GPUEndTime - impl_->inflight.GPUStartTime; if (d > 0) g_gpu_ms += d * 1000.0;
|
||||
if (!g_prof_last.empty()) { std::lock_guard<std::mutex> pl(g_prof_mu); auto& e = g_prof[g_prof_last]; e.first += d * 1e6; e.second += 1; g_prof_last.clear(); } }
|
||||
if (impl_->inflight.status != MTLCommandBufferStatusCompleted)
|
||||
std::fprintf(stderr, "mtl: deferred command buffer failed: %s\n", [[impl_->inflight.error localizedDescription] UTF8String]);
|
||||
if (impl_->inflight.status != MTLCommandBufferStatusCompleted && std::getenv("DS4_ERRINFO")) std::fprintf(stderr, "mtl: failed cb held: %s\n", impl_->inflight_names.c_str());
|
||||
impl_->inflight = nil;
|
||||
}
|
||||
for (void* h : impl_->graveyard) { id<MTLBuffer> x = (__bridge_transfer id<MTLBuffer>)h; x = nil; }
|
||||
impl_->graveyard.clear();
|
||||
#endif
|
||||
}
|
||||
|
||||
double Context::run(const std::vector<Dispatch>& ds, int reps, bool concurrent, bool barrier_between) {
|
||||
#if defined(STRATA_DS4_RUNTIME)
|
||||
std::lock_guard<std::recursive_mutex> lk_(impl_->mu);
|
||||
#endif
|
||||
@autoreleasepool {
|
||||
#if defined(STRATA_DS4_RUNTIME)
|
||||
if (ds.size() == 1 && reps == 1 && !barrier_between) {
|
||||
{ static const std::string skipk = std::getenv("DS4_SKIP_K") ? std::string(",") + std::getenv("DS4_SKIP_K") + "," : std::string(); // debugging: drop these kernels (timing only)
|
||||
if (!skipk.empty() && skipk.find("," + ds[0].p->name + ",") != std::string::npos) return 0.0; }
|
||||
if (impl_->cb_def == nil || impl_->def_n >= 2000000) {
|
||||
if (impl_->cb_def != nil) split_runs(); // (a buffer committed earlier may still be running: the queue keeps the order)
|
||||
if (std::getenv("DS4_ERRINFO")) {
|
||||
MTLCommandBufferDescriptor* cd = [MTLCommandBufferDescriptor new];
|
||||
cd.retainedReferences = NO;
|
||||
cd.errorOptions = MTLCommandBufferErrorOptionEncoderExecutionStatus;
|
||||
impl_->cb_def = [impl_->q commandBufferWithDescriptor:cd];
|
||||
} else
|
||||
impl_->cb_def = [impl_->q commandBufferWithUnretainedReferences];
|
||||
// (the wait on our own event is only for a *different* queue: this queue already runs the previous buffer
|
||||
// before this one, and the wait costs a full GPU round trip at every buffer - 107 buffers a decode token,
|
||||
// 0.28 ms each. DS4_QEV_WAIT=1 restores it.)
|
||||
static const bool qev_wait = std::getenv("DS4_NO_QEV_WAIT") == nullptr; // (measured: off is slower - keep it)
|
||||
if (qev_wait && impl_->qev != nil && impl_->qev_v > 0) [impl_->cb_def encodeWaitForEvent:impl_->qev value:impl_->qev_v];
|
||||
impl_->enc_def = tl_make_encoder(impl_->dev, impl_->cb_def, std::getenv("DS4_CONCURRENT") ? MTLDispatchTypeConcurrent : MTLDispatchTypeSerial);
|
||||
}
|
||||
const auto te0 = std::chrono::steady_clock::now();
|
||||
encode(impl_->enc_def, ds[0]);
|
||||
if (g_tl.on && g_tl.att && g_tl.n < g_tl.cap) { g_tl.names[g_tl.n++] = ds[0].p->name; g_tl.att = false; }
|
||||
g_last_act_ns.store(ka_now_ns(), std::memory_order_relaxed);
|
||||
g_recent.push_back(ds[0].p->name); if (g_recent.size() > 8) g_recent.pop_front();
|
||||
if (std::getenv("DS4_ERRINFO")) impl_->def_names += ds[0].p->name + " ";
|
||||
g_enc_ns += (uint64_t) std::chrono::duration_cast<std::chrono::nanoseconds>(std::chrono::steady_clock::now() - te0).count();
|
||||
static const bool trace = std::getenv("DS4_TRACE_DISP") != nullptr;
|
||||
if (trace) { std::lock_guard<std::mutex> lk(g_hist_mu); std::fprintf(stderr, "TRACE disp %s\n", ds[0].p->name.c_str()); }
|
||||
// Every wrapper binds its own MTLBuffer over the same memory, so Metal's hazard tracking cannot see that one
|
||||
// dispatch reads what the previous wrote: without an explicit barrier the reader saw the stale value (measured: the
|
||||
// second matvec's activation quantiser read the previous layer's x).
|
||||
static const bool no_mb = std::getenv("DS4_NO_MBARRIER") != nullptr;
|
||||
if (!no_mb) [impl_->enc_def memoryBarrierWithScope:MTLBarrierScopeBuffers];
|
||||
static const bool enc_each = std::getenv("DS4_ENC_EACH") != nullptr || g_tl.on; // a new encoder per dispatch (debugging, the timeline)
|
||||
if (enc_each) {
|
||||
[impl_->enc_def endEncoding];
|
||||
impl_->enc_def = tl_make_encoder(impl_->dev, impl_->cb_def, (std::getenv("DS4_CONCURRENT") ? MTLDispatchTypeConcurrent : MTLDispatchTypeSerial));
|
||||
}
|
||||
++impl_->def_n;
|
||||
++g_disp_count;
|
||||
static const bool prof = std::getenv("DS4_PROF") != nullptr; // every dispatch alone, its GPU time kept per kernel
|
||||
if (prof) { g_prof_last = ds[0].p->name; if (ds4_gpu_sync_all_streams_weak) ds4_gpu_sync_all_streams_weak(); flush_runs(); return 0.0; }
|
||||
static const bool each = std::getenv("DS4_FLUSH_EACH") != nullptr; // debugging: every dispatch alone, in order
|
||||
static const std::string only = std::getenv("DS4_FLUSH_AFTER") ? std::getenv("DS4_FLUSH_AFTER") : ""; // ",name1,name2,"
|
||||
const bool listed = !only.empty() && only.find("," + ds[0].p->name + ",") != std::string::npos;
|
||||
if (each || listed) {
|
||||
if (ds4_gpu_sync_all_streams_weak) ds4_gpu_sync_all_streams_weak();
|
||||
flush_runs();
|
||||
}
|
||||
return 0.0;
|
||||
}
|
||||
flush_runs();
|
||||
#endif
|
||||
id<MTLCommandBuffer> cb = [impl_->q commandBufferWithUnretainedReferences];
|
||||
id<MTLComputeCommandEncoder> enc =
|
||||
[cb computeCommandEncoderWithDispatchType:concurrent ? MTLDispatchTypeConcurrent : MTLDispatchTypeSerial];
|
||||
for (int r = 0; r < reps; ++r)
|
||||
for (const auto& d : ds) {
|
||||
encode(enc, d);
|
||||
#if defined(STRATA_DS4_RUNTIME)
|
||||
++g_disp_count;
|
||||
#endif
|
||||
if (barrier_between) [enc memoryBarrierWithScope:MTLBarrierScopeBuffers];
|
||||
}
|
||||
[enc endEncoding];
|
||||
|
||||
@@ -0,0 +1,133 @@
|
||||
// bf16mv_probe.mm - the second dsv41 kernel in MSL: the BF16 matrix-vector product (ds4_gguf.cu:1043's mx_mv_kernel /
|
||||
// bf16_mv_kernel, single-job case).
|
||||
//
|
||||
// CUDA's layout: one warp per row, lane i handles k = i, i + 32, ... in BF16 weights times F32 activations, accumulated in
|
||||
// F32. This compiles the MSL version at run time, runs it on the GPU and compares against the same arithmetic on the CPU.
|
||||
// clang++ -O2 -fobjc-arc -framework Metal -framework Foundation -o bf16mv_probe bf16mv_probe.mm && ./bf16mv_probe
|
||||
#import <Foundation/Foundation.h>
|
||||
#import <Metal/Metal.h>
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <cmath>
|
||||
#include <cstring>
|
||||
#include <vector>
|
||||
#include <mach/mach_time.h>
|
||||
|
||||
static const char* kSrc = R"MSL(
|
||||
#include <metal_stdlib>
|
||||
using namespace metal;
|
||||
|
||||
// y[r] = sum_k bf16_to_float(w[r*k + kk]) * x[kk] (one warp a row)
|
||||
kernel void ds4_bf16_mv(device const ushort* w [[buffer(0)]], device const float* x [[buffer(1)]],
|
||||
device float* y [[buffer(2)]], constant uint& k [[buffer(3)]], constant uint& n [[buffer(4)]],
|
||||
uint gid [[thread_position_in_grid]], uint lane [[thread_index_in_threadgroup]]) {
|
||||
const uint rows_per_group = 1u; // one warp covers one row
|
||||
const uint r = gid / 32u * rows_per_group; // the row this warp owns
|
||||
if (r >= n) return;
|
||||
float acc = 0.0f;
|
||||
for (uint kk = lane; kk < k; kk += 32u) {
|
||||
const uint bits = uint(w[r * k + kk]) << 16; // BF16 -> F32 is the top half
|
||||
const float wf = as_type<float>(bits);
|
||||
acc += wf * x[kk];
|
||||
}
|
||||
// warp reduce
|
||||
acc += simd_shuffle_xor(acc, 16);
|
||||
acc += simd_shuffle_xor(acc, 8);
|
||||
acc += simd_shuffle_xor(acc, 4);
|
||||
acc += simd_shuffle_xor(acc, 2);
|
||||
acc += simd_shuffle_xor(acc, 1);
|
||||
if (lane == 0) y[r] = acc;
|
||||
}
|
||||
)MSL";
|
||||
|
||||
static float bf16_to_f32(unsigned short b) {
|
||||
unsigned bits = (unsigned) b << 16;
|
||||
float f;
|
||||
memcpy(&f, &bits, 4);
|
||||
return f;
|
||||
}
|
||||
static unsigned short f32_to_bf16(float f) {
|
||||
unsigned u;
|
||||
memcpy(&u, &f, 4);
|
||||
return (unsigned short) ((u + 0x7fffu + ((u >> 16) & 1u)) >> 16);
|
||||
}
|
||||
|
||||
int main() {
|
||||
@autoreleasepool {
|
||||
const int n = 2048, k = 5120;
|
||||
std::vector<unsigned short> w((size_t) n * k);
|
||||
std::vector<float> x(k), y(n);
|
||||
srand(11);
|
||||
for (size_t i = 0; i < w.size(); ++i) w[i] = f32_to_bf16(((rand() % 20001) - 10000) / 4096.0f);
|
||||
for (int i = 0; i < k; ++i) x[i] = ((rand() % 20001) - 10000) / 8192.0f;
|
||||
|
||||
id<MTLDevice> dev = MTLCreateSystemDefaultDevice();
|
||||
NSError* err = nil;
|
||||
id<MTLLibrary> lib = [dev newLibraryWithSource:[NSString stringWithUTF8String:kSrc] options:nil error:&err];
|
||||
if (!lib) { printf("MSL compile failed: %s\n", [[err localizedDescription] UTF8String]); return 1; }
|
||||
id<MTLComputePipelineState> pso = [dev newComputePipelineStateWithFunction:[lib newFunctionWithName:@"ds4_bf16_mv"] error:&err];
|
||||
if (!pso) { printf("pipeline failed: %s\n", [[err localizedDescription] UTF8String]); return 1; }
|
||||
|
||||
id<MTLBuffer> bw = [dev newBufferWithBytes:w.data() length:w.size() * 2 options:MTLResourceStorageModeShared];
|
||||
id<MTLBuffer> bx = [dev newBufferWithBytes:x.data() length:k * 4 options:MTLResourceStorageModeShared];
|
||||
id<MTLBuffer> by = [dev newBufferWithLength:n * 4 options:MTLResourceStorageModeShared];
|
||||
id<MTLCommandQueue> q = [dev newCommandQueue];
|
||||
id<MTLCommandBuffer> cb = [q commandBuffer];
|
||||
id<MTLComputeCommandEncoder> ce = [cb computeCommandEncoder];
|
||||
[ce setComputePipelineState:pso];
|
||||
[ce setBuffer:bw offset:0 atIndex:0];
|
||||
[ce setBuffer:bx offset:0 atIndex:1];
|
||||
[ce setBuffer:by offset:0 atIndex:2];
|
||||
unsigned uk = k, un = n;
|
||||
[ce setBytes:&uk length:4 atIndex:3];
|
||||
[ce setBytes:&un length:4 atIndex:4];
|
||||
[ce dispatchThreads:MTLSizeMake((NSUInteger) n * 32, 1, 1) threadsPerThreadgroup:MTLSizeMake(32, 1, 1)];
|
||||
[ce endEncoding];
|
||||
[cb commit];
|
||||
[cb waitUntilCompleted];
|
||||
if (cb.error) { printf("dispatch error: %s\n", [[cb.error localizedDescription] UTF8String]); return 1; }
|
||||
|
||||
// throughput: 10 more dispatches, best of them
|
||||
double best = 1e9;
|
||||
for (int it = 0; it < 10; ++it) {
|
||||
id<MTLCommandBuffer> c2 = [q commandBuffer];
|
||||
id<MTLComputeCommandEncoder> e2 = [c2 computeCommandEncoder];
|
||||
[e2 setComputePipelineState:pso];
|
||||
[e2 setBuffer:bw offset:0 atIndex:0];
|
||||
[e2 setBuffer:bx offset:0 atIndex:1];
|
||||
[e2 setBuffer:by offset:0 atIndex:2];
|
||||
[e2 setBytes:&uk length:4 atIndex:3];
|
||||
[e2 setBytes:&un length:4 atIndex:4];
|
||||
[e2 dispatchThreads:MTLSizeMake((NSUInteger) n * 32, 1, 1) threadsPerThreadgroup:MTLSizeMake(32, 1, 1)];
|
||||
[e2 endEncoding];
|
||||
mach_timebase_info_data_t tb; mach_timebase_info(&tb);
|
||||
const uint64_t t0 = mach_absolute_time();
|
||||
[c2 commit];
|
||||
[c2 waitUntilCompleted];
|
||||
const double dt = (double) (mach_absolute_time() - t0) * tb.numer / tb.denom / 1e9;
|
||||
if (dt < best) best = dt;
|
||||
}
|
||||
{
|
||||
const double bytes = (double) n * k * 2.0, flops = 2.0 * n * k;
|
||||
printf("throughput: %.2f GB/s, %.1f GFLOP/s (%.3f ms for 21.0 MB)\n", bytes / best / 1e9,
|
||||
flops / best / 1e9, best * 1e3);
|
||||
}
|
||||
const float* gy = (const float*) [by contents];
|
||||
// the standard measure for a dot product: relative L2 over the whole result, not per row (a 5120-term float sum
|
||||
// cancels, so per-row relative error is ~1e-3 even when the kernels agree to float precision)
|
||||
double num = 0, den = 0, wrel = 0;
|
||||
for (int r = 0; r < n; ++r) {
|
||||
double acc = 0;
|
||||
for (int kk = 0; kk < k; ++kk) acc += (double) bf16_to_f32(w[(size_t) r * k + kk]) * (double) x[kk];
|
||||
const double d = (double) gy[r] - acc;
|
||||
num += d * d;
|
||||
den += acc * acc;
|
||||
const double rel = fabs(d) / (fabs(acc) > 1e-9 ? fabs(acc) : 1.0);
|
||||
wrel = rel > wrel ? rel : wrel;
|
||||
}
|
||||
const double l2 = sqrt(num / den);
|
||||
printf("rows %d k %d: relative L2 %.3e, worst row %.3e\n", n, k, l2, wrel);
|
||||
printf("%s\n", l2 < 1e-5 ? "PASS" : "FAIL");
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,193 @@
|
||||
// iq2s_t8_probe.mm - the IQ2_S expert kernel, tiled over the 8-token window (16 GB of the 2.25bpw set).
|
||||
//
|
||||
// Block (block_iq2_s, 82 B): f16 d, 32 index bytes, 32 sign bytes, 8 qh bytes, 8 scale bytes. Per group of 32 weights:
|
||||
// db0 = d*(0.5 + (scales[ib] & 0xf))*0.25, db1 = d*(0.5 + (scales[ib] >> 4))*0.25
|
||||
// for l in 0..3: idx = qs[4*ib + l] | ((qh[ib] << (8 - 2l)) & 0x300); grid = iq2s_grid[idx];
|
||||
// sign = signs[4*ib + l]; scale = db[l/2]
|
||||
// (ggml-quants.c dequantize_row_iq2_s, pinned 3cf03257f; signs live at qs + QK_K/8 in the block.)
|
||||
// clang++ -O2 -fobjc-arc -framework Metal -framework Foundation -o iq2s_t8_probe iq2s_t8_probe.mm && ./iq2s_t8_probe
|
||||
#import <Foundation/Foundation.h>
|
||||
#import <Metal/Metal.h>
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <cmath>
|
||||
#include <cstring>
|
||||
#include <cstdint>
|
||||
#include <fstream>
|
||||
#include <regex>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include <mach/mach_time.h>
|
||||
|
||||
static std::vector<uint64_t> table(const std::string& name, size_t expect) {
|
||||
std::ifstream f("ggml_grids.h");
|
||||
std::string all((std::istreambuf_iterator<char>(f)), std::istreambuf_iterator<char>());
|
||||
std::regex re("constant \\w+ " + name + "\\[[^\\]]*\\] = \\{([^}]*)\\}");
|
||||
std::smatch m;
|
||||
if (!std::regex_search(all, m, re)) { fprintf(stderr, "no table %s\n", name.c_str()); exit(2); }
|
||||
std::vector<uint64_t> out;
|
||||
const std::string body = m[1];
|
||||
size_t i = 0;
|
||||
while (i < body.size() && out.size() < expect) {
|
||||
while (i < body.size() && !isalnum((unsigned char) body[i])) ++i;
|
||||
size_t j = i;
|
||||
while (j < body.size() && (isalnum((unsigned char) body[j]) || body[j] == 'x')) ++j;
|
||||
if (j > i) out.push_back(strtoull(body.substr(i, j - i).c_str(), nullptr, 0));
|
||||
i = j;
|
||||
}
|
||||
if (out.size() != expect) { fprintf(stderr, "%s: %zu of %zu\n", name.c_str(), out.size(), expect); exit(2); }
|
||||
return out;
|
||||
}
|
||||
|
||||
int main() {
|
||||
@autoreleasepool {
|
||||
const int n = 65536, k = 5120, T = 8;
|
||||
const int nb = k / 256;
|
||||
const size_t bs = 82;
|
||||
auto grid = table("iq2s_grid", 1024);
|
||||
|
||||
std::vector<uint8_t> w((size_t) n * nb * bs);
|
||||
srand(13);
|
||||
for (size_t i = 0; i < w.size(); ++i) w[i] = (uint8_t) (rand() & 0xff);
|
||||
for (int r = 0; r < n; ++r)
|
||||
for (int b = 0; b < nb; ++b) {
|
||||
_Float16 d = (_Float16) (((rand() % 900) + 100) / 8000.0f);
|
||||
memcpy(&w[((size_t) r * nb + b) * bs], &d, 2);
|
||||
}
|
||||
std::vector<float> x((size_t) T * k), y((size_t) T * n);
|
||||
for (size_t i = 0; i < x.size(); ++i) x[i] = ((rand() % 20001) - 10000) / 8192.0f;
|
||||
|
||||
std::string grids;
|
||||
{
|
||||
std::ifstream f("ggml_grids.h");
|
||||
std::string line;
|
||||
while (std::getline(f, line)) {
|
||||
if (line.rfind("#pragma once", 0) == 0 || line.rfind("//", 0) == 0 || line.empty()) continue;
|
||||
grids += line + "\n";
|
||||
}
|
||||
}
|
||||
std::string src = "#include <metal_stdlib>\nusing namespace metal;\n" + grids + R"MSL(
|
||||
kernel void iq2s_t8(device const uchar* w [[buffer(0)]], device const float* x [[buffer(1)]],
|
||||
device float* y [[buffer(2)]], constant uint& k [[buffer(3)]], constant uint& n [[buffer(4)]],
|
||||
uint gid [[thread_position_in_grid]], uint lane [[thread_index_in_threadgroup]]) {
|
||||
const uint r = gid / 32u;
|
||||
if (r >= n) return;
|
||||
const uint nb = k / 256u;
|
||||
const device uchar* base = w + (size_t) r * nb * 82u;
|
||||
float acc[8];
|
||||
for (int t = 0; t < 8; ++t) acc[t] = 0.0f;
|
||||
for (uint b = lane; b < nb; b += 32u) {
|
||||
const device uchar* blk = base + (size_t) b * 82u;
|
||||
const float d = float(as_type<half>(*(const device ushort*) blk));
|
||||
const device uchar* qs = blk + 2; // 32 index bytes
|
||||
const device uchar* sg = qs + 32; // 32 sign bytes
|
||||
const device uchar* qh = blk + 2 + 64; // 8 qh bytes
|
||||
const device uchar* sc = blk + 2 + 72; // 8 scale bytes
|
||||
for (int ib = 0; ib < 8; ++ib) {
|
||||
const float db0 = d * (0.5f + float(sc[ib] & 0xf)) * 0.25f;
|
||||
const float db1 = d * (0.5f + float(sc[ib] >> 4)) * 0.25f;
|
||||
for (int l = 0; l < 4; ++l) {
|
||||
const uint idx = uint(qs[4 * ib + l]) | ((uint(qh[ib]) << (8 - 2 * l)) & 0x300u);
|
||||
const ulong g = iq2s_grid[idx & 1023u];
|
||||
const uint signs = uint(sg[4 * ib + l]);
|
||||
const float db = (l < 2) ? db0 : db1;
|
||||
const uint xoff = (uint) ib * 32u + (uint) l * 8u;
|
||||
float wv[8];
|
||||
for (int j = 0; j < 8; ++j) {
|
||||
wv[j] = db * float((g >> (8 * j)) & 0xff) * ((signs & kmask_iq2xs[j]) ? -1.0f : 1.0f);
|
||||
}
|
||||
for (uint t = 0; t < 8u; ++t) {
|
||||
const device float* xt = x + (size_t) t * k + xoff;
|
||||
float s = 0.0f;
|
||||
for (int j = 0; j < 8; ++j) s += wv[j] * xt[j];
|
||||
acc[t] += s;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
for (uint t = 0; t < 8u; ++t) {
|
||||
float a = acc[t];
|
||||
a += simd_shuffle_xor(a, 16);
|
||||
a += simd_shuffle_xor(a, 8);
|
||||
a += simd_shuffle_xor(a, 4);
|
||||
a += simd_shuffle_xor(a, 2);
|
||||
a += simd_shuffle_xor(a, 1);
|
||||
if (lane == 0) y[(size_t) t * n + r] = a;
|
||||
}
|
||||
}
|
||||
)MSL";
|
||||
|
||||
id<MTLDevice> dev = MTLCreateSystemDefaultDevice();
|
||||
NSError* err = nil;
|
||||
id<MTLLibrary> lib = [dev newLibraryWithSource:[NSString stringWithUTF8String:src.c_str()] options:nil error:&err];
|
||||
if (!lib) { printf("MSL compile failed: %s\n", [[err localizedDescription] UTF8String]); return 1; }
|
||||
id<MTLComputePipelineState> pso = [dev newComputePipelineStateWithFunction:[lib newFunctionWithName:@"iq2s_t8"] error:&err];
|
||||
if (!pso) { printf("pipeline failed: %s\n", [[err localizedDescription] UTF8String]); return 1; }
|
||||
id<MTLBuffer> bw = [dev newBufferWithBytes:w.data() length:w.size() options:MTLResourceStorageModeShared];
|
||||
id<MTLBuffer> bx = [dev newBufferWithBytes:x.data() length:x.size() * 4 options:MTLResourceStorageModeShared];
|
||||
id<MTLBuffer> by = [dev newBufferWithLength:y.size() * 4 options:MTLResourceStorageModeShared];
|
||||
id<MTLCommandQueue> q = [dev newCommandQueue];
|
||||
unsigned uk = k, un = n;
|
||||
double best = 1e9;
|
||||
mach_timebase_info_data_t tb; mach_timebase_info(&tb);
|
||||
for (int it = 0; it < 5; ++it) {
|
||||
id<MTLCommandBuffer> cb = [q commandBuffer];
|
||||
id<MTLComputeCommandEncoder> ce = [cb computeCommandEncoder];
|
||||
[ce setComputePipelineState:pso];
|
||||
[ce setBuffer:bw offset:0 atIndex:0];
|
||||
[ce setBuffer:bx offset:0 atIndex:1];
|
||||
[ce setBuffer:by offset:0 atIndex:2];
|
||||
[ce setBytes:&uk length:4 atIndex:3];
|
||||
[ce setBytes:&un length:4 atIndex:4];
|
||||
[ce dispatchThreads:MTLSizeMake((NSUInteger) n * 32, 1, 1) threadsPerThreadgroup:MTLSizeMake(32, 1, 1)];
|
||||
[ce endEncoding];
|
||||
const uint64_t t0 = mach_absolute_time();
|
||||
[cb commit];
|
||||
[cb waitUntilCompleted];
|
||||
const double dt = (double) (mach_absolute_time() - t0) * tb.numer / tb.denom / 1e9;
|
||||
if (dt < best) best = dt;
|
||||
}
|
||||
printf("IQ2_S window of 8: %.2f GB/s effective (%.2f raw), %.3f ms for %.1f MB\n", 8.0 * w.size() / best / 1e9,
|
||||
(double) w.size() / best / 1e9, best * 1e3, (double) w.size() / 1e6);
|
||||
|
||||
const float* gy = (const float*) [by contents];
|
||||
const uint8_t* kmask = (const uint8_t*) "\x01\x02\x04\x08\x10\x20\x40\x80";
|
||||
double num = 0, den = 0;
|
||||
for (int r = 0; r < n; r += 61) {
|
||||
for (int t = 0; t < T; ++t) {
|
||||
double acc = 0;
|
||||
for (int b = 0; b < nb; ++b) {
|
||||
const uint8_t* blk = &w[((size_t) r * nb + b) * bs];
|
||||
_Float16 dh; memcpy(&dh, blk, 2);
|
||||
const double d = (double) (float) dh;
|
||||
const uint8_t* qs = blk + 2;
|
||||
const uint8_t* sg = qs + 32;
|
||||
const uint8_t* qh = blk + 2 + 64;
|
||||
const uint8_t* sc = blk + 2 + 72;
|
||||
for (int ib = 0; ib < 8; ++ib) {
|
||||
const double db0 = d * (0.5 + (double) (sc[ib] & 0xf)) * 0.25;
|
||||
const double db1 = d * (0.5 + (double) (sc[ib] >> 4)) * 0.25;
|
||||
for (int l = 0; l < 4; ++l) {
|
||||
const uint32_t idx = (uint32_t) qs[4 * ib + l] | (((uint32_t) qh[ib] << (8 - 2 * l)) & 0x300u);
|
||||
const uint64_t g = grid[idx & 1023u];
|
||||
const uint32_t signs = sg[4 * ib + l];
|
||||
const double db = (l < 2) ? db0 : db1;
|
||||
const int xoff = ib * 32 + l * 8;
|
||||
for (int j = 0; j < 8; ++j) {
|
||||
const double wv = db * (double) ((g >> (8 * j)) & 0xff) * ((signs & kmask[j]) ? -1.0 : 1.0);
|
||||
acc += wv * (double) x[(size_t) t * k + xoff + j];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
const double diff = (double) gy[(size_t) t * n + r] - acc;
|
||||
num += diff * diff;
|
||||
den += acc * acc;
|
||||
}
|
||||
}
|
||||
const double l2 = sqrt(num / den);
|
||||
printf("relative L2 %.3e (sampled rows)\n", l2);
|
||||
printf("%s\n", l2 < 1e-5 ? "PASS" : "FAIL");
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,190 @@
|
||||
// iq2xs_t8_probe.mm - the IQ2_XS expert kernel, tiled over the engine's 8-token window (24 GB of the 2.25bpw set).
|
||||
//
|
||||
// Block (block_iq2_xs, 74 B): f16 d, 32 uint16 qs, 8 bytes of scales. Per group of 32 weights:
|
||||
// db0 = d*(0.5 + (scales[ib] & 0xf))*0.25, db1 = d*(0.5 + (scales[ib] >> 4))*0.25
|
||||
// for l in 0..3: grid = iq2xs_grid[qs[4ib+l] & 511], signs = ksigns_iq2xs[qs[4ib+l] >> 9], scale = db[l/2]
|
||||
// (ggml-quants.c dequantize_row_iq2_xs, pinned 3cf03257f.) Decoded once per window, applied to 8 activations.
|
||||
// clang++ -O2 -fobjc-arc -framework Metal -framework Foundation -o iq2xs_t8_probe iq2xs_t8_probe.mm && ./iq2xs_t8_probe
|
||||
#import <Foundation/Foundation.h>
|
||||
#import <Metal/Metal.h>
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <cmath>
|
||||
#include <cstring>
|
||||
#include <cstdint>
|
||||
#include <fstream>
|
||||
#include <regex>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include <mach/mach_time.h>
|
||||
|
||||
static std::vector<uint64_t> table(const std::string& name, size_t expect) {
|
||||
std::ifstream f("ggml_grids.h");
|
||||
std::string all((std::istreambuf_iterator<char>(f)), std::istreambuf_iterator<char>());
|
||||
std::regex re("constant \\w+ " + name + "\\[[^\\]]*\\] = \\{([^}]*)\\}");
|
||||
std::smatch m;
|
||||
if (!std::regex_search(all, m, re)) { fprintf(stderr, "no table %s\n", name.c_str()); exit(2); }
|
||||
std::vector<uint64_t> out;
|
||||
const std::string body = m[1];
|
||||
size_t i = 0;
|
||||
while (i < body.size() && out.size() < expect) {
|
||||
while (i < body.size() && !isalnum((unsigned char) body[i])) ++i;
|
||||
size_t j = i;
|
||||
while (j < body.size() && (isalnum((unsigned char) body[j]) || body[j] == 'x')) ++j;
|
||||
if (j > i) out.push_back(strtoull(body.substr(i, j - i).c_str(), nullptr, 0));
|
||||
i = j;
|
||||
}
|
||||
if (out.size() != expect) { fprintf(stderr, "%s: %zu of %zu\n", name.c_str(), out.size(), expect); exit(2); }
|
||||
return out;
|
||||
}
|
||||
|
||||
int main() {
|
||||
@autoreleasepool {
|
||||
const int n = 65536, k = 5120, T = 8;
|
||||
const int nb = k / 256;
|
||||
const size_t bs = 74;
|
||||
auto grid = table("iq2xs_grid", 512);
|
||||
auto signs_t = table("ksigns_iq2xs", 128);
|
||||
|
||||
std::vector<uint8_t> w((size_t) n * nb * bs);
|
||||
srand(9);
|
||||
for (size_t i = 0; i < w.size(); ++i) w[i] = (uint8_t) (rand() & 0xff);
|
||||
for (int r = 0; r < n; ++r)
|
||||
for (int b = 0; b < nb; ++b) {
|
||||
_Float16 d = (_Float16) (((rand() % 900) + 100) / 8000.0f);
|
||||
memcpy(&w[((size_t) r * nb + b) * bs], &d, 2);
|
||||
}
|
||||
std::vector<float> x((size_t) T * k), y((size_t) T * n);
|
||||
for (size_t i = 0; i < x.size(); ++i) x[i] = ((rand() % 20001) - 10000) / 8192.0f;
|
||||
|
||||
std::string grids;
|
||||
{
|
||||
std::ifstream f("ggml_grids.h");
|
||||
std::string line;
|
||||
while (std::getline(f, line)) {
|
||||
if (line.rfind("#pragma once", 0) == 0 || line.rfind("//", 0) == 0 || line.empty()) continue;
|
||||
grids += line + "\n";
|
||||
}
|
||||
}
|
||||
std::string src = "#include <metal_stdlib>\nusing namespace metal;\n" + grids + R"MSL(
|
||||
kernel void iq2xs_t8(device const uchar* w [[buffer(0)]], device const float* x [[buffer(1)]],
|
||||
device float* y [[buffer(2)]], constant uint& k [[buffer(3)]], constant uint& n [[buffer(4)]],
|
||||
uint gid [[thread_position_in_grid]], uint lane [[thread_index_in_threadgroup]]) {
|
||||
const uint r = gid / 32u;
|
||||
if (r >= n) return;
|
||||
const uint nb = k / 256u;
|
||||
const device uchar* base = w + (size_t) r * nb * 74u;
|
||||
float acc[8];
|
||||
for (int t = 0; t < 8; ++t) acc[t] = 0.0f;
|
||||
for (uint b = lane; b < nb; b += 32u) {
|
||||
const device uchar* blk = base + (size_t) b * 74u;
|
||||
const float d = float(as_type<half>(*(const device ushort*) blk));
|
||||
const device ushort* qs = (const device ushort*) (blk + 2);
|
||||
const device uchar* sc = blk + 2 + 64;
|
||||
for (int ib = 0; ib < 8; ++ib) {
|
||||
const float db0 = d * (0.5f + float(sc[ib] & 0xf)) * 0.25f;
|
||||
const float db1 = d * (0.5f + float(sc[ib] >> 4)) * 0.25f;
|
||||
for (int l = 0; l < 4; ++l) {
|
||||
const uint q = uint(qs[4 * ib + l]);
|
||||
const ulong g = iq2xs_grid[q & 511u];
|
||||
const uint sg = uint(ksigns_iq2xs[q >> 9]);
|
||||
const float db = (l < 2) ? db0 : db1;
|
||||
const uint xoff = (uint) ib * 32u + (uint) l * 8u;
|
||||
float wv[8];
|
||||
for (int j = 0; j < 8; ++j) {
|
||||
wv[j] = db * float((g >> (8 * j)) & 0xff) * ((sg & kmask_iq2xs[j]) ? -1.0f : 1.0f);
|
||||
}
|
||||
for (uint t = 0; t < 8u; ++t) {
|
||||
const device float* xt = x + (size_t) t * k + xoff;
|
||||
float s = 0.0f;
|
||||
for (int j = 0; j < 8; ++j) s += wv[j] * xt[j];
|
||||
acc[t] += s;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
for (uint t = 0; t < 8u; ++t) {
|
||||
float a = acc[t];
|
||||
a += simd_shuffle_xor(a, 16);
|
||||
a += simd_shuffle_xor(a, 8);
|
||||
a += simd_shuffle_xor(a, 4);
|
||||
a += simd_shuffle_xor(a, 2);
|
||||
a += simd_shuffle_xor(a, 1);
|
||||
if (lane == 0) y[(size_t) t * n + r] = a;
|
||||
}
|
||||
}
|
||||
)MSL";
|
||||
|
||||
id<MTLDevice> dev = MTLCreateSystemDefaultDevice();
|
||||
NSError* err = nil;
|
||||
id<MTLLibrary> lib = [dev newLibraryWithSource:[NSString stringWithUTF8String:src.c_str()] options:nil error:&err];
|
||||
if (!lib) { printf("MSL compile failed: %s\n", [[err localizedDescription] UTF8String]); return 1; }
|
||||
id<MTLComputePipelineState> pso = [dev newComputePipelineStateWithFunction:[lib newFunctionWithName:@"iq2xs_t8"] error:&err];
|
||||
if (!pso) { printf("pipeline failed: %s\n", [[err localizedDescription] UTF8String]); return 1; }
|
||||
id<MTLBuffer> bw = [dev newBufferWithBytes:w.data() length:w.size() options:MTLResourceStorageModeShared];
|
||||
id<MTLBuffer> bx = [dev newBufferWithBytes:x.data() length:x.size() * 4 options:MTLResourceStorageModeShared];
|
||||
id<MTLBuffer> by = [dev newBufferWithLength:y.size() * 4 options:MTLResourceStorageModeShared];
|
||||
id<MTLCommandQueue> q = [dev newCommandQueue];
|
||||
unsigned uk = k, un = n;
|
||||
double best = 1e9;
|
||||
mach_timebase_info_data_t tb; mach_timebase_info(&tb);
|
||||
for (int it = 0; it < 5; ++it) {
|
||||
id<MTLCommandBuffer> cb = [q commandBuffer];
|
||||
id<MTLComputeCommandEncoder> ce = [cb computeCommandEncoder];
|
||||
[ce setComputePipelineState:pso];
|
||||
[ce setBuffer:bw offset:0 atIndex:0];
|
||||
[ce setBuffer:bx offset:0 atIndex:1];
|
||||
[ce setBuffer:by offset:0 atIndex:2];
|
||||
[ce setBytes:&uk length:4 atIndex:3];
|
||||
[ce setBytes:&un length:4 atIndex:4];
|
||||
[ce dispatchThreads:MTLSizeMake((NSUInteger) n * 32, 1, 1) threadsPerThreadgroup:MTLSizeMake(32, 1, 1)];
|
||||
[ce endEncoding];
|
||||
const uint64_t t0 = mach_absolute_time();
|
||||
[cb commit];
|
||||
[cb waitUntilCompleted];
|
||||
const double dt = (double) (mach_absolute_time() - t0) * tb.numer / tb.denom / 1e9;
|
||||
if (dt < best) best = dt;
|
||||
}
|
||||
printf("IQ2_XS window of 8: %.2f GB/s effective (%.2f raw), %.3f ms for %.1f MB\n", 8.0 * w.size() / best / 1e9,
|
||||
(double) w.size() / best / 1e9, best * 1e3, (double) w.size() / 1e6);
|
||||
|
||||
const float* gy = (const float*) [by contents];
|
||||
const uint8_t* kmask = (const uint8_t*) "\x01\x02\x04\x08\x10\x20\x40\x80";
|
||||
double num = 0, den = 0;
|
||||
for (int r = 0; r < n; r += 61) {
|
||||
for (int t = 0; t < T; ++t) {
|
||||
double acc = 0;
|
||||
for (int b = 0; b < nb; ++b) {
|
||||
const uint8_t* blk = &w[((size_t) r * nb + b) * bs];
|
||||
_Float16 dh; memcpy(&dh, blk, 2);
|
||||
const double d = (double) (float) dh;
|
||||
const uint8_t* sc = blk + 2 + 64;
|
||||
const uint8_t* qb = blk + 2;
|
||||
for (int ib = 0; ib < 8; ++ib) {
|
||||
const double db0 = d * (0.5 + (double) (sc[ib] & 0xf)) * 0.25;
|
||||
const double db1 = d * (0.5 + (double) (sc[ib] >> 4)) * 0.25;
|
||||
for (int l = 0; l < 4; ++l) {
|
||||
uint16_t q16;
|
||||
memcpy(&q16, qb + 2 * (4 * ib + l), 2);
|
||||
const uint64_t g = grid[q16 & 511u];
|
||||
const uint32_t sg = (uint32_t) signs_t[(q16 >> 9) & 127u];
|
||||
const double db = (l < 2) ? db0 : db1;
|
||||
const int xoff = ib * 32 + l * 8;
|
||||
for (int j = 0; j < 8; ++j) {
|
||||
const double wv = db * (double) ((g >> (8 * j)) & 0xff) * ((sg & kmask[j]) ? -1.0 : 1.0);
|
||||
acc += wv * (double) x[(size_t) t * k + xoff + j];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
const double diff = (double) gy[(size_t) t * n + r] - acc;
|
||||
num += diff * diff;
|
||||
den += acc * acc;
|
||||
}
|
||||
}
|
||||
const double l2 = sqrt(num / den);
|
||||
printf("relative L2 %.3e (sampled rows)\n", l2);
|
||||
printf("%s\n", l2 < 1e-5 ? "PASS" : "FAIL");
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,185 @@
|
||||
// iq2xxs_probe.mm - the IQ2_XXS expert GEMV in MSL, the 2.25bpw set's dominant expert format (49 GB of the 149 GB).
|
||||
//
|
||||
// Decode (ggml-quants.c dequantize_row_iq2_xxs, pinned 3cf03257f): a block is 66 bytes - an f16 d and 32 bytes of qs. Eight
|
||||
// groups of 32 weights: aux32[0..1] are the group's 8 bytes; db = d * (0.5 + (aux32[1] >> 28)) * 0.25; each of the group's four
|
||||
// sub-blocks takes grid = iq2xxs_grid[aux8[l]] (8 bytes) and signs = ksigns_iq2xs[(aux32[1] >> 7l) & 127], giving
|
||||
// y[j] = db * grid[j] * (signs & kmask_iq2xs[j] ? -1 : 1).
|
||||
//
|
||||
// The grid tables are read from the generated MSL header so the CPU reference uses exactly the same numbers as the GPU.
|
||||
// clang++ -O2 -fobjc-arc -framework Metal -framework Foundation -o iq2xxs_probe iq2xxs_probe.mm && ./iq2xxs_probe
|
||||
#import <Foundation/Foundation.h>
|
||||
#import <Metal/Metal.h>
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <cmath>
|
||||
#include <cstring>
|
||||
#include <cstdint>
|
||||
#include <fstream>
|
||||
#include <regex>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include <mach/mach_time.h>
|
||||
|
||||
static std::vector<uint64_t> load_msl_table(const std::string& path, const std::string& name, size_t expect) {
|
||||
std::ifstream f(path);
|
||||
std::string all((std::istreambuf_iterator<char>(f)), std::istreambuf_iterator<char>());
|
||||
std::regex re("constant \\w+ " + name + "\\[[^\\]]*\\] = \\{([^}]*)\\}");
|
||||
std::smatch m;
|
||||
std::vector<uint64_t> out;
|
||||
if (!std::regex_search(all, m, re)) { fprintf(stderr, "table %s not found\n", name.c_str()); exit(2); }
|
||||
std::string body = m[1];
|
||||
size_t i = 0;
|
||||
while (i < body.size()) {
|
||||
while (i < body.size() && !isdigit((unsigned char) body[i])) ++i;
|
||||
if (i >= body.size()) break;
|
||||
size_t j = i;
|
||||
while (j < body.size() && (isdigit((unsigned char) body[j]) || body[j] == 'x' || isxdigit((unsigned char) body[j]))) ++j;
|
||||
out.push_back(strtoull(body.substr(i, j - i).c_str(), nullptr, 0));
|
||||
i = j;
|
||||
if (out.size() > expect) break;
|
||||
}
|
||||
if (out.size() != expect) { fprintf(stderr, "table %s: %zu of %zu\n", name.c_str(), out.size(), expect); exit(2); }
|
||||
return out;
|
||||
}
|
||||
|
||||
int main() {
|
||||
@autoreleasepool {
|
||||
const int n = 65536, k = 5120; // rows and k
|
||||
const int nb = k / 256; // IQ2_XXS blocks a row
|
||||
const size_t bs = 66; // bytes a block
|
||||
auto grid = load_msl_table("ggml_grids.h", "iq2xxs_grid", 256);
|
||||
auto signs_t = load_msl_table("ggml_grids.h", "ksigns_iq2xs", 128);
|
||||
|
||||
// a synthetic pack of blocks (the decoder does not care where the bytes come from)
|
||||
std::vector<uint8_t> w((size_t) n * nb * bs);
|
||||
srand(3);
|
||||
for (size_t i = 0; i < w.size(); ++i) w[i] = (uint8_t) (rand() & 0xff);
|
||||
for (int r = 0; r < n; ++r)
|
||||
for (int b = 0; b < nb; ++b) {
|
||||
// a sane f16 scale in the block's first two bytes
|
||||
_Float16 d = (_Float16) (((rand() % 900) + 100) / 8000.0f);
|
||||
memcpy(&w[((size_t) r * nb + b) * bs], &d, 2);
|
||||
}
|
||||
std::vector<float> x(k), y(n);
|
||||
for (int i = 0; i < k; ++i) x[i] = ((rand() % 20001) - 10000) / 8192.0f;
|
||||
|
||||
// pull the grid table into the MSL by textual include at compile time
|
||||
std::string grids;
|
||||
{
|
||||
std::ifstream f("ggml_grids.h");
|
||||
std::string line;
|
||||
while (std::getline(f, line)) {
|
||||
if (line.rfind("#pragma once", 0) == 0 || line.rfind("//", 0) == 0 || line.empty()) continue;
|
||||
grids += line + "\n";
|
||||
}
|
||||
}
|
||||
std::string src = "#include <metal_stdlib>\nusing namespace metal;\n" + grids + R"MSL(
|
||||
kernel void iq2xxs_gemv(device const uchar* w [[buffer(0)]], device const float* x [[buffer(1)]],
|
||||
device float* y [[buffer(2)]], constant uint& k [[buffer(3)]], constant uint& n [[buffer(4)]],
|
||||
uint gid [[thread_position_in_grid]], uint lane [[thread_index_in_threadgroup]]) {
|
||||
const uint r = gid / 32u;
|
||||
if (r >= n) return;
|
||||
const uint nb = k / 256u;
|
||||
const device uchar* base = w + (size_t) r * nb * 66u;
|
||||
float acc = 0.0f;
|
||||
for (uint b = lane; b < nb; b += 32u) {
|
||||
const device uchar* blk = base + (size_t) b * 66u;
|
||||
const float d = float(as_type<half>(*(const device ushort*) blk));
|
||||
for (int ib = 0; ib < 8; ++ib) {
|
||||
// groups advance 4 bytes and read 8, so an 8-byte vector load is misaligned on odd groups: read bytes
|
||||
const uint aux0 = uint(blk[2 + 4 * ib]) | (uint(blk[3 + 4 * ib]) << 8) | (uint(blk[4 + 4 * ib]) << 16) | (uint(blk[5 + 4 * ib]) << 24);
|
||||
const uint aux1 = uint(blk[6 + 4 * ib]) | (uint(blk[7 + 4 * ib]) << 8) | (uint(blk[8 + 4 * ib]) << 16) | (uint(blk[9 + 4 * ib]) << 24);
|
||||
const float db = d * (0.5f + float(aux1 >> 28)) * 0.25f;
|
||||
for (int l = 0; l < 4; ++l) {
|
||||
const ulong g = iq2xxs_grid[(aux0 >> (8 * l)) & 0xff];
|
||||
const uint sg = uint(ksigns_iq2xs[(aux1 >> (7 * l)) & 127]);
|
||||
const uint xoff = (uint) ib * 32u + (uint) l * 8u;
|
||||
for (int j = 0; j < 8; ++j) {
|
||||
const float wv = db * float((g >> (8 * j)) & 0xff) * ((sg & kmask_iq2xs[j]) ? -1.0f : 1.0f);
|
||||
acc += wv * x[xoff + j];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
acc += simd_shuffle_xor(acc, 16);
|
||||
acc += simd_shuffle_xor(acc, 8);
|
||||
acc += simd_shuffle_xor(acc, 4);
|
||||
acc += simd_shuffle_xor(acc, 2);
|
||||
acc += simd_shuffle_xor(acc, 1);
|
||||
if (lane == 0) y[r] = acc;
|
||||
}
|
||||
)MSL";
|
||||
const uint8_t kmask[8] = {1, 2, 4, 8, 16, 32, 64, 128};
|
||||
// kmask_iq2xs comes from ggml_grids.h
|
||||
std::string src_with_km = src;
|
||||
|
||||
id<MTLDevice> dev = MTLCreateSystemDefaultDevice();
|
||||
NSError* err = nil;
|
||||
id<MTLLibrary> lib = [dev newLibraryWithSource:[NSString stringWithUTF8String:src_with_km.c_str()] options:nil error:&err];
|
||||
if (!lib) { printf("MSL compile failed: %s\n", [[err localizedDescription] UTF8String]); return 1; }
|
||||
id<MTLComputePipelineState> pso = [dev newComputePipelineStateWithFunction:[lib newFunctionWithName:@"iq2xxs_gemv"] error:&err];
|
||||
if (!pso) { printf("pipeline failed: %s\n", [[err localizedDescription] UTF8String]); return 1; }
|
||||
|
||||
id<MTLBuffer> bw = [dev newBufferWithBytes:w.data() length:w.size() options:MTLResourceStorageModeShared];
|
||||
id<MTLBuffer> bx = [dev newBufferWithBytes:x.data() length:k * 4 options:MTLResourceStorageModeShared];
|
||||
id<MTLBuffer> by = [dev newBufferWithLength:n * 4 options:MTLResourceStorageModeShared];
|
||||
id<MTLCommandQueue> q = [dev newCommandQueue];
|
||||
unsigned uk = k, un = n;
|
||||
double best = 1e9;
|
||||
mach_timebase_info_data_t tb; mach_timebase_info(&tb);
|
||||
for (int it = 0; it < 5; ++it) {
|
||||
id<MTLCommandBuffer> cb = [q commandBuffer];
|
||||
id<MTLComputeCommandEncoder> ce = [cb computeCommandEncoder];
|
||||
[ce setComputePipelineState:pso];
|
||||
[ce setBuffer:bw offset:0 atIndex:0];
|
||||
[ce setBuffer:bx offset:0 atIndex:1];
|
||||
[ce setBuffer:by offset:0 atIndex:2];
|
||||
[ce setBytes:&uk length:4 atIndex:3];
|
||||
[ce setBytes:&un length:4 atIndex:4];
|
||||
[ce dispatchThreads:MTLSizeMake((NSUInteger) n * 32, 1, 1) threadsPerThreadgroup:MTLSizeMake(32, 1, 1)];
|
||||
[ce endEncoding];
|
||||
const uint64_t t0 = mach_absolute_time();
|
||||
[cb commit];
|
||||
[cb waitUntilCompleted];
|
||||
const double dt = (double) (mach_absolute_time() - t0) * tb.numer / tb.denom / 1e9;
|
||||
if (dt < best) best = dt;
|
||||
}
|
||||
printf("throughput: %.2f GB/s (%.3f ms for %.1f MB)\n", (double) w.size() / best / 1e9, best * 1e3,
|
||||
(double) w.size() / 1e6);
|
||||
|
||||
// the CPU reference: the same decode, from the same tables
|
||||
const float* gy = (const float*) [by contents];
|
||||
double num = 0, den = 0;
|
||||
for (int r = 0; r < n; ++r) {
|
||||
double acc = 0;
|
||||
for (int b = 0; b < nb; ++b) {
|
||||
const uint8_t* blk = &w[((size_t) r * nb + b) * bs];
|
||||
_Float16 dh;
|
||||
memcpy(&dh, blk, 2);
|
||||
const double d = (double) (float) dh;
|
||||
for (int ib = 0; ib < 8; ++ib) {
|
||||
uint32_t aux[2];
|
||||
memcpy(aux, blk + 2 + 4 * ib, 8);
|
||||
const double db = d * (0.5 + (double) (aux[1] >> 28)) * 0.25;
|
||||
const uint8_t* a8 = (const uint8_t*) aux;
|
||||
for (int l = 0; l < 4; ++l) {
|
||||
const uint64_t g = grid[a8[l]];
|
||||
const uint32_t sg = (uint32_t) signs_t[(aux[1] >> (7 * l)) & 127];
|
||||
const int xoff = ib * 32 + l * 8;
|
||||
for (int j = 0; j < 8; ++j) {
|
||||
const double wv = db * (double) ((g >> (8 * j)) & 0xff) * ((sg & kmask[j]) ? -1.0 : 1.0);
|
||||
acc += wv * (double) x[xoff + j];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
const double diff = (double) gy[r] - acc;
|
||||
num += diff * diff;
|
||||
den += acc * acc;
|
||||
}
|
||||
const double l2 = sqrt(num / den);
|
||||
printf("relative L2 %.3e\n", l2);
|
||||
printf("%s\n", l2 < 1e-4 ? "PASS" : "FAIL");
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
Loaded 100 of 258 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user